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 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: []