zzq 0.1.1 → 0.2.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 +4 -4
- data/CHANGELOG.md +24 -0
- data/LICENSE-APACHE +202 -0
- data/LICENSE-ISC +15 -0
- data/README.md +67 -122
- data/ext/zzq_native/Cargo.toml +35 -0
- data/ext/zzq_native/extconf.rb +8 -0
- data/ext/zzq_native/src/broker.rs +163 -0
- data/ext/zzq_native/src/client.rs +294 -0
- data/ext/zzq_native/src/context.rs +51 -0
- data/ext/zzq_native/src/error.rs +117 -0
- data/ext/zzq_native/src/lib.rs +52 -0
- data/ext/zzq_native/src/op.rs +140 -0
- data/ext/zzq_native/src/options.rs +168 -0
- data/ext/zzq_native/src/rb.rs +424 -0
- data/ext/zzq_native/src/wake.rs +155 -0
- data/lib/zzq/broker.rb +90 -21
- data/lib/zzq/client.rb +202 -35
- data/lib/zzq/errors.rb +37 -0
- data/lib/zzq/message.rb +16 -107
- data/lib/zzq/options.rb +27 -86
- data/lib/zzq/version.rb +2 -1
- data/lib/zzq/waiting.rb +65 -0
- data/lib/zzq.rb +22 -19
- metadata +33 -77
- data/LICENSE +0 -13
- data/lib/zzq/engine/connection_lifecycle.rb +0 -139
- data/lib/zzq/engine/keepalive.rb +0 -75
- data/lib/zzq/engine/socket_lifecycle.rb +0 -79
- data/lib/zzq/engine.rb +0 -169
- data/lib/zzq/error.rb +0 -42
- data/lib/zzq/monitor_event.rb +0 -14
- data/lib/zzq/persistence/interface.rb +0 -43
- data/lib/zzq/persistence/memory.rb +0 -37
- data/lib/zzq/persistence/pstore.rb +0 -66
- data/lib/zzq/routing/broker.rb +0 -380
- data/lib/zzq/routing/client.rb +0 -327
- data/lib/zzq/routing/packet_id_allocator.rb +0 -54
- data/lib/zzq/routing/qos_tracker.rb +0 -108
- data/lib/zzq/routing/retained_store.rb +0 -67
- data/lib/zzq/routing/topic_match.rb +0 -49
- data/lib/zzq/routing/topic_trie.rb +0 -232
- data/lib/zzq/socket.rb +0 -146
- data/lib/zzq/subscription.rb +0 -70
- data/lib/zzq/transport/ipc.rb +0 -92
- data/lib/zzq/transport/mqtt.rb +0 -83
- data/lib/zzq/transport/mqtts.rb +0 -173
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
//! Calls into the Ruby C API. Every call that may raise runs under
|
|
2
|
+
//! `rb_protect`, so a Ruby exception never unwinds through Rust frames.
|
|
3
|
+
|
|
4
|
+
use std::ffi::{CStr, CString, c_char, c_long, c_void};
|
|
5
|
+
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
6
|
+
use std::ptr;
|
|
7
|
+
|
|
8
|
+
use rb_sys::{VALUE, rb_data_type_struct__bindgen_ty_1, rb_data_type_t, size_t};
|
|
9
|
+
|
|
10
|
+
pub type RbResult<T = VALUE> = Result<T, RubyErr>;
|
|
11
|
+
|
|
12
|
+
#[derive(Debug)]
|
|
13
|
+
pub enum RubyErr {
|
|
14
|
+
/// An exception object to raise.
|
|
15
|
+
Exception(VALUE),
|
|
16
|
+
/// An exception to build from a class and a message.
|
|
17
|
+
Error { class: VALUE, message: String },
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl RubyErr {
|
|
21
|
+
pub fn new(class: VALUE, message: impl Into<String>) -> Self {
|
|
22
|
+
Self::Error {
|
|
23
|
+
class,
|
|
24
|
+
message: message.into(),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub fn arg(message: impl Into<String>) -> Self {
|
|
29
|
+
Self::new(unsafe { rb_sys::rb_eArgError }, message)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub fn runtime(message: impl Into<String>) -> Self {
|
|
33
|
+
Self::new(unsafe { rb_sys::rb_eRuntimeError }, message)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub fn type_error(message: impl Into<String>) -> Self {
|
|
37
|
+
Self::new(unsafe { rb_sys::rb_eTypeError }, message)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn current_exception() -> Self {
|
|
41
|
+
let err = unsafe { rb_sys::rb_errinfo() };
|
|
42
|
+
unsafe { rb_sys::rb_set_errinfo(qnil()) };
|
|
43
|
+
if err == qnil() {
|
|
44
|
+
Self::runtime("Ruby exception")
|
|
45
|
+
} else {
|
|
46
|
+
Self::Exception(err)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Runs a method body: raises its error, and turns a panic into an error.
|
|
52
|
+
pub fn wrap<F>(f: F) -> VALUE
|
|
53
|
+
where
|
|
54
|
+
F: FnOnce() -> RbResult<VALUE>,
|
|
55
|
+
{
|
|
56
|
+
match catch_unwind(AssertUnwindSafe(f)) {
|
|
57
|
+
Ok(Ok(value)) => value,
|
|
58
|
+
Ok(Err(err)) => raise(err),
|
|
59
|
+
Err(_) => raise(RubyErr::runtime("native Rust panic")),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub fn raise(err: RubyErr) -> ! {
|
|
64
|
+
match err {
|
|
65
|
+
RubyErr::Exception(exc) => unsafe { rb_sys::rb_exc_raise(exc) },
|
|
66
|
+
RubyErr::Error { class, message } => {
|
|
67
|
+
let message = CString::new(message.replace('\0', "\\0")).unwrap_or_default();
|
|
68
|
+
let exc = unsafe { rb_sys::rb_exc_new_cstr(class, message.as_ptr()) };
|
|
69
|
+
unsafe { rb_sys::rb_exc_raise(exc) }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
struct ProtectData<F> {
|
|
75
|
+
func: Option<F>,
|
|
76
|
+
panicked: bool,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Runs `func`, which calls into Ruby, catching a Ruby exception.
|
|
80
|
+
pub fn protect<F>(func: F) -> RbResult<VALUE>
|
|
81
|
+
where
|
|
82
|
+
F: FnOnce() -> VALUE,
|
|
83
|
+
{
|
|
84
|
+
unsafe extern "C" fn call<F>(arg: VALUE) -> VALUE
|
|
85
|
+
where
|
|
86
|
+
F: FnOnce() -> VALUE,
|
|
87
|
+
{
|
|
88
|
+
let data = unsafe { &mut *(arg as *mut ProtectData<F>) };
|
|
89
|
+
let Some(func) = data.func.take() else {
|
|
90
|
+
data.panicked = true;
|
|
91
|
+
return qnil();
|
|
92
|
+
};
|
|
93
|
+
if let Ok(value) = catch_unwind(AssertUnwindSafe(func)) {
|
|
94
|
+
value
|
|
95
|
+
} else {
|
|
96
|
+
data.panicked = true;
|
|
97
|
+
qnil()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let mut data = ProtectData {
|
|
102
|
+
func: Some(func),
|
|
103
|
+
panicked: false,
|
|
104
|
+
};
|
|
105
|
+
let mut state = 0;
|
|
106
|
+
let value = unsafe {
|
|
107
|
+
rb_sys::rb_protect(
|
|
108
|
+
Some(call::<F>),
|
|
109
|
+
(&raw mut data).cast::<ProtectData<F>>() as VALUE,
|
|
110
|
+
&raw mut state,
|
|
111
|
+
)
|
|
112
|
+
};
|
|
113
|
+
if state != 0 {
|
|
114
|
+
Err(RubyErr::current_exception())
|
|
115
|
+
} else if data.panicked {
|
|
116
|
+
Err(RubyErr::runtime("native Rust panic"))
|
|
117
|
+
} else {
|
|
118
|
+
Ok(value)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pub const fn qnil() -> VALUE {
|
|
123
|
+
rb_sys::ruby_special_consts::RUBY_Qnil as VALUE
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
pub const fn qtrue() -> VALUE {
|
|
127
|
+
rb_sys::ruby_special_consts::RUBY_Qtrue as VALUE
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pub const fn qfalse() -> VALUE {
|
|
131
|
+
rb_sys::ruby_special_consts::RUBY_Qfalse as VALUE
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const fn qundef() -> VALUE {
|
|
135
|
+
rb_sys::ruby_special_consts::RUBY_Qundef as VALUE
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pub fn bool_value(value: bool) -> VALUE {
|
|
139
|
+
if value { qtrue() } else { qfalse() }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn is_kind_of(value: VALUE, class: VALUE) -> bool {
|
|
143
|
+
unsafe { rb_sys::rb_obj_is_kind_of(value, class) == qtrue() }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// The value under a String key, `None` if the key is missing.
|
|
147
|
+
pub fn hash_get(hash: VALUE, key: &str) -> RbResult<Option<VALUE>> {
|
|
148
|
+
if !is_kind_of(hash, unsafe { rb_sys::rb_cHash }) {
|
|
149
|
+
return Err(RubyErr::type_error("expected a Hash"));
|
|
150
|
+
}
|
|
151
|
+
let key = new_utf8_string(key)?;
|
|
152
|
+
let value = protect(|| unsafe { rb_sys::rb_hash_lookup2(hash, key, qundef()) })?;
|
|
153
|
+
Ok((value != qundef()).then_some(value))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
pub fn hash_new() -> RbResult<VALUE> {
|
|
157
|
+
protect(|| unsafe { rb_sys::rb_hash_new() })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
pub fn hash_aset(hash: VALUE, key: VALUE, value: VALUE) -> RbResult<()> {
|
|
161
|
+
protect(|| unsafe { rb_sys::rb_hash_aset(hash, key, value) })?;
|
|
162
|
+
Ok(())
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
pub fn array_new_capa(capacity: usize) -> RbResult<VALUE> {
|
|
166
|
+
let capacity = c_long_len(capacity)?;
|
|
167
|
+
protect(|| unsafe { rb_sys::rb_ary_new_capa(capacity) })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pub fn array_entries(array: VALUE) -> RbResult<Vec<VALUE>> {
|
|
171
|
+
if !is_kind_of(array, unsafe { rb_sys::rb_cArray }) {
|
|
172
|
+
return Err(RubyErr::type_error("expected an Array"));
|
|
173
|
+
}
|
|
174
|
+
let len = unsafe { rb_sys::RARRAY_LEN(array) };
|
|
175
|
+
(0..len)
|
|
176
|
+
.map(|i| protect(|| unsafe { rb_sys::rb_ary_entry(array, i) }))
|
|
177
|
+
.collect()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
pub fn array_push(array: VALUE, value: VALUE) -> RbResult<()> {
|
|
181
|
+
protect(|| unsafe { rb_sys::rb_ary_push(array, value) })?;
|
|
182
|
+
Ok(())
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
pub fn freeze(value: VALUE) -> VALUE {
|
|
186
|
+
unsafe { rb_sys::rb_obj_freeze(value) }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
pub fn symbol(name: &str) -> RbResult<VALUE> {
|
|
190
|
+
let len = c_long_len(name.len())?;
|
|
191
|
+
protect(|| unsafe { rb_sys::rb_id2sym(rb_sys::rb_intern2(name.as_ptr().cast(), len)) })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// A frozen binary String.
|
|
195
|
+
pub fn new_binary_string(bytes: &[u8]) -> RbResult<VALUE> {
|
|
196
|
+
let len = c_long_len(bytes.len())?;
|
|
197
|
+
let ptr = if bytes.is_empty() {
|
|
198
|
+
ptr::null()
|
|
199
|
+
} else {
|
|
200
|
+
bytes.as_ptr().cast::<c_char>()
|
|
201
|
+
};
|
|
202
|
+
let value = protect(|| unsafe { rb_sys::rb_str_new(ptr, len) })?;
|
|
203
|
+
Ok(freeze(value))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
pub fn new_utf8_string(text: &str) -> RbResult<VALUE> {
|
|
207
|
+
let len = c_long_len(text.len())?;
|
|
208
|
+
protect(|| unsafe { rb_sys::rb_utf8_str_new(text.as_ptr().cast::<c_char>(), len) })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// A frozen, deduplicated UTF-8 String, for values that repeat, such as
|
|
212
|
+
/// topics.
|
|
213
|
+
pub fn interned_utf8_string(text: &str) -> RbResult<VALUE> {
|
|
214
|
+
let len = c_long_len(text.len())?;
|
|
215
|
+
protect(|| unsafe {
|
|
216
|
+
rb_sys::rb_enc_interned_str(
|
|
217
|
+
text.as_ptr().cast::<c_char>(),
|
|
218
|
+
len,
|
|
219
|
+
rb_sys::rb_utf8_encoding(),
|
|
220
|
+
)
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// `value` as a String, calling `to_str` if needed.
|
|
225
|
+
pub fn string_value(value: VALUE) -> RbResult<VALUE> {
|
|
226
|
+
protect(|| unsafe { rb_sys::rb_str_to_str(value) })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// The bytes of a String from [`string_value`].
|
|
230
|
+
///
|
|
231
|
+
/// # Safety
|
|
232
|
+
///
|
|
233
|
+
/// The slice points into the Ruby string. Use it before the next call
|
|
234
|
+
/// into Ruby, which may run GC or change the string.
|
|
235
|
+
pub unsafe fn string_bytes<'a>(string: VALUE) -> &'a [u8] {
|
|
236
|
+
let len = unsafe { rb_sys::RSTRING_LEN(string) };
|
|
237
|
+
let ptr = unsafe { rb_sys::RSTRING_PTR(string) };
|
|
238
|
+
if len <= 0 || ptr.is_null() {
|
|
239
|
+
return &[];
|
|
240
|
+
}
|
|
241
|
+
unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len as usize) }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
pub fn value_to_bytes(value: VALUE) -> RbResult<Vec<u8>> {
|
|
245
|
+
let string = string_value(value)?;
|
|
246
|
+
Ok(unsafe { string_bytes(string) }.to_vec())
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
pub fn value_to_string(value: VALUE) -> RbResult<String> {
|
|
250
|
+
String::from_utf8(value_to_bytes(value)?).map_err(|_| RubyErr::arg("expected a UTF-8 String"))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
pub fn value_to_i64(value: VALUE) -> RbResult<i64> {
|
|
254
|
+
let mut out = 0i64;
|
|
255
|
+
protect(|| {
|
|
256
|
+
out = unsafe { rb_sys::rb_num2ll(value) };
|
|
257
|
+
qnil()
|
|
258
|
+
})?;
|
|
259
|
+
Ok(out)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
pub fn value_to_f64(value: VALUE) -> RbResult<f64> {
|
|
263
|
+
let mut out = 0.0f64;
|
|
264
|
+
protect(|| {
|
|
265
|
+
out = unsafe { rb_sys::rb_num2dbl(value) };
|
|
266
|
+
qnil()
|
|
267
|
+
})?;
|
|
268
|
+
Ok(out)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
pub fn value_to_bool(value: VALUE) -> RbResult<bool> {
|
|
272
|
+
if value == qtrue() {
|
|
273
|
+
Ok(true)
|
|
274
|
+
} else if value == qfalse() || value == qnil() {
|
|
275
|
+
Ok(false)
|
|
276
|
+
} else {
|
|
277
|
+
Err(RubyErr::type_error("expected true or false"))
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
pub fn u64_value(value: u64) -> VALUE {
|
|
282
|
+
unsafe { rb_sys::rb_ull2inum(value) }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// Calls `receiver.name(*args)`.
|
|
286
|
+
pub fn call(receiver: VALUE, name: &CStr, args: &[VALUE]) -> RbResult<VALUE> {
|
|
287
|
+
let count = std::os::raw::c_int::try_from(args.len())
|
|
288
|
+
.map_err(|_| RubyErr::arg("too many arguments"))?;
|
|
289
|
+
protect(|| unsafe {
|
|
290
|
+
rb_sys::rb_funcallv(
|
|
291
|
+
receiver,
|
|
292
|
+
rb_sys::rb_intern(name.as_ptr()),
|
|
293
|
+
count,
|
|
294
|
+
args.as_ptr(),
|
|
295
|
+
)
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
pub fn const_get(module: VALUE, name: &CStr) -> RbResult<VALUE> {
|
|
300
|
+
protect(|| unsafe { rb_sys::rb_const_get(module, rb_sys::rb_intern(name.as_ptr())) })
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// A Ruby type for a boxed `T`, which GC frees with the object.
|
|
304
|
+
pub struct DataType(rb_data_type_t);
|
|
305
|
+
|
|
306
|
+
unsafe impl Send for DataType {}
|
|
307
|
+
unsafe impl Sync for DataType {}
|
|
308
|
+
|
|
309
|
+
impl DataType {
|
|
310
|
+
pub const fn new<T>(name: &'static CStr) -> Self {
|
|
311
|
+
DataType(rb_data_type_t {
|
|
312
|
+
wrap_struct_name: name.as_ptr(),
|
|
313
|
+
function: rb_data_type_struct__bindgen_ty_1 {
|
|
314
|
+
dmark: None,
|
|
315
|
+
dfree: Some(free_box::<T>),
|
|
316
|
+
dsize: Some(size_box::<T>),
|
|
317
|
+
dcompact: None,
|
|
318
|
+
reserved: [ptr::null_mut(); 1],
|
|
319
|
+
},
|
|
320
|
+
parent: ptr::null(),
|
|
321
|
+
data: ptr::null_mut(),
|
|
322
|
+
// RUBY_TYPED_FREE_IMMEDIATELY: dropping never blocks or calls Ruby.
|
|
323
|
+
flags: 1,
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const fn ptr(&'static self) -> *const rb_data_type_t {
|
|
328
|
+
&raw const self.0
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/// Wraps `value` in a new object of `class`.
|
|
332
|
+
pub fn wrap<T>(&'static self, class: VALUE, value: T) -> RbResult<VALUE> {
|
|
333
|
+
let raw = Box::into_raw(Box::new(value));
|
|
334
|
+
protect(|| unsafe { rb_sys::rb_data_typed_object_wrap(class, raw.cast(), self.ptr()) })
|
|
335
|
+
.inspect_err(|_| drop(unsafe { Box::from_raw(raw) }))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// The `T` inside `value`.
|
|
339
|
+
///
|
|
340
|
+
/// # Safety
|
|
341
|
+
///
|
|
342
|
+
/// `T` must be the type this data type was made for.
|
|
343
|
+
pub unsafe fn get<T>(&'static self, value: VALUE) -> RbResult<&'static T> {
|
|
344
|
+
let mut ptr = ptr::null_mut();
|
|
345
|
+
protect(|| unsafe {
|
|
346
|
+
ptr = rb_sys::rb_check_typeddata(value, self.ptr());
|
|
347
|
+
qnil()
|
|
348
|
+
})?;
|
|
349
|
+
if ptr.is_null() {
|
|
350
|
+
return Err(RubyErr::runtime("native object is empty"));
|
|
351
|
+
}
|
|
352
|
+
Ok(unsafe { &*ptr.cast::<T>() })
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
unsafe extern "C" fn free_box<T>(ptr: *mut c_void) {
|
|
357
|
+
if !ptr.is_null() {
|
|
358
|
+
let _ = catch_unwind(AssertUnwindSafe(|| {
|
|
359
|
+
drop(unsafe { Box::from_raw(ptr.cast::<T>()) });
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
unsafe extern "C" fn size_box<T>(_ptr: *const c_void) -> size_t {
|
|
365
|
+
std::mem::size_of::<T>() as size_t
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
pub fn define_module(name: &CStr) -> RbResult<VALUE> {
|
|
369
|
+
protect(|| unsafe { rb_sys::rb_define_module(name.as_ptr()) })
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
pub fn define_module_under(outer: VALUE, name: &CStr) -> RbResult<VALUE> {
|
|
373
|
+
protect(|| unsafe { rb_sys::rb_define_module_under(outer, name.as_ptr()) })
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/// A class whose objects only native code creates.
|
|
377
|
+
pub fn define_class_under(outer: VALUE, name: &CStr) -> RbResult<VALUE> {
|
|
378
|
+
let class = protect(|| unsafe {
|
|
379
|
+
rb_sys::rb_define_class_under(outer, name.as_ptr(), rb_sys::rb_cObject)
|
|
380
|
+
})?;
|
|
381
|
+
protect(|| unsafe {
|
|
382
|
+
rb_sys::rb_undef_alloc_func(class);
|
|
383
|
+
qnil()
|
|
384
|
+
})?;
|
|
385
|
+
Ok(class)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
pub fn define_const(module: VALUE, name: &CStr, value: VALUE) -> RbResult<()> {
|
|
389
|
+
protect(|| unsafe {
|
|
390
|
+
rb_sys::rb_define_const(module, name.as_ptr(), value);
|
|
391
|
+
qnil()
|
|
392
|
+
})?;
|
|
393
|
+
Ok(())
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/// A method with `arity` arguments. `func` takes `self` and then each
|
|
397
|
+
/// argument as a `VALUE`.
|
|
398
|
+
pub fn define_method(class: VALUE, name: &CStr, func: *const (), arity: i32) -> RbResult<()> {
|
|
399
|
+
let func = unsafe { std::mem::transmute::<*const (), unsafe extern "C" fn() -> VALUE>(func) };
|
|
400
|
+
protect(|| unsafe {
|
|
401
|
+
rb_sys::rb_define_method(class, name.as_ptr(), Some(func), arity);
|
|
402
|
+
qnil()
|
|
403
|
+
})?;
|
|
404
|
+
Ok(())
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/// A singleton method, as [`define_method`].
|
|
408
|
+
pub fn define_singleton_method(
|
|
409
|
+
object: VALUE,
|
|
410
|
+
name: &CStr,
|
|
411
|
+
func: *const (),
|
|
412
|
+
arity: i32,
|
|
413
|
+
) -> RbResult<()> {
|
|
414
|
+
let func = unsafe { std::mem::transmute::<*const (), unsafe extern "C" fn() -> VALUE>(func) };
|
|
415
|
+
protect(|| unsafe {
|
|
416
|
+
rb_sys::rb_define_singleton_method(object, name.as_ptr(), Some(func), arity);
|
|
417
|
+
qnil()
|
|
418
|
+
})?;
|
|
419
|
+
Ok(())
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
fn c_long_len(len: usize) -> RbResult<c_long> {
|
|
423
|
+
c_long::try_from(len).map_err(|_| RubyErr::arg("length too large"))
|
|
424
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
//! `ZZQ::Native::Wake`: how a Ruby fiber waits for a Rust future.
|
|
2
|
+
//!
|
|
3
|
+
//! A native call polls the future with a waker that writes one byte to a
|
|
4
|
+
//! pipe. If the future is pending, Ruby waits for the pipe to turn
|
|
5
|
+
//! readable, which suspends only the fiber under a fiber scheduler and
|
|
6
|
+
//! releases the GVL otherwise, then polls again.
|
|
7
|
+
|
|
8
|
+
use std::future::Future;
|
|
9
|
+
use std::os::fd::RawFd;
|
|
10
|
+
use std::pin::Pin;
|
|
11
|
+
use std::sync::Arc;
|
|
12
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
13
|
+
use std::task::{Context, Poll, Waker};
|
|
14
|
+
|
|
15
|
+
use rb_sys::VALUE;
|
|
16
|
+
|
|
17
|
+
use crate::rb::{self, DataType, RbResult, RubyErr};
|
|
18
|
+
|
|
19
|
+
static TYPE: DataType = DataType::new::<Wake>(c"ZZQ::Native::Wake");
|
|
20
|
+
static CLASS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
|
|
21
|
+
|
|
22
|
+
/// A pipe one fiber waits on.
|
|
23
|
+
#[derive(Debug)]
|
|
24
|
+
pub struct Wake {
|
|
25
|
+
pipe: Arc<Pipe>,
|
|
26
|
+
waker: Waker,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[derive(Debug)]
|
|
30
|
+
struct Pipe {
|
|
31
|
+
read_fd: RawFd,
|
|
32
|
+
write_fd: RawFd,
|
|
33
|
+
/// The fiber is about to wait, so a wake must write.
|
|
34
|
+
armed: AtomicBool,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl std::task::Wake for Pipe {
|
|
38
|
+
fn wake(self: Arc<Self>) {
|
|
39
|
+
self.wake_by_ref();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn wake_by_ref(self: &Arc<Self>) {
|
|
43
|
+
if self.armed.swap(false, Ordering::AcqRel) {
|
|
44
|
+
let byte = 1u8;
|
|
45
|
+
loop {
|
|
46
|
+
let n = unsafe { libc::write(self.write_fd, (&raw const byte).cast(), 1) };
|
|
47
|
+
// A full pipe holds a wake already.
|
|
48
|
+
if n >= 0 || std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
impl Drop for Pipe {
|
|
57
|
+
fn drop(&mut self) {
|
|
58
|
+
unsafe {
|
|
59
|
+
libc::close(self.read_fd);
|
|
60
|
+
libc::close(self.write_fd);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
impl Wake {
|
|
66
|
+
fn new() -> std::io::Result<Wake> {
|
|
67
|
+
let mut fds = [0; 2];
|
|
68
|
+
if unsafe { pipe(&mut fds) } != 0 {
|
|
69
|
+
return Err(std::io::Error::last_os_error());
|
|
70
|
+
}
|
|
71
|
+
let pipe = Arc::new(Pipe {
|
|
72
|
+
read_fd: fds[0],
|
|
73
|
+
write_fd: fds[1],
|
|
74
|
+
armed: AtomicBool::new(false),
|
|
75
|
+
});
|
|
76
|
+
let waker = Waker::from(pipe.clone());
|
|
77
|
+
Ok(Wake { pipe, waker })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Polls `fut`. While it is pending, a wake writes to the pipe.
|
|
81
|
+
pub fn poll<F: Future + ?Sized>(&self, fut: Pin<&mut F>) -> Poll<F::Output> {
|
|
82
|
+
self.pipe.armed.store(true, Ordering::Release);
|
|
83
|
+
let out = fut.poll(&mut Context::from_waker(&self.waker));
|
|
84
|
+
if out.is_ready() {
|
|
85
|
+
self.pipe.armed.store(false, Ordering::Release);
|
|
86
|
+
}
|
|
87
|
+
out
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Reads the wakes written so far.
|
|
91
|
+
fn clear(&self) {
|
|
92
|
+
let mut buf = [0u8; 64];
|
|
93
|
+
loop {
|
|
94
|
+
let n = unsafe { libc::read(self.pipe.read_fd, buf.as_mut_ptr().cast(), buf.len()) };
|
|
95
|
+
if n > 0 {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if n < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[cfg(any(target_os = "linux", target_os = "android"))]
|
|
107
|
+
unsafe fn pipe(fds: &mut [RawFd; 2]) -> i32 {
|
|
108
|
+
unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
|
|
112
|
+
unsafe fn pipe(fds: &mut [RawFd; 2]) -> i32 {
|
|
113
|
+
let ret = unsafe { libc::pipe(fds.as_mut_ptr()) };
|
|
114
|
+
if ret == 0 {
|
|
115
|
+
for fd in *fds {
|
|
116
|
+
unsafe {
|
|
117
|
+
libc::fcntl(fd, libc::F_SETFL, libc::O_NONBLOCK);
|
|
118
|
+
libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
ret
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// The `Wake` inside `value`.
|
|
126
|
+
pub fn get(value: VALUE) -> RbResult<&'static Wake> {
|
|
127
|
+
unsafe { TYPE.get(value) }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
unsafe extern "C" fn wake_new(_class: VALUE) -> VALUE {
|
|
131
|
+
rb::wrap(|| {
|
|
132
|
+
let wake = Wake::new().map_err(|e| RubyErr::runtime(format!("pipe failed: {e}")))?;
|
|
133
|
+
TYPE.wrap(*CLASS.get().expect("registered") as VALUE, wake)
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
unsafe extern "C" fn wake_fd(this: VALUE) -> VALUE {
|
|
138
|
+
rb::wrap(|| Ok(rb::u64_value(get(this)?.pipe.read_fd as u64)))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
unsafe extern "C" fn wake_clear(this: VALUE) -> VALUE {
|
|
142
|
+
rb::wrap(|| {
|
|
143
|
+
get(this)?.clear();
|
|
144
|
+
Ok(rb::qnil())
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pub fn register(native: VALUE) -> RbResult<()> {
|
|
149
|
+
let class = rb::define_class_under(native, c"Wake")?;
|
|
150
|
+
let _ = CLASS.set(class as usize);
|
|
151
|
+
rb::define_singleton_method(class, c"new", wake_new as *const (), 0)?;
|
|
152
|
+
rb::define_method(class, c"fd", wake_fd as *const (), 0)?;
|
|
153
|
+
rb::define_method(class, c"clear", wake_clear as *const (), 0)?;
|
|
154
|
+
Ok(())
|
|
155
|
+
}
|
data/lib/zzq/broker.rb
CHANGED
|
@@ -1,34 +1,103 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "socket"
|
|
4
|
-
|
|
5
3
|
module ZZQ
|
|
6
|
-
# MQTT
|
|
7
|
-
#
|
|
4
|
+
# An MQTT 3.1.1 and 5.0 broker with an MQTT-SN gateway on +udp://+. It
|
|
5
|
+
# runs on the process's IO lanes; see {ZZQ.io_threads=}.
|
|
8
6
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
# @example
|
|
8
|
+
# broker = ZZQ::Broker.new
|
|
9
|
+
# broker.bind("mqtt://0.0.0.0:1883")
|
|
10
|
+
class Broker
|
|
11
|
+
OPTIONS = %i[
|
|
12
|
+
max_packet_size max_inflight max_queued receive_maximum max_subscriptions max_retained
|
|
13
|
+
max_topic_levels topic_alias_maximum max_session_expiry max_connections max_keep_alive
|
|
14
|
+
connect_timeout sys_interval persistence tls
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
# Starts a broker and binds it. With a block, yields it and closes it
|
|
18
|
+
# after.
|
|
19
|
+
#
|
|
20
|
+
# @param endpoint [String] see {#bind}
|
|
21
|
+
# @param options [Hash] see {#initialize}
|
|
22
|
+
# @return [Broker, Object] the broker, or the block's result
|
|
23
|
+
def self.bind(endpoint, **options)
|
|
24
|
+
broker = new(**options)
|
|
25
|
+
broker.bind(endpoint)
|
|
26
|
+
return broker unless block_given?
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
yield broker
|
|
30
|
+
ensure
|
|
31
|
+
broker.close
|
|
32
|
+
end
|
|
14
33
|
end
|
|
15
34
|
|
|
35
|
+
# @return [String, nil] the endpoint bound last
|
|
36
|
+
attr_reader :last_endpoint
|
|
16
37
|
|
|
17
|
-
#
|
|
18
|
-
|
|
38
|
+
# Starts a broker.
|
|
39
|
+
#
|
|
40
|
+
# @param options [Hash] limits as in zzq-broker's +BrokerOptions+, such
|
|
41
|
+
# as +max_connections:+ or +max_queued:+; +max_keep_alive:+ and
|
|
42
|
+
# +sys_interval:+ (seconds, nil for none); +connect_timeout:+
|
|
43
|
+
# (seconds); +persistence:+ (path of a file for retained messages);
|
|
44
|
+
# +tls:+ (+cert:+, +key:+, +client_ca:+ as PEM strings)
|
|
45
|
+
def initialize(**options)
|
|
46
|
+
opts = Options.check(options, OPTIONS)
|
|
47
|
+
@native = Waiting.await(Native::Broker.start(opts))
|
|
48
|
+
@closed = false
|
|
49
|
+
end
|
|
19
50
|
|
|
51
|
+
# Listens on an endpoint.
|
|
52
|
+
#
|
|
53
|
+
# @param endpoint [String] +mqtt://host:port+, +mqtts://+, +tcp://+,
|
|
54
|
+
# +tls://+, +ws://+, +wss://+, +quic://+, +udp://+ (MQTT-SN),
|
|
55
|
+
# +ipc://+ or +inproc://+; port 0 picks a free port
|
|
56
|
+
# @return [String] the bound endpoint, with the actual port
|
|
57
|
+
def bind(endpoint)
|
|
58
|
+
@last_endpoint = Waiting.await(native.bind(Options.endpoint(endpoint)))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Publishes a message as the broker itself.
|
|
62
|
+
#
|
|
63
|
+
# @param topic [String]
|
|
64
|
+
# @param payload [String]
|
|
65
|
+
# @param qos [Integer]
|
|
66
|
+
# @param retain [Boolean]
|
|
67
|
+
# @return [nil]
|
|
68
|
+
def publish(topic, payload, qos: 0, retain: false)
|
|
69
|
+
payload = payload.to_s unless payload.is_a?(String)
|
|
70
|
+
native.publish(topic, payload, qos, retain)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Hash{Symbol => Integer}] counters summed over lanes
|
|
74
|
+
def stats
|
|
75
|
+
Waiting.await(native.stats)
|
|
76
|
+
end
|
|
20
77
|
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@
|
|
78
|
+
# Stops the broker and waits until its connections are closed.
|
|
79
|
+
#
|
|
80
|
+
# @return [nil]
|
|
81
|
+
def close
|
|
82
|
+
return if @closed
|
|
83
|
+
|
|
84
|
+
@closed = true
|
|
85
|
+
op = @native.shutdown
|
|
86
|
+
Waiting.await(op) if op
|
|
28
87
|
nil
|
|
29
88
|
end
|
|
30
|
-
|
|
89
|
+
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def closed?
|
|
92
|
+
@closed
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def native
|
|
98
|
+
raise ClosedError, "broker closed" if @closed
|
|
99
|
+
|
|
100
|
+
@native
|
|
101
|
+
end
|
|
31
102
|
end
|
|
32
103
|
end
|
|
33
|
-
|
|
34
|
-
require_relative "routing/broker"
|