omq 0.23.0 → 0.26.2

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -3
  3. data/README.md +23 -7
  4. data/lib/omq/client_server.rb +1 -1
  5. data/lib/omq/engine/connection_lifecycle.rb +12 -6
  6. data/lib/omq/engine/heartbeat.rb +1 -1
  7. data/lib/omq/engine/recv_pump.rb +29 -16
  8. data/lib/omq/engine.rb +6 -5
  9. data/lib/omq/ffi/engine.rb +646 -0
  10. data/lib/omq/ffi/libzmq.rb +134 -0
  11. data/lib/omq/ffi.rb +12 -0
  12. data/lib/omq/peer.rb +1 -1
  13. data/lib/omq/radio_dish.rb +1 -1
  14. data/lib/omq/readable.rb +5 -1
  15. data/lib/omq/routing/channel.rb +4 -4
  16. data/lib/omq/routing/client.rb +2 -2
  17. data/lib/omq/routing/conn_send_pump.rb +1 -1
  18. data/lib/omq/routing/dealer.rb +2 -2
  19. data/lib/omq/routing/dish.rb +2 -2
  20. data/lib/omq/routing/fan_out.rb +7 -7
  21. data/lib/omq/routing/gather.rb +2 -2
  22. data/lib/omq/routing/pair.rb +4 -4
  23. data/lib/omq/routing/peer.rb +2 -2
  24. data/lib/omq/routing/pub.rb +2 -2
  25. data/lib/omq/routing/pull.rb +2 -2
  26. data/lib/omq/routing/push.rb +3 -3
  27. data/lib/omq/routing/radio.rb +2 -2
  28. data/lib/omq/routing/rep.rb +2 -2
  29. data/lib/omq/routing/req.rb +2 -2
  30. data/lib/omq/routing/round_robin.rb +4 -4
  31. data/lib/omq/routing/router.rb +2 -2
  32. data/lib/omq/routing/scatter.rb +4 -5
  33. data/lib/omq/routing/server.rb +2 -2
  34. data/lib/omq/routing/sub.rb +2 -2
  35. data/lib/omq/routing/xpub.rb +2 -2
  36. data/lib/omq/routing/xsub.rb +2 -2
  37. data/lib/omq/socket.rb +2 -1
  38. data/lib/omq/transport/inproc/{direct_pipe.rb → pipe.rb} +22 -9
  39. data/lib/omq/transport/inproc.rb +26 -14
  40. data/lib/omq/transport/udp.rb +1 -1
  41. data/lib/omq/version.rb +1 -1
  42. data/lib/omq/writable.rb +32 -43
  43. metadata +5 -2
@@ -0,0 +1,646 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "uri"
5
+
6
+ module OMQ
7
+ module FFI
8
+ # FFI Engine — wraps a libzmq socket to implement the OMQ Engine contract.
9
+ #
10
+ # A dedicated I/O thread owns the zmq_socket exclusively (libzmq sockets
11
+ # are not thread-safe). Send and recv flow through queues, with an IO pipe
12
+ # to wake the Async fiber scheduler.
13
+ #
14
+ class Engine
15
+ L = Libzmq
16
+
17
+
18
+ # @return [Options] socket options
19
+ attr_reader :options
20
+ # @return [Array] active connections
21
+ attr_reader :connections
22
+ # @return [RoutingStub] subscription/group routing interface
23
+ attr_reader :routing
24
+ # @return [Async::Promise] resolved when the first peer connects
25
+ attr_reader :peer_connected
26
+ # @return [Async::Promise] resolved when all peers have disconnected
27
+ attr_reader :all_peers_gone
28
+ # @return [Async::Task, nil] root of the engine's task tree
29
+ attr_reader :parent_task
30
+ # @return [Boolean] true when the engine's parent task lives on the
31
+ # shared {OMQ::Reactor} IO thread (i.e. not created under an
32
+ # Async task). Writable/Readable check this to pick the fast path.
33
+ attr_reader :on_io_thread
34
+ alias on_io_thread? on_io_thread
35
+ # @param value [Boolean] enables or disables automatic reconnection
36
+ attr_writer :reconnect_enabled
37
+ # @note Monitor events are not yet emitted by the FFI backend; these
38
+ # writers exist so Socket#monitor can attach without raising. Wiring
39
+ # libzmq's zmq_socket_monitor is a TODO.
40
+ attr_writer :monitor_queue, :verbose_monitor
41
+
42
+ # Routing stub that delegates subscribe/unsubscribe/join/leave to
43
+ # libzmq socket options via the I/O thread.
44
+ #
45
+ class RoutingStub
46
+ # @return [Async::Promise] resolved when a subscriber joins
47
+ attr_reader :subscriber_joined
48
+
49
+ # @param engine [Engine] the parent engine instance
50
+ def initialize(engine)
51
+ @engine = engine
52
+ @subscriber_joined = Async::Promise.new
53
+ end
54
+
55
+
56
+ # Subscribes to messages matching the given prefix.
57
+ #
58
+ # @param prefix [String] subscription prefix
59
+ # @return [void]
60
+ def subscribe(prefix)
61
+ @engine.send_cmd(:subscribe, prefix.b)
62
+ end
63
+
64
+
65
+ # Removes a subscription for the given prefix.
66
+ #
67
+ # @param prefix [String] subscription prefix to remove
68
+ # @return [void]
69
+ def unsubscribe(prefix)
70
+ @engine.send_cmd(:unsubscribe, prefix.b)
71
+ end
72
+
73
+
74
+ # Joins a DISH group for receiving RADIO messages.
75
+ #
76
+ # @param group [String] group name
77
+ # @return [void]
78
+ def join(group)
79
+ @engine.send_cmd(:join, group)
80
+ end
81
+
82
+
83
+ # Leaves a DISH group.
84
+ #
85
+ # @param group [String] group name
86
+ # @return [void]
87
+ def leave(group)
88
+ @engine.send_cmd(:leave, group)
89
+ end
90
+ end
91
+
92
+
93
+ # Maps an OMQ +linger+ value (seconds, or +nil+/+Float::INFINITY+
94
+ # for "wait forever") to libzmq's ZMQ_LINGER int milliseconds
95
+ # (-1 = infinite, 0 = drop, N = N ms).
96
+ #
97
+ # @param linger [Numeric, nil]
98
+ # @return [Integer]
99
+ #
100
+ def self.linger_to_zmq_ms(linger)
101
+ return -1 if linger.nil? || linger == Float::INFINITY
102
+ (linger * 1000).to_i
103
+ end
104
+
105
+
106
+ # @param socket_type [Symbol] e.g. :REQ, :PAIR
107
+ # @param options [Options]
108
+ #
109
+ def initialize(socket_type, options)
110
+ @socket_type = socket_type
111
+ @options = options
112
+ @peer_connected = Async::Promise.new
113
+ @all_peers_gone = Async::Promise.new
114
+ @connections = []
115
+ @closed = false
116
+ @parent_task = nil
117
+ @on_io_thread = false
118
+
119
+ @zmq_socket = L.zmq_socket(OMQ::FFI.context, L::SOCKET_TYPES.fetch(@socket_type))
120
+ raise "zmq_socket failed: #{L.zmq_strerror(L.zmq_errno)}" if @zmq_socket.null?
121
+
122
+ apply_options
123
+
124
+ @routing = RoutingStub.new(self)
125
+
126
+ # Queues for cross-thread communication
127
+ @send_queue = Thread::Queue.new # main → io thread
128
+ @recv_queue = Thread::Queue.new # io thread → main
129
+ @cmd_queue = Thread::Queue.new # control commands → io thread
130
+
131
+ # Signal pipe: io thread → Async fiber (message received)
132
+ @recv_signal_r, @recv_signal_w = IO.pipe
133
+ # Wake pipe: main thread → io thread (send/cmd enqueued)
134
+ @wake_r, @wake_w = IO.pipe
135
+
136
+ @io_thread = nil
137
+ end
138
+
139
+
140
+ # --- Socket lifecycle ---
141
+
142
+ # Binds the socket to the given endpoint.
143
+ #
144
+ # @param endpoint [String] ZMQ endpoint URL (e.g. "tcp://*:5555")
145
+ # @return [URI::Generic] resolved endpoint URI (with auto-selected port for "tcp://host:0")
146
+ def bind(endpoint)
147
+ sync_identity
148
+ send_cmd(:bind, endpoint)
149
+ resolved = get_string_option(L::ZMQ_LAST_ENDPOINT)
150
+ @connections << :ffi
151
+ @peer_connected.resolve(:ffi) unless @peer_connected.resolved?
152
+ URI.parse(resolved)
153
+ end
154
+
155
+
156
+ # Connects the socket to the given endpoint.
157
+ #
158
+ # @param endpoint [String] ZMQ endpoint URL
159
+ # @return [URI::Generic] parsed endpoint URI
160
+ def connect(endpoint)
161
+ sync_identity
162
+ send_cmd(:connect, endpoint)
163
+ @connections << :ffi
164
+ @peer_connected.resolve(:ffi) unless @peer_connected.resolved?
165
+ URI.parse(endpoint)
166
+ end
167
+
168
+
169
+ # Disconnects from the given endpoint.
170
+ #
171
+ # @param endpoint [String] ZMQ endpoint URL
172
+ # @return [void]
173
+ def disconnect(endpoint)
174
+ send_cmd(:disconnect, endpoint)
175
+ end
176
+
177
+
178
+ # Unbinds from the given endpoint.
179
+ #
180
+ # @param endpoint [String] ZMQ endpoint URL
181
+ # @return [void]
182
+ def unbind(endpoint)
183
+ send_cmd(:unbind, endpoint)
184
+ end
185
+
186
+
187
+ # Subscribes to a topic prefix (SUB/XSUB). Delegates to the routing
188
+ # stub for API parity with the pure-Ruby Engine.
189
+ #
190
+ # @param prefix [String]
191
+ # @return [void]
192
+ def subscribe(prefix)
193
+ @routing.subscribe(prefix)
194
+ end
195
+
196
+
197
+ # Unsubscribes from a topic prefix (SUB/XSUB).
198
+ #
199
+ # @param prefix [String]
200
+ # @return [void]
201
+ def unsubscribe(prefix)
202
+ @routing.unsubscribe(prefix)
203
+ end
204
+
205
+
206
+ # @return [Async::Promise] resolved when a subscriber joins (PUB/XPUB).
207
+ def subscriber_joined
208
+ @routing.subscriber_joined
209
+ end
210
+
211
+
212
+ # Closes the socket and shuts down the I/O thread.
213
+ #
214
+ # Honors `options.linger`:
215
+ # nil → wait forever for Ruby-side queue to drain into libzmq
216
+ # and for libzmq's own LINGER to flush to the network
217
+ # 0 → drop anything not yet in libzmq's kernel buffers, close fast
218
+ # N → up to N seconds for drain + N + 1s grace for join
219
+ #
220
+ # @return [void]
221
+ def close
222
+ return if @closed
223
+ @closed = true
224
+ if @io_thread
225
+ @cmd_queue.push([:stop])
226
+ wake_io_thread
227
+ linger = @options.linger
228
+ if linger.nil?
229
+ @io_thread.join
230
+ elsif linger.zero?
231
+ @io_thread.join(0.5) # fast path: zmq_close is non-blocking with LINGER=0
232
+ else
233
+ @io_thread.join(linger + 1.0)
234
+ end
235
+ @io_thread.kill if @io_thread.alive? # hard stop if deadline exceeded
236
+ else
237
+ # IO thread never started — close socket directly
238
+ L.zmq_close(@zmq_socket)
239
+ end
240
+ @recv_signal_r&.close rescue nil
241
+ @recv_signal_w&.close rescue nil
242
+ @wake_r&.close rescue nil
243
+ @wake_w&.close rescue nil
244
+ end
245
+
246
+
247
+ # Captures the current Async task as the parent for I/O scheduling.
248
+ # +parent:+ is accepted for API compatibility with the pure-Ruby
249
+ # engine but has no effect: the FFI backend runs its own I/O
250
+ # thread and doesn't participate in the Async barrier tree.
251
+ #
252
+ # @return [void]
253
+ def capture_parent_task(parent: nil)
254
+ return if @parent_task
255
+ if parent
256
+ @parent_task = parent
257
+ elsif Async::Task.current?
258
+ @parent_task = Async::Task.current
259
+ else
260
+ @parent_task = Reactor.root_task
261
+ @on_io_thread = true
262
+ Reactor.track_linger(@options.linger)
263
+ end
264
+ end
265
+
266
+
267
+ # --- Send ---
268
+
269
+ # Enqueues a multipart message for sending via the I/O thread.
270
+ #
271
+ # @param parts [Array<String>] message frames
272
+ # @return [void]
273
+ def enqueue_send(parts)
274
+ ensure_io_thread
275
+ @send_queue.push(parts)
276
+ wake_io_thread
277
+ end
278
+
279
+
280
+ # --- Recv ---
281
+
282
+ # Dequeues the next received message, blocking until one is available.
283
+ #
284
+ # @return [Array<String>] multipart message
285
+ def dequeue_recv
286
+ ensure_io_thread
287
+ wait_for_message
288
+ end
289
+
290
+
291
+ # Pushes a nil sentinel into the recv queue to unblock a waiting consumer.
292
+ #
293
+ # @return [void]
294
+ def dequeue_recv_sentinel
295
+ @recv_queue.push(nil)
296
+ @recv_signal_w.write_nonblock(".", exception: false) rescue nil
297
+ end
298
+
299
+
300
+ # Send a control command to the I/O thread.
301
+ # @api private
302
+ #
303
+ def send_cmd(cmd, *args)
304
+ ensure_io_thread
305
+ result = Thread::Queue.new
306
+ @cmd_queue.push([cmd, args, result])
307
+ wake_io_thread
308
+ r = result.pop
309
+ raise r if r.is_a?(Exception)
310
+ r
311
+ end
312
+
313
+
314
+ # Wakes the I/O thread via the internal pipe.
315
+ #
316
+ # @return [void]
317
+ def wake_io_thread
318
+ @wake_w.write_nonblock(".", exception: false)
319
+ end
320
+
321
+ private
322
+
323
+ # Waits for a message from the I/O thread's recv queue.
324
+ # Uses the signal pipe so Async can yield the fiber.
325
+ #
326
+ def wait_for_message
327
+ loop do
328
+ begin
329
+ return @recv_queue.pop(true)
330
+ rescue ThreadError
331
+ # empty
332
+ end
333
+ @recv_signal_r.wait_readable
334
+ @recv_signal_r.read_nonblock(256, exception: false)
335
+ end
336
+ end
337
+
338
+
339
+ def ensure_io_thread
340
+ return if @io_thread
341
+ @io_thread = Thread.new { io_loop }
342
+ end
343
+
344
+
345
+ # The I/O loop runs on a dedicated thread. It owns the zmq_socket
346
+ # exclusively and processes commands, sends, and recvs.
347
+ #
348
+ def io_loop
349
+ zmq_fd_io = IO.for_fd(get_zmq_fd, autoclose: false)
350
+
351
+ loop do
352
+ drain_cmds or break
353
+ drain_sends
354
+ try_recv
355
+
356
+ # Block until ZMQ or wake pipe has activity.
357
+ IO.select([zmq_fd_io, @wake_r], nil, nil, 0.1)
358
+ @wake_r.read_nonblock(4096, exception: false)
359
+ end
360
+ rescue
361
+ # Thread exit
362
+ ensure
363
+ # Drain Ruby-side send queue into libzmq, bounded by linger deadline.
364
+ # Then re-apply current linger to libzmq (user may have changed it
365
+ # after apply_options ran in initialize) and zmq_close uses it to
366
+ # flush libzmq's own queue to TCP.
367
+ drain_sends_with_deadline(zmq_fd_io, shutdown_deadline) rescue nil
368
+ set_int_option(L::ZMQ_LINGER, Engine.linger_to_zmq_ms(@options.linger)) rescue nil
369
+ zmq_fd_io&.close rescue nil
370
+ L.zmq_close(@zmq_socket)
371
+ end
372
+
373
+
374
+ # Returns a monotonic deadline for the Ruby-side drain phase, or nil
375
+ # for infinite, or the current clock for "drop immediately".
376
+ #
377
+ def shutdown_deadline
378
+ linger = @options.linger
379
+ return nil if linger.nil?
380
+ now = Async::Clock.now
381
+ return now if linger.zero?
382
+ now + linger
383
+ end
384
+
385
+
386
+ # Retries drain_sends with IO.select until either the Ruby-side queue
387
+ # is empty or the deadline is hit. nil deadline = wait forever.
388
+ #
389
+ def drain_sends_with_deadline(zmq_fd_io, deadline)
390
+ loop do
391
+ drain_sends
392
+ break if @pending_send.nil? && @send_queue.empty?
393
+ if deadline
394
+ remaining = deadline - Async::Clock.now
395
+ break if remaining <= 0
396
+ IO.select([zmq_fd_io], nil, nil, [remaining, 0.1].min)
397
+ else
398
+ IO.select([zmq_fd_io], nil, nil, 0.1)
399
+ end
400
+ end
401
+ end
402
+
403
+
404
+ def zmq_has_events?
405
+ @events_buf ||= ::FFI::MemoryPointer.new(:int)
406
+ @events_len ||= ::FFI::MemoryPointer.new(:size_t).tap { |p| p.write(:size_t, ::FFI.type_size(:int)) }
407
+ L.zmq_getsockopt(@zmq_socket, L::ZMQ_EVENTS, @events_buf, @events_len)
408
+ @events_buf.read_int != 0
409
+ end
410
+
411
+
412
+ def drain_cmds
413
+ loop do
414
+ begin
415
+ cmd = @cmd_queue.pop(true)
416
+ rescue ThreadError
417
+ return true # queue empty, continue
418
+ end
419
+ return false unless process_cmd(cmd)
420
+ end
421
+ end
422
+
423
+
424
+ def process_cmd(cmd)
425
+ name, args, result = cmd
426
+ case name
427
+ when :stop
428
+ result&.push(nil)
429
+ return false
430
+ when :bind
431
+ rc = L.zmq_bind(@zmq_socket, args[0])
432
+ result&.push(rc >= 0 ? nil : syscall_error)
433
+ when :connect
434
+ rc = L.zmq_connect(@zmq_socket, args[0])
435
+ result&.push(rc >= 0 ? nil : syscall_error)
436
+ when :disconnect
437
+ rc = L.zmq_disconnect(@zmq_socket, args[0])
438
+ result&.push(rc >= 0 ? nil : syscall_error)
439
+ when :unbind
440
+ rc = L.zmq_unbind(@zmq_socket, args[0])
441
+ result&.push(rc >= 0 ? nil : syscall_error)
442
+ when :set_identity
443
+ set_bytes_option(L::ZMQ_IDENTITY, args[0])
444
+ result&.push(nil)
445
+ when :subscribe
446
+ set_bytes_option(L::ZMQ_SUBSCRIBE, args[0])
447
+ result&.push(nil)
448
+ when :unsubscribe
449
+ set_bytes_option(L::ZMQ_UNSUBSCRIBE, args[0])
450
+ result&.push(nil)
451
+ when :join
452
+ rc = L.respond_to?(:zmq_join) ? L.zmq_join(@zmq_socket, args[0]) : -1
453
+ result&.push(rc >= 0 ? nil : RuntimeError.new("zmq_join not available"))
454
+ when :leave
455
+ rc = L.respond_to?(:zmq_leave) ? L.zmq_leave(@zmq_socket, args[0]) : -1
456
+ result&.push(rc >= 0 ? nil : RuntimeError.new("zmq_leave not available"))
457
+ when :drain_send
458
+ # handled in drain_sends
459
+ result&.push(nil)
460
+ end
461
+ true
462
+ end
463
+
464
+
465
+ def try_recv
466
+ loop do
467
+ parts = recv_multipart_nonblock
468
+ break unless parts
469
+ @recv_queue.push(parts.freeze)
470
+ @recv_signal_w.write_nonblock(".", exception: false)
471
+ end
472
+ end
473
+
474
+
475
+ def drain_sends
476
+ @pending_send ||= nil
477
+ loop do
478
+ parts = @pending_send || begin
479
+ @send_queue.pop(true)
480
+ rescue ThreadError
481
+ break
482
+ end
483
+ if send_multipart_nonblock(parts)
484
+ @pending_send = nil
485
+ else
486
+ @pending_send = parts # retry next cycle (HWM reached)
487
+ break
488
+ end
489
+ end
490
+ end
491
+
492
+
493
+ # Returns true if fully sent, false if would block (HWM).
494
+ #
495
+ def send_multipart_nonblock(parts)
496
+ parts.each_with_index do |part, i|
497
+ flags = L::ZMQ_DONTWAIT
498
+ flags |= L::ZMQ_SNDMORE if i < parts.size - 1
499
+ msg = L.alloc_msg
500
+ L.zmq_msg_init_size(msg, part.bytesize)
501
+ L.zmq_msg_data(msg).write_bytes(part)
502
+ rc = L.zmq_msg_send(msg, @zmq_socket, flags)
503
+ if rc < 0
504
+ L.zmq_msg_close(msg)
505
+ return false # EAGAIN — would block
506
+ end
507
+ end
508
+ true
509
+ end
510
+
511
+
512
+ def recv_multipart_nonblock
513
+ parts = []
514
+ loop do
515
+ msg = L.alloc_msg
516
+ L.zmq_msg_init(msg)
517
+ rc = L.zmq_msg_recv(msg, @zmq_socket, L::ZMQ_DONTWAIT)
518
+ if rc < 0
519
+ L.zmq_msg_close(msg)
520
+ return parts.empty? ? nil : parts # EAGAIN = no more data
521
+ end
522
+
523
+ size = L.zmq_msg_size(msg)
524
+ data = L.zmq_msg_data(msg).read_bytes(size)
525
+ L.zmq_msg_close(msg)
526
+ parts << data.freeze
527
+
528
+ break unless rcvmore?
529
+ end
530
+ parts.empty? ? nil : parts
531
+ end
532
+
533
+
534
+ def rcvmore?
535
+ buf = ::FFI::MemoryPointer.new(:int)
536
+ len = ::FFI::MemoryPointer.new(:size_t)
537
+ len.write(:size_t, ::FFI.type_size(:int))
538
+ L.zmq_getsockopt(@zmq_socket, L::ZMQ_RCVMORE, buf, len)
539
+ buf.read_int != 0
540
+ end
541
+
542
+
543
+ def get_zmq_fd
544
+ buf = ::FFI::MemoryPointer.new(:int)
545
+ len = ::FFI::MemoryPointer.new(:size_t)
546
+ len.write(:size_t, ::FFI.type_size(:int))
547
+ L.zmq_getsockopt(@zmq_socket, L::ZMQ_FD, buf, len)
548
+ buf.read_int
549
+ end
550
+
551
+
552
+ # Re-syncs identity to libzmq (user may set it after construction).
553
+ #
554
+ def sync_identity
555
+ id = @options.identity
556
+ if id && !id.empty?
557
+ send_cmd(:set_identity, id)
558
+ end
559
+ end
560
+
561
+
562
+ def apply_options
563
+ set_int_option(L::ZMQ_SNDHWM, @options.send_hwm)
564
+ set_int_option(L::ZMQ_RCVHWM, @options.recv_hwm)
565
+ set_int_option(L::ZMQ_LINGER, Engine.linger_to_zmq_ms(@options.linger))
566
+ set_int_option(L::ZMQ_CONFLATE, @options.conflate ? 1 : 0)
567
+
568
+ if @options.identity && !@options.identity.empty?
569
+ set_bytes_option(L::ZMQ_IDENTITY, @options.identity)
570
+ end
571
+
572
+ if @options.max_message_size
573
+ set_int64_option(L::ZMQ_MAXMSGSIZE, @options.max_message_size)
574
+ end
575
+
576
+ if @options.reconnect_interval
577
+ ivl = @options.reconnect_interval
578
+ if ivl.is_a?(Range)
579
+ set_int_option(L::ZMQ_RECONNECT_IVL, (ivl.begin * 1000).to_i)
580
+ set_int_option(L::ZMQ_RECONNECT_IVL_MAX, (ivl.end * 1000).to_i)
581
+ else
582
+ set_int_option(L::ZMQ_RECONNECT_IVL, (ivl * 1000).to_i)
583
+ end
584
+ end
585
+
586
+ set_int_option(L::ZMQ_ROUTER_MANDATORY, 1) if @options.router_mandatory
587
+ end
588
+
589
+
590
+ def set_int_option(opt, value)
591
+ buf = ::FFI::MemoryPointer.new(:int)
592
+ buf.write_int(value)
593
+ L.zmq_setsockopt(@zmq_socket, opt, buf, ::FFI.type_size(:int))
594
+ end
595
+
596
+
597
+ def set_int64_option(opt, value)
598
+ buf = ::FFI::MemoryPointer.new(:int64)
599
+ buf.write_int64(value)
600
+ L.zmq_setsockopt(@zmq_socket, opt, buf, ::FFI.type_size(:int64))
601
+ end
602
+
603
+
604
+ def set_bytes_option(opt, value)
605
+ buf = ::FFI::MemoryPointer.from_string(value)
606
+ L.zmq_setsockopt(@zmq_socket, opt, buf, value.bytesize)
607
+ end
608
+
609
+
610
+ def get_string_option(opt)
611
+ buf = ::FFI::MemoryPointer.new(:char, 256)
612
+ len = ::FFI::MemoryPointer.new(:size_t)
613
+ len.write(:size_t, 256)
614
+ L.check!(L.zmq_getsockopt(@zmq_socket, opt, buf, len), "zmq_getsockopt")
615
+ buf.read_string(len.read(:size_t) - 1)
616
+ end
617
+
618
+
619
+ # Builds an Errno::XXX exception from the current zmq_errno so callers
620
+ # can rescue the same classes they would from the pure-Ruby backend
621
+ # (e.g. `Errno::EADDRINUSE`, `Errno::ECONNREFUSED`). Falls back to a
622
+ # plain SystemCallError when the errno is libzmq-specific.
623
+ #
624
+ def syscall_error
625
+ errno = L.zmq_errno
626
+ SystemCallError.new(L.zmq_strerror(errno), errno)
627
+ end
628
+
629
+
630
+ end
631
+
632
+
633
+ # Returns the shared ZMQ context (one per process, lazily initialized).
634
+ #
635
+ # @return [FFI::Pointer] zmq context pointer
636
+ def self.context
637
+ @context ||= Libzmq.zmq_ctx_new.tap do |ctx|
638
+ raise "zmq_ctx_new failed" if ctx.null?
639
+ at_exit do
640
+ Libzmq.zmq_ctx_shutdown(ctx) rescue nil
641
+ Libzmq.zmq_ctx_term(ctx) rescue nil
642
+ end
643
+ end
644
+ end
645
+ end
646
+ end