omq-rs 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: deaed5d4439c9c15105ab96ca99b1589052d80bdf0730d07aff19f7b146cdc47
4
+ data.tar.gz: 116dc7537c497dc57a59a5348966dc6c5f82bf3dea038ddc488eef7aa6efb470
5
+ SHA512:
6
+ metadata.gz: c82ec7acb65d986cdc8f0d6289ac8fd952b256cbe8b02276d0b1a84e4ff06fbb9f00dafa2373091bdcd17daeaffc53c4fff6a69d713a68d4b9aa9446e0c5bbbe
7
+ data.tar.gz: e8b55702563c7849a521545c387ef91fbf632b329efad536105af4306d67f6fae31d914f3755c7be71639cd864241843d358417570c0451f5003a441f4b35f56
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ - Add standalone `omq-rs` Ruby binding with all 20 OMQ socket types.
6
+ - Add PLAIN and CURVE sockets, Z85 key generation, CURVE client authentication,
7
+ and pyzmq CURVE interoperability.
8
+ - Add LZ4/zstd transports and compression options.
9
+ - Add socket lifecycle monitoring with Fiber-aware waits.
10
+ - Add receive wake and monitor-fd hooks for backend adapters.
11
+ - Support Ractor-owned sockets and cross-Ractor inproc/TCP messaging on Ruby 4.
12
+ - Support synchronous sockets on TruffleRuby and test them in CI.
data/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Patrik Wenger
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # omq-rs
2
+
3
+ Fast Ruby binding for [OMQ.rs](https://github.com/paddor/omq.rs). No libzmq,
4
+ FFI, or broker. Networking runs on OMQ-owned Tokio threads. The Ruby API is
5
+ synchronous; its waits cooperate with an installed `Fiber.scheduler`.
6
+
7
+ MRI 3.3+, MRI 4.0+, and TruffleRuby are supported. TruffleRuby 34 does not
8
+ provide Ruby's `Fiber.scheduler` API, so waits block the calling thread there.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ gem install omq-rs
14
+ ```
15
+
16
+ Source installs require Rust 1.93 or newer.
17
+
18
+ ## Usage
19
+
20
+ ```ruby
21
+ require "omq/rs"
22
+
23
+ pull = OMQ.rs(:pull)
24
+ push = OMQ.rs(:push)
25
+
26
+ endpoint = pull.bind("tcp://127.0.0.1:0")
27
+ push.connect(endpoint).wait_for_peer(timeout: 2)
28
+
29
+ push << "hello"
30
+ p pull.recv # => ["hello"]
31
+
32
+ push.close
33
+ pull.close
34
+ ```
35
+
36
+ Socket classes are also available directly:
37
+
38
+ ```ruby
39
+ push = OMQ::Rust::PUSH.new
40
+ pull = OMQ.rs::PULL.new
41
+ ```
42
+
43
+ `#send` accepts one frame, multiple arguments, or an Array. `#recv` always
44
+ returns an Array of frozen binary Strings. SERVER messages prepend a numeric
45
+ routing ID: `[routing_id, body]`. ROUTER, STREAM, and PEER use their normal
46
+ identity frame. RADIO/DISH messages use `[group, body]`.
47
+
48
+ `SERVER#peer_info(routing_id)` returns connection metadata for a live route,
49
+ including `:peer_address` and `:peer_identity`, or `nil` for a stale route.
50
+
51
+ All 20 socket types are available: REQ, REP, PUB, SUB, XPUB, XSUB, PUSH,
52
+ PULL, DEALER, ROUTER, PAIR, STREAM, CLIENT, SERVER, RADIO, DISH, SCATTER,
53
+ GATHER, CHANNEL, and PEER.
54
+
55
+ Published gems include PLAIN, CURVE, LZ4, zstd, and WebSocket support. Check
56
+ features with `OMQ::Rust.has(:curve)`.
57
+
58
+ ## CURVE
59
+
60
+ Keys use the standard 40-byte Z85 representation.
61
+
62
+ ```ruby
63
+ server_public, server_secret = OMQ::Rust.curve_keypair
64
+ client_public, client_secret = OMQ::Rust.curve_keypair
65
+
66
+ pull = OMQ.rs(
67
+ :pull,
68
+ curve_server: true,
69
+ curve_publickey: server_public,
70
+ curve_secretkey: server_secret,
71
+ )
72
+ pull.set_curve_auth([client_public])
73
+
74
+ push = OMQ.rs(
75
+ :push,
76
+ curve_serverkey: server_public,
77
+ curve_publickey: client_public,
78
+ curve_secretkey: client_secret,
79
+ )
80
+ ```
81
+
82
+ `#set_curve_auth` accepts an Array of allowed public keys, a callable receiving
83
+ an `OMQ::Rust::MechanismPeerInfo`, or `nil` to accept every valid CURVE client.
84
+ Configure it before the first bind, connect, send, receive, or monitor call.
85
+ `OMQ::Rust.curve_public(secret_key)` derives a public key.
86
+
87
+ PLAIN uses `plain_server: true` on the server and `plain_username` plus
88
+ `plain_password` on clients. PLAIN authenticates without encryption; use it
89
+ only on trusted transports.
90
+
91
+ ## Compression
92
+
93
+ Use `lz4+tcp://` or `zstd+tcp://` endpoints on both peers. zstd senders accept
94
+ `compression_level`, `compression_dict`, and `compression_auto_train` socket
95
+ options.
96
+
97
+ ## Monitoring
98
+
99
+ `socket.monitor` returns an Enumerable monitor. `#recv(timeout:)` blocks for
100
+ the next event; `#recv_nowait` returns an event Hash or `nil`. Event hashes
101
+ contain `:event` and event-specific fields such as `:endpoint`,
102
+ `:connection_id`, and `:peer_identity`.
103
+
104
+ ## Fiber Schedulers
105
+
106
+ There is no separate async API and omq-rs does not install a scheduler.
107
+ `#send`, `#recv`, and connection waits use Ruby IO readiness. When the caller
108
+ installs a `Fiber.scheduler`, such as Async, waits suspend only the current
109
+ fiber.
110
+
111
+ TruffleRuby currently has no `Fiber.scheduler` API. Use threads when concurrent
112
+ blocking waits are needed there.
113
+
114
+ ```ruby
115
+ require "async"
116
+ require "omq/rs"
117
+
118
+ Async do
119
+ OMQ.rs(:pull) do |pull|
120
+ pull.bind("tcp://127.0.0.1:5555")
121
+ p pull.recv
122
+ end
123
+ end
124
+ ```
125
+
126
+ ## Ractors
127
+
128
+ Ruby 4 Ractors can create and use omq-rs sockets. Each socket must remain owned
129
+ by the Ractor that created it. Ractors can communicate through inproc, IPC, or
130
+ TCP endpoints; no preparation call is required.
131
+
132
+ ## Development
133
+
134
+ ```sh
135
+ bundle install
136
+ bundle exec rake
137
+ ```
138
+
139
+ ## Performance
140
+
141
+ ![Ruby binding benchmark](doc/charts/bindings.svg)
142
+
143
+ The binding benchmark uses separate Ruby processes over TCP and compares
144
+ `omq-rs` with [cztop](https://github.com/paddor/cztop), which calls CZMQ and
145
+ libzmq through FFI, and [ffi-rzmq](https://github.com/chuckremes/ffi-rzmq),
146
+ which calls libzmq directly through FFI. Install CZMQ and libzmq to include
147
+ both baselines.
148
+
149
+ ```sh
150
+ ruby -Ilib scripts/update_perf.rb
151
+ ruby -Ilib scripts/update_perf.rb --quick
152
+ ruby -Ilib scripts/update_perf.rb --chart-only
153
+ ```
154
+
155
+ Rows append to `~/.cache/omq-rs/bindings.jsonl`. The generated chart is
156
+ `doc/charts/bindings.svg`. `--quick` only runs a smoke benchmark and does not
157
+ write results or a chart.
158
+
159
+ ## License
160
+
161
+ [ISC](LICENSE)
@@ -0,0 +1,50 @@
1
+ [package]
2
+ name = "omq_rs_native"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ rust-version = "1.93"
6
+ license = "ISC"
7
+ authors = ["Patrik Wenger <paddor@gmail.com>"]
8
+ publish = false
9
+
10
+ [lib]
11
+ name = "omq_rs_native"
12
+ crate-type = ["cdylib"]
13
+
14
+ [features]
15
+ default = ["plain", "curve", "lz4", "zstd", "ws"]
16
+ plain = ["omq-tokio/plain"]
17
+ curve = ["omq-tokio/curve"]
18
+ lz4 = ["omq-tokio/lz4"]
19
+ zstd = ["omq-tokio/zstd"]
20
+ ws = ["omq-tokio/ws"]
21
+
22
+ [dependencies]
23
+ omq-proto = { version = ">=0.26.0, <0.28.0", default-features = false }
24
+ omq-tokio = { version = ">=0.21.3, <0.23.0", default-features = false }
25
+ yring = { version = "=0.3.14", features = ["async"] }
26
+
27
+ bytes = "1.12.0"
28
+ flume = { version = "0.12", default-features = false, features = ["async"] }
29
+ futures = { version = "0.3", default-features = false, features = ["std", "async-await"] }
30
+ libc = "0.2"
31
+ rb-sys = "0.9"
32
+ tokio = { version = "1.52.0", features = ["rt", "rt-multi-thread", "time", "sync", "io-util", "net"] }
33
+
34
+ [build-dependencies]
35
+ rb-sys = "0.9"
36
+
37
+ [lints.rust]
38
+ missing_debug_implementations = "deny"
39
+ unsafe_op_in_unsafe_fn = "deny"
40
+ rust_2018_idioms = { level = "warn", priority = -1 }
41
+
42
+ [lints.clippy]
43
+ all = { level = "warn", priority = -1 }
44
+ pedantic = { level = "warn", priority = -1 }
45
+ module_name_repetitions = "allow"
46
+ missing_errors_doc = "allow"
47
+ missing_panics_doc = "allow"
48
+ must_use_candidate = "allow"
49
+ cast_possible_truncation = "allow"
50
+ cast_sign_loss = "allow"
@@ -0,0 +1,24 @@
1
+ use std::process::Command;
2
+
3
+ fn main() {
4
+ println!("cargo:rerun-if-env-changed=RUBY");
5
+ println!("cargo:rustc-check-cfg=cfg(ruby_engine, values(\"mri\", \"truffleruby\"))");
6
+
7
+ let ruby = std::env::var("RUBY").unwrap_or_else(|_| "ruby".to_string());
8
+ let output = Command::new(&ruby)
9
+ .arg("-rrbconfig")
10
+ .arg("-e")
11
+ .arg("print RbConfig::CONFIG.fetch('ruby_install_name')")
12
+ .output()
13
+ .unwrap_or_else(|err| panic!("failed to run {ruby}: {err}"));
14
+
15
+ assert!(
16
+ output.status.success(),
17
+ "failed to query Ruby engine with {ruby}"
18
+ );
19
+
20
+ match String::from_utf8_lossy(&output.stdout).trim() {
21
+ "truffleruby" => println!("cargo:rustc-cfg=ruby_engine=\"truffleruby\""),
22
+ _ => println!("cargo:rustc-cfg=ruby_engine=\"mri\""),
23
+ }
24
+ }
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("omq/rs/omq_rs_native") do |r|
7
+ r.profile = ENV.fetch("RB_SYS_CARGO_PROFILE", :release).to_sym
8
+ end
@@ -0,0 +1,182 @@
1
+ use std::ffi::c_void;
2
+ use std::panic::{AssertUnwindSafe, catch_unwind};
3
+ use std::sync::Arc;
4
+
5
+ use bytes::Bytes;
6
+ use rb_sys::VALUE;
7
+
8
+ use crate::notify::PipeNotify;
9
+ use crate::options::parse_curve_public_key;
10
+ use crate::rb::{self, RbResult};
11
+
12
+ enum AuthRequest {
13
+ Check {
14
+ public_key: [u8; 32],
15
+ identity: Option<Bytes>,
16
+ reply: flume::Sender<bool>,
17
+ },
18
+ Stop,
19
+ }
20
+
21
+ pub struct AuthWorker {
22
+ sender: flume::Sender<AuthRequest>,
23
+ notify: Arc<PipeNotify>,
24
+ callback: VALUE,
25
+ thread: VALUE,
26
+ }
27
+
28
+ impl AuthWorker {
29
+ pub fn stop(self) {
30
+ let _ = self.sender.send(AuthRequest::Stop);
31
+ self.notify.notify();
32
+ let _ = rb::call_method_0(self.thread, c"join");
33
+ }
34
+
35
+ pub fn request_stop(self) {
36
+ let _ = self.sender.send(AuthRequest::Stop);
37
+ self.notify.notify();
38
+ }
39
+
40
+ pub fn callback(&self) -> VALUE {
41
+ self.callback
42
+ }
43
+
44
+ pub fn thread(&self) -> VALUE {
45
+ self.thread
46
+ }
47
+ }
48
+
49
+ struct WorkerData {
50
+ callback: VALUE,
51
+ receiver: flume::Receiver<AuthRequest>,
52
+ notify: Arc<PipeNotify>,
53
+ }
54
+
55
+ fn wait_for_request(data: &WorkerData) -> Option<AuthRequest> {
56
+ loop {
57
+ match data.receiver.try_recv() {
58
+ Ok(request) => return Some(request),
59
+ Err(flume::TryRecvError::Disconnected) => return None,
60
+ Err(flume::TryRecvError::Empty) => {}
61
+ }
62
+
63
+ data.notify.park_begin();
64
+ match data.receiver.try_recv() {
65
+ Ok(request) => {
66
+ data.notify.cancel_park();
67
+ return Some(request);
68
+ }
69
+ Err(flume::TryRecvError::Disconnected) => {
70
+ data.notify.cancel_park();
71
+ return None;
72
+ }
73
+ Err(flume::TryRecvError::Empty) => unsafe {
74
+ rb_sys::rb_thread_wait_fd(data.notify.read_fd());
75
+ },
76
+ }
77
+ data.notify.clear();
78
+ }
79
+ }
80
+
81
+ unsafe extern "C" fn auth_worker_main(data: *mut c_void) -> VALUE {
82
+ let data = unsafe { Box::from_raw(data.cast::<WorkerData>()) };
83
+ let _ = catch_unwind(AssertUnwindSafe(|| {
84
+ while let Some(AuthRequest::Check {
85
+ public_key,
86
+ identity,
87
+ reply,
88
+ }) = wait_for_request(&data)
89
+ {
90
+ let accepted = invoke_callback(data.callback, public_key, identity.as_ref());
91
+ let _ = reply.send(accepted);
92
+ }
93
+ }));
94
+ rb::qnil()
95
+ }
96
+
97
+ fn invoke_callback(callback: VALUE, public_key: [u8; 32], identity: Option<&Bytes>) -> bool {
98
+ let result = (|| -> RbResult<VALUE> {
99
+ let peer = rb::hash_new()?;
100
+ let key = omq_proto::CurvePublicKey::from_bytes(public_key)
101
+ .to_z85()
102
+ .into_bytes();
103
+ rb::hash_aset(
104
+ peer,
105
+ rb::symbol("public_key")?,
106
+ rb::new_binary_string(&key)?,
107
+ )?;
108
+ let identity = match identity {
109
+ Some(value) => rb::new_binary_string(value)?,
110
+ None => rb::qnil(),
111
+ };
112
+ rb::hash_aset(peer, rb::symbol("identity")?, identity)?;
113
+ rb::call_method_1(callback, c"call", peer)
114
+ })();
115
+
116
+ if let Ok(value) = result {
117
+ value != rb::qfalse() && value != rb::qnil()
118
+ } else {
119
+ unsafe { rb_sys::rb_set_errinfo(rb::qnil()) };
120
+ false
121
+ }
122
+ }
123
+
124
+ pub fn allowed_keys(value: VALUE) -> RbResult<omq_proto::Authenticator> {
125
+ let count = rb::array_len(value)?;
126
+ let mut keys = std::collections::HashSet::with_capacity(count);
127
+ for index in 0..count {
128
+ let value = rb::array_entry(value, index)?;
129
+ let bytes = rb::value_to_bytes(value)?;
130
+ let key = parse_curve_public_key(&bytes, "CURVE allowlist key")?;
131
+ keys.insert(*key.as_bytes());
132
+ }
133
+ Ok(omq_proto::Authenticator::new(move |peer| {
134
+ keys.contains(&peer.public_key)
135
+ }))
136
+ }
137
+
138
+ pub fn callback(callback: VALUE) -> RbResult<(omq_proto::Authenticator, AuthWorker)> {
139
+ let (sender, receiver) = flume::unbounded();
140
+ let notify = Arc::new(PipeNotify::new());
141
+ let data = Box::new(WorkerData {
142
+ callback,
143
+ receiver,
144
+ notify: Arc::clone(&notify),
145
+ });
146
+ let raw = Box::into_raw(data);
147
+ let thread = rb::protect_value(|| unsafe {
148
+ rb_sys::rb_thread_create(Some(auth_worker_main), raw.cast::<c_void>())
149
+ });
150
+ let thread = match thread {
151
+ Ok(thread) => thread,
152
+ Err(error) => {
153
+ unsafe { drop(Box::from_raw(raw)) };
154
+ return Err(error);
155
+ }
156
+ };
157
+
158
+ let auth_sender = sender.clone();
159
+ let auth_notify = Arc::clone(&notify);
160
+ let authenticator = omq_proto::Authenticator::new(move |peer| {
161
+ let (reply, result) = flume::bounded(1);
162
+ let request = AuthRequest::Check {
163
+ public_key: peer.public_key,
164
+ identity: peer.identity.clone(),
165
+ reply,
166
+ };
167
+ if auth_sender.send(request).is_err() {
168
+ return false;
169
+ }
170
+ auth_notify.notify();
171
+ result.recv().unwrap_or(false)
172
+ });
173
+ Ok((
174
+ authenticator,
175
+ AuthWorker {
176
+ sender,
177
+ notify,
178
+ callback,
179
+ thread,
180
+ },
181
+ ))
182
+ }
@@ -0,0 +1,15 @@
1
+ use omq_proto::error::Error as OmqError;
2
+
3
+ use crate::rb::RubyErr;
4
+
5
+ pub fn map_err(e: OmqError) -> RubyErr {
6
+ match e {
7
+ OmqError::Closed => RubyErr::io("socket closed"),
8
+ OmqError::Timeout => RubyErr::runtime("operation timed out"),
9
+ OmqError::Unroutable => RubyErr::runtime("no route to peer"),
10
+ OmqError::InvalidEndpoint(msg) => RubyErr::arg(msg),
11
+ OmqError::Protocol(msg) | OmqError::HandshakeFailed(msg) => RubyErr::runtime(msg),
12
+ OmqError::Io(e) => RubyErr::runtime(e.to_string()),
13
+ _ => RubyErr::runtime(format!("{e}")),
14
+ }
15
+ }
@@ -0,0 +1,126 @@
1
+ #[cfg(feature = "curve")]
2
+ mod auth;
3
+ mod error;
4
+ mod notify;
5
+ mod options;
6
+ mod rb;
7
+ mod runtime;
8
+ mod socket;
9
+
10
+ use rb_sys::VALUE;
11
+
12
+ use crate::rb::{RbResult, RubyErr};
13
+
14
+ fn has_impl(name: VALUE) -> RbResult<VALUE> {
15
+ let name = rb::value_to_string(name)?;
16
+ let available = match name.as_str() {
17
+ "ipc" | "inproc" => true,
18
+ #[cfg(feature = "curve")]
19
+ "curve" => true,
20
+ #[cfg(feature = "plain")]
21
+ "plain" => true,
22
+ #[cfg(feature = "lz4")]
23
+ "lz4" => true,
24
+ #[cfg(feature = "zstd")]
25
+ "zstd" => true,
26
+ #[cfg(feature = "ws")]
27
+ "ws" => true,
28
+ _ => false,
29
+ };
30
+ Ok(rb::bool_value(available))
31
+ }
32
+
33
+ unsafe extern "C" fn has(_module: VALUE, name: VALUE) -> VALUE {
34
+ rb::wrap(|| has_impl(name))
35
+ }
36
+
37
+ #[cfg(feature = "curve")]
38
+ fn curve_keypair_impl() -> RbResult<VALUE> {
39
+ let keypair = omq_proto::CurveKeypair::generate();
40
+ let pair = rb::array_new_capa(2)?;
41
+ rb::array_push(
42
+ pair,
43
+ rb::new_binary_string(keypair.public.to_z85().as_bytes())?,
44
+ )?;
45
+ rb::array_push(
46
+ pair,
47
+ rb::new_binary_string(keypair.secret.to_z85().as_bytes())?,
48
+ )?;
49
+ Ok(pair)
50
+ }
51
+
52
+ #[cfg(feature = "curve")]
53
+ unsafe extern "C" fn curve_keypair(_module: VALUE) -> VALUE {
54
+ rb::wrap(curve_keypair_impl)
55
+ }
56
+
57
+ #[cfg(feature = "curve")]
58
+ fn curve_public_impl(secret: VALUE) -> RbResult<VALUE> {
59
+ let secret = rb::value_to_string(secret)?;
60
+ let secret = omq_proto::CurveSecretKey::from_z85(&secret)
61
+ .map_err(|error| RubyErr::arg(error.to_string()))?;
62
+ rb::new_binary_string(secret.derive_public().to_z85().as_bytes())
63
+ }
64
+
65
+ #[cfg(feature = "curve")]
66
+ unsafe extern "C" fn curve_public(_module: VALUE, secret: VALUE) -> VALUE {
67
+ rb::wrap(|| curve_public_impl(secret))
68
+ }
69
+
70
+ fn set_io_threads_impl(n: VALUE) -> RbResult<VALUE> {
71
+ let n = rb::value_to_i64(n)?;
72
+ if n < 0 {
73
+ return Err(RubyErr::arg("io_threads must be non-negative"));
74
+ }
75
+ let n = usize::try_from(n).map_err(|_| RubyErr::arg("io_threads too large"))?;
76
+ socket::set_io_threads(n);
77
+ Ok(rb::qnil())
78
+ }
79
+
80
+ fn io_threads_impl() -> RbResult<VALUE> {
81
+ let n = u64::try_from(socket::io_threads())
82
+ .map_err(|_| RubyErr::runtime("io_threads too large"))?;
83
+ Ok(rb::u64_value(n))
84
+ }
85
+
86
+ unsafe extern "C" fn io_threads(_module: VALUE) -> VALUE {
87
+ rb::wrap(io_threads_impl)
88
+ }
89
+
90
+ unsafe extern "C" fn set_io_threads(_module: VALUE, n: VALUE) -> VALUE {
91
+ rb::wrap(|| set_io_threads_impl(n))
92
+ }
93
+
94
+ #[unsafe(no_mangle)]
95
+ /// # Safety
96
+ ///
97
+ /// Ruby calls this once while loading the native extension.
98
+ pub unsafe extern "C" fn Init_omq_rs_native() {
99
+ rb::wrap_init(init);
100
+ }
101
+
102
+ fn init() -> RbResult<()> {
103
+ #[cfg(ruby_engine = "mri")]
104
+ unsafe {
105
+ rb_sys::rb_ext_ractor_safe(true);
106
+ }
107
+
108
+ let omq = unsafe { rb::define_module(c"OMQ")? };
109
+ let rust = unsafe { rb::define_module_under(omq, c"Rust")? };
110
+ let native = unsafe { rb::define_module_under(rust, c"Native")? };
111
+
112
+ unsafe {
113
+ rb::define_module_function_0(native, c"io_threads", io_threads)?;
114
+ rb::define_module_function_1(native, c"io_threads=", set_io_threads)?;
115
+ rb::define_module_function_1(native, c"has", has)?;
116
+ #[cfg(feature = "curve")]
117
+ {
118
+ rb::define_module_function_0(native, c"curve_keypair", curve_keypair)?;
119
+ rb::define_module_function_1(native, c"curve_public", curve_public)?;
120
+ }
121
+ }
122
+
123
+ socket::register(native)?;
124
+
125
+ Ok(())
126
+ }