omq-ffi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +38 -0
- data/lib/omq/ffi/engine.rb +448 -0
- data/lib/omq/ffi/libzmq.rb +128 -0
- data/lib/omq/ffi.rb +12 -0
- metadata +71 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 88e0942ce849494a8af0baa0cc5d894a27c6c64c7ea2e15a726a203df262296d
|
|
4
|
+
data.tar.gz: 4d9e966c9bbdb5317112dd1aed5f7037f4b911f3096cfe3e103f75dbd019588d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 83f0e5f7e637634e5d07a56d91f4e3887b4d542b3cad88fc2189755b3857b01693871a10f0a5a2c4939981b634acd3a7915e8cd247947694fdb9c3afe4ce74d6
|
|
7
|
+
data.tar.gz: 4f1e5da9f453d8fa90816b16c712e718e1ad7855be0dfa53fa18582cbb52c217057034e2dbe1c432b073c12a0d50debc21989321817e4462bec927637c2fdfeb
|
data/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# OMQ::FFI -- libzmq Backend for OMQ
|
|
2
|
+
|
|
3
|
+
Use libzmq under the same OMQ socket API. Requires libzmq 4.x installed
|
|
4
|
+
on the system.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
require "omq"
|
|
8
|
+
require "omq/ffi"
|
|
9
|
+
|
|
10
|
+
push = OMQ::PUSH.new(backend: :ffi)
|
|
11
|
+
push.connect("tcp://127.0.0.1:5555")
|
|
12
|
+
push.send("hello from libzmq")
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The FFI backend replaces the entire pure-Ruby ZMTP stack with libzmq.
|
|
16
|
+
The socket API, options, and Async integration remain identical. A
|
|
17
|
+
dedicated I/O thread per socket handles all libzmq operations (libzmq
|
|
18
|
+
sockets are not thread-safe).
|
|
19
|
+
|
|
20
|
+
## Interop
|
|
21
|
+
|
|
22
|
+
FFI and native (pure Ruby) backends are wire-compatible. You can mix
|
|
23
|
+
them freely:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# native REP server
|
|
27
|
+
rep = OMQ::REP.bind("tcp://127.0.0.1:5555")
|
|
28
|
+
|
|
29
|
+
# FFI REQ client
|
|
30
|
+
req = OMQ::REQ.new(backend: :ffi)
|
|
31
|
+
req.connect("tcp://127.0.0.1:5555")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Ruby >= 3.3
|
|
37
|
+
- libzmq 4.x (`libzmq5` / `libzmq3-dev` on Debian/Ubuntu)
|
|
38
|
+
- [omq](https://github.com/paddor/omq) >= 0.10
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
|
|
5
|
+
module OMQ
|
|
6
|
+
module FFI
|
|
7
|
+
# FFI Engine — wraps a libzmq socket to implement the OMQ Engine contract.
|
|
8
|
+
#
|
|
9
|
+
# A dedicated I/O thread owns the zmq_socket exclusively (libzmq sockets
|
|
10
|
+
# are not thread-safe). Send and recv flow through queues, with an IO pipe
|
|
11
|
+
# to wake the Async fiber scheduler.
|
|
12
|
+
#
|
|
13
|
+
class Engine
|
|
14
|
+
L = Libzmq
|
|
15
|
+
|
|
16
|
+
attr_reader :options, :last_endpoint, :last_tcp_port, :connections, :routing
|
|
17
|
+
attr_reader :peer_connected, :all_peers_gone
|
|
18
|
+
attr_writer :reconnect_enabled
|
|
19
|
+
|
|
20
|
+
# Routing stub that delegates subscribe/unsubscribe/join/leave to
|
|
21
|
+
# libzmq socket options via the I/O thread.
|
|
22
|
+
#
|
|
23
|
+
class RoutingStub
|
|
24
|
+
attr_reader :subscriber_joined
|
|
25
|
+
|
|
26
|
+
def initialize(engine)
|
|
27
|
+
@engine = engine
|
|
28
|
+
@subscriber_joined = Async::Promise.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def subscribe(prefix)
|
|
32
|
+
@engine.send_cmd(:subscribe, prefix.b)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def unsubscribe(prefix)
|
|
36
|
+
@engine.send_cmd(:unsubscribe, prefix.b)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def join(group)
|
|
40
|
+
@engine.send_cmd(:join, group)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def leave(group)
|
|
44
|
+
@engine.send_cmd(:leave, group)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param socket_type [Symbol] e.g. :REQ, :PAIR
|
|
49
|
+
# @param options [Options]
|
|
50
|
+
#
|
|
51
|
+
def initialize(socket_type, options)
|
|
52
|
+
@socket_type = socket_type
|
|
53
|
+
@options = options
|
|
54
|
+
@last_endpoint = nil
|
|
55
|
+
@last_tcp_port = nil
|
|
56
|
+
@peer_connected = Async::Promise.new
|
|
57
|
+
@all_peers_gone = Async::Promise.new
|
|
58
|
+
@connections = []
|
|
59
|
+
@closed = false
|
|
60
|
+
@parent_task = nil
|
|
61
|
+
@on_io_thread = false
|
|
62
|
+
|
|
63
|
+
@zmq_socket = L.zmq_socket(OMQ::FFI.context, L::SOCKET_TYPES.fetch(@socket_type))
|
|
64
|
+
raise "zmq_socket failed: #{L.zmq_strerror(L.zmq_errno)}" if @zmq_socket.null?
|
|
65
|
+
|
|
66
|
+
apply_options
|
|
67
|
+
|
|
68
|
+
@routing = RoutingStub.new(self)
|
|
69
|
+
|
|
70
|
+
# Queues for cross-thread communication
|
|
71
|
+
@send_queue = Thread::Queue.new # main → io thread
|
|
72
|
+
@recv_queue = Thread::Queue.new # io thread → main
|
|
73
|
+
@cmd_queue = Thread::Queue.new # control commands → io thread
|
|
74
|
+
|
|
75
|
+
# Signal pipe: io thread → Async fiber (message received)
|
|
76
|
+
@recv_signal_r, @recv_signal_w = IO.pipe
|
|
77
|
+
# Wake pipe: main thread → io thread (send/cmd enqueued)
|
|
78
|
+
@wake_r, @wake_w = IO.pipe
|
|
79
|
+
|
|
80
|
+
@io_thread = nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# --- Socket lifecycle ---
|
|
84
|
+
|
|
85
|
+
def bind(endpoint)
|
|
86
|
+
sync_identity
|
|
87
|
+
send_cmd(:bind, endpoint)
|
|
88
|
+
@last_endpoint = get_string_option(L::ZMQ_LAST_ENDPOINT)
|
|
89
|
+
@last_tcp_port = extract_tcp_port(@last_endpoint)
|
|
90
|
+
@connections << :ffi
|
|
91
|
+
@peer_connected.resolve(:ffi) unless @peer_connected.resolved?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def connect(endpoint)
|
|
95
|
+
sync_identity
|
|
96
|
+
send_cmd(:connect, endpoint)
|
|
97
|
+
@last_endpoint ||= endpoint
|
|
98
|
+
@last_tcp_port ||= extract_tcp_port(endpoint)
|
|
99
|
+
@connections << :ffi
|
|
100
|
+
@peer_connected.resolve(:ffi) unless @peer_connected.resolved?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def disconnect(endpoint)
|
|
104
|
+
send_cmd(:disconnect, endpoint)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def unbind(endpoint)
|
|
108
|
+
send_cmd(:unbind, endpoint)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def close
|
|
112
|
+
return if @closed
|
|
113
|
+
@closed = true
|
|
114
|
+
if @io_thread
|
|
115
|
+
@cmd_queue.push([:stop])
|
|
116
|
+
wake_io_thread
|
|
117
|
+
@io_thread.join(2)
|
|
118
|
+
else
|
|
119
|
+
# IO thread never started — close socket directly
|
|
120
|
+
L.zmq_close(@zmq_socket)
|
|
121
|
+
end
|
|
122
|
+
@recv_signal_r&.close rescue nil
|
|
123
|
+
@recv_signal_w&.close rescue nil
|
|
124
|
+
@wake_r&.close rescue nil
|
|
125
|
+
@wake_w&.close rescue nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def capture_parent_task
|
|
129
|
+
return if @parent_task
|
|
130
|
+
if Async::Task.current?
|
|
131
|
+
@parent_task = Async::Task.current
|
|
132
|
+
else
|
|
133
|
+
@parent_task = Reactor.root_task
|
|
134
|
+
@on_io_thread = true
|
|
135
|
+
Reactor.track_linger(@options.linger)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# --- Send ---
|
|
140
|
+
|
|
141
|
+
def enqueue_send(parts)
|
|
142
|
+
ensure_io_thread
|
|
143
|
+
@send_queue.push(parts)
|
|
144
|
+
wake_io_thread
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# --- Recv ---
|
|
148
|
+
|
|
149
|
+
def dequeue_recv_batch(max)
|
|
150
|
+
ensure_io_thread
|
|
151
|
+
msg = wait_for_message
|
|
152
|
+
batch = [msg]
|
|
153
|
+
while batch.size < max
|
|
154
|
+
begin
|
|
155
|
+
batch << @recv_queue.pop(true)
|
|
156
|
+
rescue ThreadError
|
|
157
|
+
break
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
batch
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def dequeue_recv_sentinel
|
|
164
|
+
@recv_queue.push(nil)
|
|
165
|
+
@recv_signal_w.write_nonblock(".", exception: false) rescue nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Send a control command to the I/O thread.
|
|
169
|
+
# @api private
|
|
170
|
+
#
|
|
171
|
+
def send_cmd(cmd, *args)
|
|
172
|
+
ensure_io_thread
|
|
173
|
+
result = Thread::Queue.new
|
|
174
|
+
@cmd_queue.push([cmd, args, result])
|
|
175
|
+
wake_io_thread
|
|
176
|
+
r = result.pop
|
|
177
|
+
raise r if r.is_a?(Exception)
|
|
178
|
+
r
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def wake_io_thread
|
|
182
|
+
@wake_w.write_nonblock(".", exception: false)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
# Waits for a message from the I/O thread's recv queue.
|
|
188
|
+
# Uses the signal pipe so Async can yield the fiber.
|
|
189
|
+
#
|
|
190
|
+
def wait_for_message
|
|
191
|
+
loop do
|
|
192
|
+
begin
|
|
193
|
+
return @recv_queue.pop(true)
|
|
194
|
+
rescue ThreadError
|
|
195
|
+
# empty
|
|
196
|
+
end
|
|
197
|
+
@recv_signal_r.wait_readable
|
|
198
|
+
@recv_signal_r.read_nonblock(256, exception: false)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def ensure_io_thread
|
|
203
|
+
return if @io_thread
|
|
204
|
+
@io_thread = Thread.new { io_loop }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The I/O loop runs on a dedicated thread. It owns the zmq_socket
|
|
208
|
+
# exclusively and processes commands, sends, and recvs.
|
|
209
|
+
#
|
|
210
|
+
def io_loop
|
|
211
|
+
zmq_fd_io = IO.for_fd(get_zmq_fd, autoclose: false)
|
|
212
|
+
|
|
213
|
+
loop do
|
|
214
|
+
drain_cmds or break
|
|
215
|
+
drain_sends
|
|
216
|
+
try_recv
|
|
217
|
+
|
|
218
|
+
# Block until ZMQ or wake pipe has activity.
|
|
219
|
+
IO.select([zmq_fd_io, @wake_r], nil, nil, 0.1)
|
|
220
|
+
@wake_r.read_nonblock(4096, exception: false)
|
|
221
|
+
end
|
|
222
|
+
rescue
|
|
223
|
+
# Thread exit
|
|
224
|
+
ensure
|
|
225
|
+
zmq_fd_io&.close rescue nil
|
|
226
|
+
L.zmq_close(@zmq_socket)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def zmq_has_events?
|
|
230
|
+
@events_buf ||= ::FFI::MemoryPointer.new(:int)
|
|
231
|
+
@events_len ||= ::FFI::MemoryPointer.new(:size_t).tap { |p| p.write(:size_t, ::FFI.type_size(:int)) }
|
|
232
|
+
L.zmq_getsockopt(@zmq_socket, L::ZMQ_EVENTS, @events_buf, @events_len)
|
|
233
|
+
@events_buf.read_int != 0
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def drain_cmds
|
|
237
|
+
loop do
|
|
238
|
+
begin
|
|
239
|
+
cmd = @cmd_queue.pop(true)
|
|
240
|
+
rescue ThreadError
|
|
241
|
+
return true # queue empty, continue
|
|
242
|
+
end
|
|
243
|
+
return false unless process_cmd(cmd)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def process_cmd(cmd)
|
|
248
|
+
name, args, result = cmd
|
|
249
|
+
case name
|
|
250
|
+
when :stop
|
|
251
|
+
result&.push(nil)
|
|
252
|
+
return false
|
|
253
|
+
when :bind
|
|
254
|
+
rc = L.zmq_bind(@zmq_socket, args[0])
|
|
255
|
+
result&.push(rc >= 0 ? nil : RuntimeError.new(L.zmq_strerror(L.zmq_errno)))
|
|
256
|
+
when :connect
|
|
257
|
+
rc = L.zmq_connect(@zmq_socket, args[0])
|
|
258
|
+
result&.push(rc >= 0 ? nil : RuntimeError.new(L.zmq_strerror(L.zmq_errno)))
|
|
259
|
+
when :disconnect
|
|
260
|
+
rc = L.zmq_disconnect(@zmq_socket, args[0])
|
|
261
|
+
result&.push(rc >= 0 ? nil : RuntimeError.new(L.zmq_strerror(L.zmq_errno)))
|
|
262
|
+
when :unbind
|
|
263
|
+
rc = L.zmq_unbind(@zmq_socket, args[0])
|
|
264
|
+
result&.push(rc >= 0 ? nil : RuntimeError.new(L.zmq_strerror(L.zmq_errno)))
|
|
265
|
+
when :set_identity
|
|
266
|
+
set_bytes_option(L::ZMQ_IDENTITY, args[0])
|
|
267
|
+
result&.push(nil)
|
|
268
|
+
when :subscribe
|
|
269
|
+
set_bytes_option(L::ZMQ_SUBSCRIBE, args[0])
|
|
270
|
+
result&.push(nil)
|
|
271
|
+
when :unsubscribe
|
|
272
|
+
set_bytes_option(L::ZMQ_UNSUBSCRIBE, args[0])
|
|
273
|
+
result&.push(nil)
|
|
274
|
+
when :join
|
|
275
|
+
rc = L.respond_to?(:zmq_join) ? L.zmq_join(@zmq_socket, args[0]) : -1
|
|
276
|
+
result&.push(rc >= 0 ? nil : RuntimeError.new("zmq_join not available"))
|
|
277
|
+
when :leave
|
|
278
|
+
rc = L.respond_to?(:zmq_leave) ? L.zmq_leave(@zmq_socket, args[0]) : -1
|
|
279
|
+
result&.push(rc >= 0 ? nil : RuntimeError.new("zmq_leave not available"))
|
|
280
|
+
when :drain_send
|
|
281
|
+
# handled in drain_sends
|
|
282
|
+
result&.push(nil)
|
|
283
|
+
end
|
|
284
|
+
true
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def try_recv
|
|
288
|
+
loop do
|
|
289
|
+
parts = recv_multipart_nonblock
|
|
290
|
+
break unless parts
|
|
291
|
+
@recv_queue.push(parts.freeze)
|
|
292
|
+
@recv_signal_w.write_nonblock(".", exception: false)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def drain_sends
|
|
297
|
+
@pending_send ||= nil
|
|
298
|
+
loop do
|
|
299
|
+
parts = @pending_send || begin
|
|
300
|
+
@send_queue.pop(true)
|
|
301
|
+
rescue ThreadError
|
|
302
|
+
break
|
|
303
|
+
end
|
|
304
|
+
if send_multipart_nonblock(parts)
|
|
305
|
+
@pending_send = nil
|
|
306
|
+
else
|
|
307
|
+
@pending_send = parts # retry next cycle (HWM reached)
|
|
308
|
+
break
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Returns true if fully sent, false if would block (HWM).
|
|
314
|
+
#
|
|
315
|
+
def send_multipart_nonblock(parts)
|
|
316
|
+
parts.each_with_index do |part, i|
|
|
317
|
+
flags = L::ZMQ_DONTWAIT
|
|
318
|
+
flags |= L::ZMQ_SNDMORE if i < parts.size - 1
|
|
319
|
+
msg = L.alloc_msg
|
|
320
|
+
L.zmq_msg_init_size(msg, part.bytesize)
|
|
321
|
+
L.zmq_msg_data(msg).write_bytes(part)
|
|
322
|
+
rc = L.zmq_msg_send(msg, @zmq_socket, flags)
|
|
323
|
+
if rc < 0
|
|
324
|
+
L.zmq_msg_close(msg)
|
|
325
|
+
return false # EAGAIN — would block
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
true
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def recv_multipart_nonblock
|
|
332
|
+
parts = []
|
|
333
|
+
loop do
|
|
334
|
+
msg = L.alloc_msg
|
|
335
|
+
L.zmq_msg_init(msg)
|
|
336
|
+
rc = L.zmq_msg_recv(msg, @zmq_socket, L::ZMQ_DONTWAIT)
|
|
337
|
+
if rc < 0
|
|
338
|
+
L.zmq_msg_close(msg)
|
|
339
|
+
return parts.empty? ? nil : parts # EAGAIN = no more data
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
size = L.zmq_msg_size(msg)
|
|
343
|
+
data = L.zmq_msg_data(msg).read_bytes(size)
|
|
344
|
+
L.zmq_msg_close(msg)
|
|
345
|
+
parts << data.freeze
|
|
346
|
+
|
|
347
|
+
break unless rcvmore?
|
|
348
|
+
end
|
|
349
|
+
parts.empty? ? nil : parts
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def rcvmore?
|
|
353
|
+
buf = ::FFI::MemoryPointer.new(:int)
|
|
354
|
+
len = ::FFI::MemoryPointer.new(:size_t)
|
|
355
|
+
len.write(:size_t, ::FFI.type_size(:int))
|
|
356
|
+
L.zmq_getsockopt(@zmq_socket, L::ZMQ_RCVMORE, buf, len)
|
|
357
|
+
buf.read_int != 0
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def get_zmq_fd
|
|
361
|
+
buf = ::FFI::MemoryPointer.new(:int)
|
|
362
|
+
len = ::FFI::MemoryPointer.new(:size_t)
|
|
363
|
+
len.write(:size_t, ::FFI.type_size(:int))
|
|
364
|
+
L.zmq_getsockopt(@zmq_socket, L::ZMQ_FD, buf, len)
|
|
365
|
+
buf.read_int
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Re-syncs identity to libzmq (user may set it after construction).
|
|
369
|
+
#
|
|
370
|
+
def sync_identity
|
|
371
|
+
id = @options.identity
|
|
372
|
+
if id && !id.empty?
|
|
373
|
+
send_cmd(:set_identity, id)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def apply_options
|
|
378
|
+
set_int_option(L::ZMQ_SNDHWM, @options.send_hwm)
|
|
379
|
+
set_int_option(L::ZMQ_RCVHWM, @options.recv_hwm)
|
|
380
|
+
set_int_option(L::ZMQ_LINGER, (@options.linger * 1000).to_i)
|
|
381
|
+
set_int_option(L::ZMQ_CONFLATE, @options.conflate ? 1 : 0)
|
|
382
|
+
|
|
383
|
+
if @options.identity && !@options.identity.empty?
|
|
384
|
+
set_bytes_option(L::ZMQ_IDENTITY, @options.identity)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
if @options.max_message_size
|
|
388
|
+
set_int64_option(L::ZMQ_MAXMSGSIZE, @options.max_message_size)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
if @options.reconnect_interval
|
|
392
|
+
ivl = @options.reconnect_interval
|
|
393
|
+
if ivl.is_a?(Range)
|
|
394
|
+
set_int_option(L::ZMQ_RECONNECT_IVL, (ivl.begin * 1000).to_i)
|
|
395
|
+
set_int_option(L::ZMQ_RECONNECT_IVL_MAX, (ivl.end * 1000).to_i)
|
|
396
|
+
else
|
|
397
|
+
set_int_option(L::ZMQ_RECONNECT_IVL, (ivl * 1000).to_i)
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
set_int_option(L::ZMQ_ROUTER_MANDATORY, 1) if @options.router_mandatory
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def set_int_option(opt, value)
|
|
405
|
+
buf = ::FFI::MemoryPointer.new(:int)
|
|
406
|
+
buf.write_int(value)
|
|
407
|
+
L.zmq_setsockopt(@zmq_socket, opt, buf, ::FFI.type_size(:int))
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def set_int64_option(opt, value)
|
|
411
|
+
buf = ::FFI::MemoryPointer.new(:int64)
|
|
412
|
+
buf.write_int64(value)
|
|
413
|
+
L.zmq_setsockopt(@zmq_socket, opt, buf, ::FFI.type_size(:int64))
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def set_bytes_option(opt, value)
|
|
417
|
+
buf = ::FFI::MemoryPointer.from_string(value)
|
|
418
|
+
L.zmq_setsockopt(@zmq_socket, opt, buf, value.bytesize)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def get_string_option(opt)
|
|
422
|
+
buf = ::FFI::MemoryPointer.new(:char, 256)
|
|
423
|
+
len = ::FFI::MemoryPointer.new(:size_t)
|
|
424
|
+
len.write(:size_t, 256)
|
|
425
|
+
L.check!(L.zmq_getsockopt(@zmq_socket, opt, buf, len), "zmq_getsockopt")
|
|
426
|
+
buf.read_string(len.read(:size_t) - 1)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def extract_tcp_port(endpoint)
|
|
430
|
+
return nil unless endpoint
|
|
431
|
+
port = endpoint.split(":").last.to_i
|
|
432
|
+
port.positive? ? port : nil
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Shared ZMQ context — one per process.
|
|
437
|
+
#
|
|
438
|
+
def self.context
|
|
439
|
+
@context ||= Libzmq.zmq_ctx_new.tap do |ctx|
|
|
440
|
+
raise "zmq_ctx_new failed" if ctx.null?
|
|
441
|
+
at_exit do
|
|
442
|
+
Libzmq.zmq_ctx_shutdown(ctx) rescue nil
|
|
443
|
+
Libzmq.zmq_ctx_term(ctx) rescue nil
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
module OMQ
|
|
6
|
+
module FFI
|
|
7
|
+
# Minimal libzmq FFI bindings — only what OMQ needs.
|
|
8
|
+
#
|
|
9
|
+
module Libzmq
|
|
10
|
+
extend ::FFI::Library
|
|
11
|
+
ffi_lib ["libzmq.so.5", "libzmq.5.dylib", "libzmq"]
|
|
12
|
+
|
|
13
|
+
# Context
|
|
14
|
+
attach_function :zmq_ctx_new, [], :pointer
|
|
15
|
+
attach_function :zmq_ctx_term, [:pointer], :int
|
|
16
|
+
attach_function :zmq_ctx_shutdown, [:pointer], :int
|
|
17
|
+
|
|
18
|
+
# Socket
|
|
19
|
+
attach_function :zmq_socket, [:pointer, :int], :pointer
|
|
20
|
+
attach_function :zmq_close, [:pointer], :int
|
|
21
|
+
attach_function :zmq_bind, [:pointer, :string], :int
|
|
22
|
+
attach_function :zmq_connect, [:pointer, :string], :int
|
|
23
|
+
attach_function :zmq_disconnect, [:pointer, :string], :int
|
|
24
|
+
attach_function :zmq_unbind, [:pointer, :string], :int
|
|
25
|
+
|
|
26
|
+
# Message
|
|
27
|
+
attach_function :zmq_msg_init, [:pointer], :int
|
|
28
|
+
attach_function :zmq_msg_init_size, [:pointer, :size_t], :int
|
|
29
|
+
attach_function :zmq_msg_data, [:pointer], :pointer
|
|
30
|
+
attach_function :zmq_msg_size, [:pointer], :size_t
|
|
31
|
+
attach_function :zmq_msg_close, [:pointer], :int
|
|
32
|
+
attach_function :zmq_msg_send, [:pointer, :pointer, :int], :int
|
|
33
|
+
attach_function :zmq_msg_recv, [:pointer, :pointer, :int], :int
|
|
34
|
+
|
|
35
|
+
# Socket options
|
|
36
|
+
attach_function :zmq_setsockopt, [:pointer, :int, :pointer, :size_t], :int
|
|
37
|
+
attach_function :zmq_getsockopt, [:pointer, :int, :pointer, :pointer], :int
|
|
38
|
+
|
|
39
|
+
# Group membership (RADIO/DISH) — draft API, may not be available
|
|
40
|
+
begin
|
|
41
|
+
attach_function :zmq_join, [:pointer, :string], :int
|
|
42
|
+
attach_function :zmq_leave, [:pointer, :string], :int
|
|
43
|
+
rescue ::FFI::NotFoundError
|
|
44
|
+
# libzmq built without ZMQ_BUILD_DRAFT_API
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Error
|
|
48
|
+
attach_function :zmq_errno, [], :int
|
|
49
|
+
attach_function :zmq_strerror, [:int], :string
|
|
50
|
+
|
|
51
|
+
# Socket types
|
|
52
|
+
ZMQ_PAIR = 0
|
|
53
|
+
ZMQ_PUB = 1
|
|
54
|
+
ZMQ_SUB = 2
|
|
55
|
+
ZMQ_REQ = 3
|
|
56
|
+
ZMQ_REP = 4
|
|
57
|
+
ZMQ_DEALER = 5
|
|
58
|
+
ZMQ_ROUTER = 6
|
|
59
|
+
ZMQ_PULL = 7
|
|
60
|
+
ZMQ_PUSH = 8
|
|
61
|
+
ZMQ_XPUB = 9
|
|
62
|
+
ZMQ_XSUB = 10
|
|
63
|
+
ZMQ_SERVER = 12
|
|
64
|
+
ZMQ_CLIENT = 13
|
|
65
|
+
ZMQ_RADIO = 14
|
|
66
|
+
ZMQ_DISH = 15
|
|
67
|
+
ZMQ_GATHER = 16
|
|
68
|
+
ZMQ_SCATTER = 17
|
|
69
|
+
ZMQ_PEER = 19
|
|
70
|
+
ZMQ_CHANNEL = 20
|
|
71
|
+
|
|
72
|
+
# Socket type name → constant
|
|
73
|
+
SOCKET_TYPES = {
|
|
74
|
+
PAIR: ZMQ_PAIR, PUB: ZMQ_PUB, SUB: ZMQ_SUB,
|
|
75
|
+
REQ: ZMQ_REQ, REP: ZMQ_REP,
|
|
76
|
+
DEALER: ZMQ_DEALER, ROUTER: ZMQ_ROUTER,
|
|
77
|
+
PULL: ZMQ_PULL, PUSH: ZMQ_PUSH,
|
|
78
|
+
XPUB: ZMQ_XPUB, XSUB: ZMQ_XSUB,
|
|
79
|
+
SERVER: ZMQ_SERVER, CLIENT: ZMQ_CLIENT,
|
|
80
|
+
RADIO: ZMQ_RADIO, DISH: ZMQ_DISH,
|
|
81
|
+
GATHER: ZMQ_GATHER, SCATTER: ZMQ_SCATTER,
|
|
82
|
+
PEER: ZMQ_PEER, CHANNEL: ZMQ_CHANNEL,
|
|
83
|
+
}.freeze
|
|
84
|
+
|
|
85
|
+
# Send/recv flags
|
|
86
|
+
ZMQ_DONTWAIT = 1
|
|
87
|
+
ZMQ_SNDMORE = 2
|
|
88
|
+
|
|
89
|
+
# Socket options
|
|
90
|
+
ZMQ_IDENTITY = 5
|
|
91
|
+
ZMQ_SUBSCRIBE = 6
|
|
92
|
+
ZMQ_UNSUBSCRIBE = 7
|
|
93
|
+
ZMQ_RCVMORE = 13
|
|
94
|
+
ZMQ_FD = 14
|
|
95
|
+
ZMQ_EVENTS = 15
|
|
96
|
+
ZMQ_LINGER = 17
|
|
97
|
+
ZMQ_SNDHWM = 23
|
|
98
|
+
ZMQ_RCVHWM = 24
|
|
99
|
+
ZMQ_RCVTIMEO = 27
|
|
100
|
+
ZMQ_SNDTIMEO = 28
|
|
101
|
+
ZMQ_MAXMSGSIZE = 22
|
|
102
|
+
ZMQ_LAST_ENDPOINT = 32
|
|
103
|
+
ZMQ_ROUTER_MANDATORY = 33
|
|
104
|
+
ZMQ_RECONNECT_IVL = 18
|
|
105
|
+
ZMQ_RECONNECT_IVL_MAX = 21
|
|
106
|
+
ZMQ_CONFLATE = 54
|
|
107
|
+
|
|
108
|
+
# zmq_msg_t is 64 bytes on all platforms
|
|
109
|
+
MSG_T_SIZE = 64
|
|
110
|
+
|
|
111
|
+
# Allocates a zmq_msg_t on the heap.
|
|
112
|
+
#
|
|
113
|
+
# @return [FFI::MemoryPointer]
|
|
114
|
+
#
|
|
115
|
+
def self.alloc_msg
|
|
116
|
+
::FFI::MemoryPointer.new(MSG_T_SIZE)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Raises an error with the current zmq_errno message.
|
|
120
|
+
#
|
|
121
|
+
def self.check!(rc, label = "zmq")
|
|
122
|
+
return rc if rc >= 0
|
|
123
|
+
errno = zmq_errno
|
|
124
|
+
raise "#{label}: #{zmq_strerror(errno)} (errno #{errno})"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/omq/ffi.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Load the FFI backend for OMQ.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# require "omq/ffi"
|
|
7
|
+
# push = OMQ::PUSH.new(backend: :ffi)
|
|
8
|
+
#
|
|
9
|
+
# Raises LoadError if libzmq is not installed.
|
|
10
|
+
|
|
11
|
+
require_relative "ffi/libzmq"
|
|
12
|
+
require_relative "ffi/engine"
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: omq-ffi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Patrik Wenger
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: omq
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.11'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.11'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ffi
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
email:
|
|
41
|
+
- paddor@gmail.com
|
|
42
|
+
executables: []
|
|
43
|
+
extensions: []
|
|
44
|
+
extra_rdoc_files: []
|
|
45
|
+
files:
|
|
46
|
+
- README.md
|
|
47
|
+
- lib/omq/ffi.rb
|
|
48
|
+
- lib/omq/ffi/engine.rb
|
|
49
|
+
- lib/omq/ffi/libzmq.rb
|
|
50
|
+
homepage: https://github.com/paddor/omq-ffi
|
|
51
|
+
licenses:
|
|
52
|
+
- ISC
|
|
53
|
+
metadata: {}
|
|
54
|
+
rdoc_options: []
|
|
55
|
+
require_paths:
|
|
56
|
+
- lib
|
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.3'
|
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
requirements: []
|
|
68
|
+
rubygems_version: 4.0.6
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: libzmq FFI backend for OMQ — use libzmq under the same OMQ socket API
|
|
71
|
+
test_files: []
|