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 +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE +15 -0
- data/README.md +161 -0
- data/ext/omq_rs_native/Cargo.toml +50 -0
- data/ext/omq_rs_native/build.rs +24 -0
- data/ext/omq_rs_native/extconf.rb +8 -0
- data/ext/omq_rs_native/src/auth.rs +182 -0
- data/ext/omq_rs_native/src/error.rs +15 -0
- data/ext/omq_rs_native/src/lib.rs +126 -0
- data/ext/omq_rs_native/src/notify.rs +114 -0
- data/ext/omq_rs_native/src/options.rs +434 -0
- data/ext/omq_rs_native/src/rb.rs +494 -0
- data/ext/omq_rs_native/src/runtime.rs +459 -0
- data/ext/omq_rs_native/src/socket.rs +972 -0
- data/lib/omq/rs/socket.rb +662 -0
- data/lib/omq/rs/version.rb +10 -0
- data/lib/omq/rs.rb +30 -0
- data/lib/omq-rs.rb +3 -0
- metadata +77 -0
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
|
+

|
|
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,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(¬ify),
|
|
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(¬ify);
|
|
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
|
+
}
|