omq-backend-rust 0.1.1

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: d3204e074915dc804b56b3b5b713cbd08c11b84dec67732f3f0f7d12e02b5b87
4
+ data.tar.gz: c46c4dbebce74f3b164c3d8d4c5eb10a92ab4cc730c25198ef66edee7e50330b
5
+ SHA512:
6
+ metadata.gz: 47c78d4a47f5f3494a90325065ac4e7adf8361d30b37ea0d8ff46cb7b96d47709d8021e5448fd3fef8fa85c0e4f1516e8ce6c7fa5465d435ae5c669499c0427e
7
+ data.tar.gz: 66a1f72298ea9e30cec1c729a76f77cd6551abdff58edfbd31cdc826d54bf66764dd01889d59da9cd098b1cef01367d50c6eac94aca0c39559689ff0621c7b6b
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## 0.1.1 - 2026-07-23
6
+
7
+ ### Changed
8
+
9
+ - Updated to `omq-tokio` 0.19.3 and `omq-proto` 0.23.2.
10
+ - Removed obsolete Blake3ZMQ support.
11
+ - Moved release source to the `zeromq/omq.rb` monorepo.
12
+
13
+ ## v0.1.0 — 2026-06-24
14
+
15
+ Initial release.
16
+
17
+ ### Added
18
+
19
+ - **`OMQ::Rust::Engine`** — drop-in OMQ engine backed by omq-tokio. Pass
20
+ `backend: :rust` to any OMQ socket constructor.
21
+ - All standard socket types: REQ/REP, PUB/SUB, PUSH/PULL, DEALER/ROUTER,
22
+ XPUB/XSUB, PAIR.
23
+ - All draft socket types: CLIENT/SERVER, RADIO/DISH, SCATTER/GATHER, CHANNEL.
24
+ - TCP and IPC transports.
25
+ - CURVE (CurveZMQ) and BLAKE3ZMQ security mechanisms.
26
+ - Full cross-backend interop with the default Ruby engine.
27
+ - Lifecycle promises: `peer_connected`, `all_peers_gone`, `subscriber_joined`.
28
+ - Monitor event forwarding from the Tokio runtime.
29
+ - Configurable IO thread count via `OMQ::Rust.io_threads`.
data/Cargo.toml ADDED
@@ -0,0 +1,3 @@
1
+ [workspace]
2
+ members = ["ext/omq_backend_rust"]
3
+ resolver = "3"
data/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025-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
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # omq-backend-rust
2
+
3
+ [![CI](https://github.com/zeromq/omq.rb/actions/workflows/ci.yml/badge.svg)](https://github.com/zeromq/omq.rb/actions/workflows/ci.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/omq-backend-rust?color=e9573f)](https://rubygems.org/gems/omq-backend-rust)
5
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
6
+ [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.3-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org)
7
+
8
+ Rust-backed engine for [OMQ](https://github.com/zeromq/omq.rb). Same socket API,
9
+ but networking runs on a [Tokio](https://tokio.rs/) runtime inside a native
10
+ extension compiled via [rb_sys](https://github.com/oxidize-rb/rb-sys).
11
+
12
+ ## Install
13
+
14
+ Requires a Rust toolchain (stable) at gem install time.
15
+
16
+ ```ruby
17
+ # Gemfile
18
+ gem "omq-backend-rust"
19
+ ```
20
+
21
+ ```sh
22
+ gem install omq-backend-rust
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ require "omq"
29
+ require "omq/rust"
30
+
31
+ Async do
32
+ push = OMQ::PUSH.new(backend: :rust)
33
+ pull = OMQ::PULL.new(backend: :rust)
34
+
35
+ port = pull.bind("tcp://127.0.0.1:0").port
36
+ push.connect("tcp://127.0.0.1:#{port}")
37
+ push.peer_connected.wait
38
+
39
+ push << "hello from Rust"
40
+ p pull.receive # => ["hello from Rust"]
41
+ ensure
42
+ push&.close
43
+ pull&.close
44
+ end
45
+ ```
46
+
47
+ The `:rust` backend is fully interoperable with the default `:ruby` backend.
48
+ Mix backends freely within the same process.
49
+
50
+ ## Supported socket types
51
+
52
+ All standard and draft ZMTP socket types: REQ/REP, PUB/SUB, PUSH/PULL,
53
+ DEALER/ROUTER, XPUB/XSUB, PAIR, CLIENT/SERVER, RADIO/DISH,
54
+ SCATTER/GATHER, CHANNEL.
55
+
56
+ ## Security mechanisms
57
+
58
+ - **NULL** (default)
59
+ - **CURVE** (CurveZMQ, via [Nuckle](https://github.com/paddor/nuckle))
60
+
61
+ ## Development
62
+
63
+ ```sh
64
+ OMQ_DEV=1 bundle install
65
+ OMQ_DEV=1 bundle exec rake
66
+ ```
67
+
68
+ ## License
69
+
70
+ [ISC](LICENSE)
@@ -0,0 +1,33 @@
1
+ [package]
2
+ name = "omq_backend_rust"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ license = "ISC"
6
+ authors = ["Patrik Wenger <paddor@gmail.com>"]
7
+ publish = false
8
+
9
+ [lib]
10
+ name = "omq_backend_rust"
11
+ crate-type = ["cdylib"]
12
+
13
+ [features]
14
+ default = ["plain", "curve", "lz4"]
15
+ plain = ["omq-tokio/plain"]
16
+ curve = ["omq-tokio/curve"]
17
+ lz4 = ["omq-tokio/lz4"]
18
+
19
+ [dependencies]
20
+ omq-proto = { version = "=0.23.2", default-features = false }
21
+ omq-tokio = { version = "=0.19.3", default-features = false }
22
+ tokio = { version = "1.52.0", features = ["rt", "rt-multi-thread", "time", "sync", "io-util", "net"] }
23
+ yring = { version = "0.3.8", features = ["async"] }
24
+
25
+ bytes = "1.12.0"
26
+ flume = { version = "0.12", default-features = false, features = ["async"] }
27
+ futures = { version = "0.3", default-features = false, features = ["std", "async-await"] }
28
+ magnus = "0.8"
29
+ rb-sys = "0.9"
30
+ libc = "0.2"
31
+
32
+ [build-dependencies]
33
+ rb-sys = "0.9"
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("omq/rust/omq_backend_rust") do |r|
7
+ r.profile = ENV.fetch("RB_SYS_CARGO_PROFILE", :release).to_sym
8
+ end
@@ -0,0 +1,15 @@
1
+ use magnus::{Error, Ruby};
2
+ use omq_proto::error::Error as OmqError;
3
+
4
+ pub fn map_err(ruby: &Ruby, e: OmqError) -> Error {
5
+ match e {
6
+ OmqError::Closed => Error::new(ruby.exception_io_error(), "socket closed"),
7
+ OmqError::Timeout => Error::new(ruby.exception_runtime_error(), "operation timed out"),
8
+ OmqError::Unroutable => Error::new(ruby.exception_runtime_error(), "no route to peer"),
9
+ OmqError::InvalidEndpoint(msg) => Error::new(ruby.exception_arg_error(), msg),
10
+ OmqError::Protocol(msg) => Error::new(ruby.exception_runtime_error(), msg),
11
+ OmqError::Io(e) => Error::new(ruby.exception_runtime_error(), e.to_string()),
12
+ OmqError::HandshakeFailed(msg) => Error::new(ruby.exception_runtime_error(), msg),
13
+ _ => Error::new(ruby.exception_runtime_error(), format!("{e}")),
14
+ }
15
+ }
@@ -0,0 +1,24 @@
1
+ mod error;
2
+ mod notify;
3
+ mod options;
4
+ mod runtime;
5
+ mod socket;
6
+
7
+ use magnus::{Error, Ruby, function, prelude::*};
8
+
9
+ fn set_io_threads(n: usize) {
10
+ socket::set_io_threads(n);
11
+ }
12
+
13
+ #[magnus::init]
14
+ fn init(ruby: &Ruby) -> Result<(), Error> {
15
+ let omq = ruby.define_module("OMQ")?;
16
+ let rust = omq.define_module("Rust")?;
17
+ let native = rust.define_module("Native")?;
18
+
19
+ native.define_module_function("io_threads=", function!(set_io_threads, 1))?;
20
+
21
+ socket::register(ruby)?;
22
+
23
+ Ok(())
24
+ }
@@ -0,0 +1,69 @@
1
+ use std::os::fd::RawFd;
2
+ use std::sync::atomic::{AtomicBool, Ordering};
3
+
4
+ pub struct PipeNotify {
5
+ read_fd: RawFd,
6
+ write_fd: RawFd,
7
+ parking: AtomicBool,
8
+ }
9
+
10
+ unsafe impl Send for PipeNotify {}
11
+ unsafe impl Sync for PipeNotify {}
12
+
13
+ impl PipeNotify {
14
+ pub fn new() -> Self {
15
+ let mut fds = [0i32; 2];
16
+ let ret = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) };
17
+ assert!(
18
+ ret == 0,
19
+ "pipe2 failed: {}",
20
+ std::io::Error::last_os_error()
21
+ );
22
+ Self {
23
+ read_fd: fds[0],
24
+ write_fd: fds[1],
25
+ parking: AtomicBool::new(false),
26
+ }
27
+ }
28
+
29
+ pub fn notify(&self) {
30
+ if self.parking.load(Ordering::Acquire) {
31
+ self.write_byte();
32
+ }
33
+ }
34
+
35
+ pub fn force_wake(&self) {
36
+ self.write_byte();
37
+ }
38
+
39
+ pub fn read_fd(&self) -> RawFd {
40
+ self.read_fd
41
+ }
42
+
43
+ pub fn park_begin(&self) {
44
+ self.parking.store(true, Ordering::Release);
45
+ }
46
+
47
+ fn write_byte(&self) {
48
+ let val: u8 = 1;
49
+ loop {
50
+ let ret =
51
+ unsafe { libc::write(self.write_fd, &val as *const u8 as *const libc::c_void, 1) };
52
+ if ret >= 0 {
53
+ break;
54
+ }
55
+ if std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) {
56
+ break;
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ impl Drop for PipeNotify {
63
+ fn drop(&mut self) {
64
+ unsafe {
65
+ libc::close(self.read_fd);
66
+ libc::close(self.write_fd);
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,185 @@
1
+ use std::time::Duration;
2
+
3
+ use bytes::Bytes;
4
+ use magnus::{Error, Ruby, TryConvert, r_hash::RHash, value::ReprValue};
5
+
6
+ pub fn build_options(ruby: &Ruby, hash: RHash) -> Result<omq_tokio::Options, Error> {
7
+ let mut opts = omq_tokio::Options::default();
8
+
9
+ if let Some(v) = get_opt::<i64>(ruby, hash, "send_hwm")? {
10
+ opts.send_hwm = v.max(0) as u32;
11
+ }
12
+ if let Some(v) = get_opt::<i64>(ruby, hash, "recv_hwm")? {
13
+ opts.recv_hwm = v.max(0) as u32;
14
+ }
15
+ if let Some(v) = get_opt::<f64>(ruby, hash, "linger")? {
16
+ opts.linger = if v.is_infinite() {
17
+ None
18
+ } else {
19
+ Some(Duration::from_secs_f64(v))
20
+ };
21
+ }
22
+ if let Some(v) = get_opt::<Vec<u8>>(ruby, hash, "identity")? {
23
+ if !v.is_empty() {
24
+ opts.identity = Bytes::from(v);
25
+ }
26
+ }
27
+ if let Some(v) = get_opt::<bool>(ruby, hash, "router_mandatory")? {
28
+ opts.router_mandatory = v;
29
+ }
30
+ if let Some(v) = get_opt::<bool>(ruby, hash, "conflate")? {
31
+ opts.conflate = v;
32
+ }
33
+ if let Some(v) = get_opt_duration(ruby, hash, "heartbeat_interval")? {
34
+ opts.heartbeat_interval = Some(v);
35
+ }
36
+ if let Some(v) = get_opt_duration(ruby, hash, "heartbeat_ttl")? {
37
+ opts.heartbeat_ttl = Some(v);
38
+ }
39
+ if let Some(v) = get_opt_duration(ruby, hash, "heartbeat_timeout")? {
40
+ opts.heartbeat_timeout = Some(v);
41
+ }
42
+ if let Some(v) = get_opt::<i64>(ruby, hash, "max_message_size")? {
43
+ opts.max_message_size = Some(v as usize);
44
+ }
45
+ if let Some(v) = get_opt::<i64>(ruby, hash, "sndbuf")? {
46
+ opts.send_buffer_size = Some(v as usize);
47
+ }
48
+ if let Some(v) = get_opt::<i64>(ruby, hash, "rcvbuf")? {
49
+ opts.recv_buffer_size = Some(v as usize);
50
+ }
51
+ if let Some(v) = get_opt::<String>(ruby, hash, "on_mute")? {
52
+ opts.on_mute = match v.as_str() {
53
+ "drop_newest" | "drop" => omq_tokio::OnMute::DropNewest,
54
+ _ => omq_tokio::OnMute::Block,
55
+ };
56
+ }
57
+
58
+ if let Some(v) = get_opt::<f64>(ruby, hash, "reconnect_interval")? {
59
+ opts.reconnect = omq_proto::options::ReconnectPolicy::Fixed(Duration::from_secs_f64(v));
60
+ }
61
+ if let Some(min) = get_opt::<f64>(ruby, hash, "reconnect_interval_min")? {
62
+ let max = get_opt::<f64>(ruby, hash, "reconnect_interval_max")?.unwrap_or(min * 16.0);
63
+ opts.reconnect = omq_proto::options::ReconnectPolicy::Exponential {
64
+ min: Duration::from_secs_f64(min),
65
+ max: Duration::from_secs_f64(max),
66
+ };
67
+ }
68
+
69
+ if let Some(mech_type) = get_opt::<String>(ruby, hash, "mechanism_type")? {
70
+ apply_mechanism(ruby, hash, &mech_type, &mut opts)?;
71
+ }
72
+
73
+ Ok(opts)
74
+ }
75
+
76
+ fn apply_mechanism(
77
+ ruby: &Ruby,
78
+ hash: RHash,
79
+ mech_type: &str,
80
+ opts: &mut omq_tokio::Options,
81
+ ) -> Result<(), Error> {
82
+ match mech_type {
83
+ "null" => {}
84
+
85
+ #[cfg(feature = "curve")]
86
+ "curve" => {
87
+ let is_server = get_opt::<bool>(ruby, hash, "mechanism_server")?.unwrap_or(false);
88
+ let pub_key = get_opt_bytes(ruby, hash, "mechanism_public_key")?;
89
+ let sec_key = get_opt_bytes(ruby, hash, "mechanism_secret_key")?;
90
+
91
+ if is_server {
92
+ if let (Some(pk), Some(sk)) = (pub_key, sec_key) {
93
+ let keypair = omq_proto::CurveKeypair {
94
+ public: omq_proto::CurvePublicKey::from_bytes(to_32(
95
+ ruby,
96
+ &pk,
97
+ "public key",
98
+ )?),
99
+ secret: omq_proto::CurveSecretKey::from_bytes(to_32(
100
+ ruby,
101
+ &sk,
102
+ "secret key",
103
+ )?),
104
+ };
105
+ opts.mechanism = omq_proto::MechanismSetup::CurveServer {
106
+ our_keypair: keypair,
107
+ cookie_keyring: std::sync::Arc::new(omq_proto::CurveCookieKeyring::new()),
108
+ authenticator: None,
109
+ };
110
+ }
111
+ } else {
112
+ let srv_key = get_opt_bytes(ruby, hash, "mechanism_server_key")?;
113
+ if let (Some(pk), Some(sk), Some(svk)) = (pub_key, sec_key, srv_key) {
114
+ let keypair = omq_proto::CurveKeypair {
115
+ public: omq_proto::CurvePublicKey::from_bytes(to_32(
116
+ ruby,
117
+ &pk,
118
+ "public key",
119
+ )?),
120
+ secret: omq_proto::CurveSecretKey::from_bytes(to_32(
121
+ ruby,
122
+ &sk,
123
+ "secret key",
124
+ )?),
125
+ };
126
+ opts.mechanism = omq_proto::MechanismSetup::CurveClient {
127
+ our_keypair: keypair,
128
+ server_public: omq_proto::CurvePublicKey::from_bytes(to_32(
129
+ ruby,
130
+ &svk,
131
+ "server key",
132
+ )?),
133
+ };
134
+ }
135
+ }
136
+ }
137
+
138
+ _ => {}
139
+ }
140
+ Ok(())
141
+ }
142
+
143
+ fn to_32(ruby: &Ruby, bytes: &[u8], label: &str) -> Result<[u8; 32], Error> {
144
+ bytes.try_into().map_err(|_| {
145
+ Error::new(
146
+ ruby.exception_arg_error(),
147
+ format!("{label} must be exactly 32 bytes, got {}", bytes.len()),
148
+ )
149
+ })
150
+ }
151
+
152
+ fn get_opt_bytes(ruby: &Ruby, hash: RHash, key: &str) -> Result<Option<Vec<u8>>, Error> {
153
+ let k = ruby.str_new(key);
154
+ match hash.get(k) {
155
+ Some(v) => {
156
+ if v.is_nil() {
157
+ return Ok(None);
158
+ }
159
+ let s = magnus::r_string::RString::try_convert(v)?;
160
+ Ok(Some(unsafe { s.as_slice() }.to_vec()))
161
+ }
162
+ None => Ok(None),
163
+ }
164
+ }
165
+
166
+ fn get_opt<T: TryConvert>(ruby: &Ruby, hash: RHash, key: &str) -> Result<Option<T>, Error> {
167
+ let k = ruby.str_new(key);
168
+ match hash.get(k) {
169
+ Some(v) => {
170
+ if v.is_nil() {
171
+ Ok(None)
172
+ } else {
173
+ Ok(Some(T::try_convert(v)?))
174
+ }
175
+ }
176
+ None => Ok(None),
177
+ }
178
+ }
179
+
180
+ fn get_opt_duration(ruby: &Ruby, hash: RHash, key: &str) -> Result<Option<Duration>, Error> {
181
+ match get_opt::<f64>(ruby, hash, key)? {
182
+ Some(v) => Ok(Some(Duration::from_secs_f64(v))),
183
+ None => Ok(None),
184
+ }
185
+ }