zzq 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98819b7bfdca1b66f9a1e733cb86f851c1fc8fae5776746fad283020d90cb2ec
4
- data.tar.gz: 88f929dd51fa6804f022a607f5767f11c241759c7939f2c788198f27edd9b36c
3
+ metadata.gz: 2131a7723ecf3fde157b22ad8d84d9a03b7703fd30cb439719235047424aaded
4
+ data.tar.gz: a09642a35676ba55d866b1e813a0e4162b895131703166e77bd4c539d5c572ef
5
5
  SHA512:
6
- metadata.gz: 5ad1de21bc0adc67b0e37d301ca667e9f075f0405451c35c7b91323f4abbbe18644ced47badef177a191f6a1a5828d5277e863d50a3f94af9c4198a9d6bfdc05
7
- data.tar.gz: 10b14f7be24aa84b9b27c96707cd836b746fd132b83c0ee8344dc42c7dd98b56f25b63d9d9275809d2cc55d33e3ca9c0d40dd893db6b5bd57332b48ff3072764
6
+ metadata.gz: 10270ffcd3f415d9aa38feda85cba120037249a6997beca140b01dbd632b8a1622851ac305d2a812090e9d4a785c547f71523f88e327d4975d153ba337584c55
7
+ data.tar.gz: 961802b3a00cb62b843a93316aefdf29b56c44b582f75bbafee82d5fd8db4a18c6a5557f1248a1bcbf881cc1983138de2a4476b02ec140adae6716f64afca9e5
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## v0.8.0 — 2026-11-03
6
+
7
+ - Built on the zzq 0.6 crates. Bind and connect errors name the endpoint
8
+ as given: a `tls://`, `ws://` or `wss://` bind no longer says `tcp://`,
9
+ and an IPv6 host keeps its brackets.
10
+
5
11
  ## v0.7.0 — 2026-10-31
6
12
 
7
13
  - Built on the zzq 0.5 crates, with their fixes. With `data_dir:`, a QoS 2
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "zzq_native"
3
- version = "0.7.0"
3
+ version = "0.8.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.93"
6
6
  license = "ISC OR Apache-2.0"
@@ -12,8 +12,8 @@ name = "zzq_native"
12
12
  crate-type = ["cdylib"]
13
13
 
14
14
  [dependencies]
15
- zzq-broker = "0.5"
16
- zzq-client = "0.5"
15
+ zzq-broker = "0.6"
16
+ zzq-client = "0.6"
17
17
  bytes = "1.12"
18
18
  libc = "0.2"
19
19
  rb-sys = "0.9"
@@ -1,6 +1,6 @@
1
1
  //! `ZZQ::Native::Broker`: a running broker.
2
2
 
3
- use std::sync::{Arc, OnceLock};
3
+ use std::sync::Arc;
4
4
 
5
5
  use rb_sys::VALUE;
6
6
  use tokio::sync::{OwnedRwLockReadGuard, RwLock};
@@ -11,8 +11,7 @@ use crate::op::{self, Out};
11
11
  use crate::rb::{self, DataType, RbResult, RubyErr};
12
12
  use crate::{context, options};
13
13
 
14
- static TYPE: DataType = DataType::new::<NativeBroker>(c"ZZQ::Native::Broker");
15
- static CLASS: OnceLock<usize> = OnceLock::new();
14
+ static TYPE: DataType<NativeBroker> = DataType::new(c"ZZQ::Native::Broker");
16
15
 
17
16
  #[derive(Debug)]
18
17
  struct NativeBroker {
@@ -26,7 +25,7 @@ type Held = OwnedRwLockReadGuard<Option<Broker>, Broker>;
26
25
 
27
26
  /// The broker for one call, or `Closed` once a shutdown has begun.
28
27
  fn get(value: VALUE) -> RbResult<Held> {
29
- let native: &NativeBroker = unsafe { TYPE.get(value) }?;
28
+ let native: &NativeBroker = TYPE.get(value)?;
30
29
  native
31
30
  .broker
32
31
  .clone()
@@ -41,7 +40,7 @@ pub fn wrap(b: Broker) -> RbResult<VALUE> {
41
40
  let native = NativeBroker {
42
41
  broker: Arc::new(RwLock::new(Some(b))),
43
42
  };
44
- TYPE.wrap(*CLASS.get().expect("registered") as VALUE, native)
43
+ TYPE.wrap(native)
45
44
  }
46
45
 
47
46
  pub fn stats_hash(s: &Stats) -> RbResult<VALUE> {
@@ -115,7 +114,7 @@ fn stats(this: VALUE) -> RbResult<VALUE> {
115
114
  /// An op that waits for the calls using the broker to finish, then shuts
116
115
  /// it down.
117
116
  fn shutdown(this: VALUE) -> RbResult<VALUE> {
118
- let native: &NativeBroker = unsafe { TYPE.get(this) }?;
117
+ let native: &NativeBroker = TYPE.get(this)?;
119
118
  let broker = native.broker.clone();
120
119
  op::new(async move {
121
120
  let Some(b) = broker.write_owned().await.take() else {
@@ -126,39 +125,14 @@ fn shutdown(this: VALUE) -> RbResult<VALUE> {
126
125
  })
127
126
  }
128
127
 
129
- unsafe extern "C" fn broker_start(_class: VALUE, opts: VALUE) -> VALUE {
130
- rb::wrap(|| start(opts))
131
- }
132
-
133
- unsafe extern "C" fn broker_bind(this: VALUE, uri: VALUE) -> VALUE {
134
- rb::wrap(|| bind(this, uri))
135
- }
136
-
137
- unsafe extern "C" fn broker_publish(
138
- this: VALUE,
139
- topic: VALUE,
140
- payload: VALUE,
141
- qos: VALUE,
142
- retain: VALUE,
143
- ) -> VALUE {
144
- rb::wrap(|| publish(this, topic, payload, qos, retain))
145
- }
146
-
147
- unsafe extern "C" fn broker_stats(this: VALUE) -> VALUE {
148
- rb::wrap(|| stats(this))
149
- }
150
-
151
- unsafe extern "C" fn broker_shutdown(this: VALUE) -> VALUE {
152
- rb::wrap(|| shutdown(this))
153
- }
154
-
155
128
  pub fn register(native: VALUE) -> RbResult<()> {
156
- let class = rb::define_class_under(native, c"Broker")?;
157
- let _ = CLASS.set(class as usize);
158
- rb::define_singleton_method(class, c"start", broker_start as *const (), 1)?;
159
- rb::define_method(class, c"bind", broker_bind as *const (), 1)?;
160
- rb::define_method(class, c"publish", broker_publish as *const (), 4)?;
161
- rb::define_method(class, c"stats", broker_stats as *const (), 0)?;
162
- rb::define_method(class, c"shutdown", broker_shutdown as *const (), 0)?;
129
+ let class = TYPE.register(native, c"Broker")?;
130
+ rb::singleton!(class, c"start", |_class, opts| start(opts))?;
131
+ rb::method!(class, c"bind", |this, uri| bind(this, uri))?;
132
+ rb::method!(class, c"publish", |this, topic, payload, qos, retain| {
133
+ publish(this, topic, payload, qos, retain)
134
+ })?;
135
+ rb::method!(class, c"stats", |this| stats(this))?;
136
+ rb::method!(class, c"shutdown", |this| shutdown(this))?;
163
137
  Ok(())
164
138
  }
@@ -18,8 +18,7 @@ use crate::op::{self, Out};
18
18
  use crate::rb::{self, DataType, RbResult, RubyErr};
19
19
  use crate::{context, options, wake};
20
20
 
21
- static TYPE: DataType = DataType::new::<NativeClient>(c"ZZQ::Native::Client");
22
- static CLASS: OnceLock<usize> = OnceLock::new();
21
+ static TYPE: DataType<NativeClient> = DataType::new(c"ZZQ::Native::Client");
23
22
  /// `ZZQ::Message` and a frozen empty Array for messages without
24
23
  /// Subscription Identifiers.
25
24
  static MESSAGE: OnceLock<(usize, usize)> = OnceLock::new();
@@ -44,7 +43,7 @@ impl std::fmt::Debug for NativeClient {
44
43
  }
45
44
 
46
45
  fn get(value: VALUE) -> RbResult<&'static NativeClient> {
47
- unsafe { TYPE.get(value) }
46
+ TYPE.get(value)
48
47
  }
49
48
 
50
49
  /// A Ruby object for a connected client.
@@ -57,7 +56,7 @@ pub fn wrap(c: Client) -> RbResult<VALUE> {
57
56
  client: Arc::new(tokio::sync::Mutex::new(c)),
58
57
  recv: Mutex::new(None),
59
58
  };
60
- TYPE.wrap(*CLASS.get().expect("registered") as VALUE, native)
59
+ TYPE.wrap(native)
61
60
  }
62
61
 
63
62
  fn connect(uri: VALUE, opts: VALUE) -> RbResult<VALUE> {
@@ -223,72 +222,37 @@ fn disconnect(this: VALUE, reason: VALUE) -> RbResult<VALUE> {
223
222
  })
224
223
  }
225
224
 
226
- unsafe extern "C" fn client_connect(_class: VALUE, uri: VALUE, opts: VALUE) -> VALUE {
227
- rb::wrap(|| connect(uri, opts))
228
- }
229
-
230
- unsafe extern "C" fn client_publish(
231
- this: VALUE,
232
- topic: VALUE,
233
- payload: VALUE,
234
- qos: VALUE,
235
- retain: VALUE,
236
- ) -> VALUE {
237
- rb::wrap(|| publish(this, topic, payload, qos, retain))
238
- }
239
-
240
- unsafe extern "C" fn client_subscribe(this: VALUE, filters: VALUE, sub_id: VALUE) -> VALUE {
241
- rb::wrap(|| subscribe(this, filters, sub_id))
242
- }
243
-
244
- unsafe extern "C" fn client_unsubscribe(this: VALUE, filters: VALUE) -> VALUE {
245
- rb::wrap(|| unsubscribe(this, filters))
246
- }
247
-
248
- unsafe extern "C" fn client_receive(this: VALUE, wake: VALUE, max: VALUE) -> VALUE {
249
- rb::wrap(|| receive(this, wake, max))
250
- }
251
-
252
- unsafe extern "C" fn client_disconnect(this: VALUE, reason: VALUE) -> VALUE {
253
- rb::wrap(|| disconnect(this, reason))
254
- }
255
-
256
- unsafe extern "C" fn client_session_present(this: VALUE) -> VALUE {
257
- rb::wrap(|| Ok(rb::bool_value(get(this)?.session_present)))
258
- }
259
-
260
- unsafe extern "C" fn client_assigned_client_id(this: VALUE) -> VALUE {
261
- rb::wrap(|| match &get(this)?.assigned_client_id {
262
- Some(id) => rb::new_utf8_string(id),
263
- None => Ok(rb::qnil()),
264
- })
265
- }
266
-
267
225
  pub fn register(zzq: VALUE, native: VALUE) -> RbResult<()> {
268
226
  let message = rb::const_get(zzq, c"Message")?;
269
227
  let no_ids = rb::freeze(rb::array_new_capa(0)?);
270
228
  rb::define_const(native, c"NO_SUBSCRIPTION_IDS", no_ids)?;
271
229
  let _ = MESSAGE.set((message as usize, no_ids as usize));
272
230
 
273
- let class = rb::define_class_under(native, c"Client")?;
274
- let _ = CLASS.set(class as usize);
275
- rb::define_singleton_method(class, c"connect", client_connect as *const (), 2)?;
276
- rb::define_method(class, c"publish", client_publish as *const (), 4)?;
277
- rb::define_method(class, c"subscribe", client_subscribe as *const (), 2)?;
278
- rb::define_method(class, c"unsubscribe", client_unsubscribe as *const (), 1)?;
279
- rb::define_method(class, c"receive", client_receive as *const (), 2)?;
280
- rb::define_method(class, c"disconnect", client_disconnect as *const (), 1)?;
281
- rb::define_method(
282
- class,
283
- c"session_present?",
284
- client_session_present as *const (),
285
- 0,
286
- )?;
287
- rb::define_method(
288
- class,
289
- c"assigned_client_id",
290
- client_assigned_client_id as *const (),
291
- 0,
292
- )?;
231
+ let class = TYPE.register(native, c"Client")?;
232
+ rb::singleton!(class, c"connect", |_class, uri, opts| connect(uri, opts))?;
233
+ rb::method!(class, c"publish", |this, topic, payload, qos, retain| {
234
+ publish(this, topic, payload, qos, retain)
235
+ })?;
236
+ rb::method!(class, c"subscribe", |this, filters, sub_id| {
237
+ subscribe(this, filters, sub_id)
238
+ })?;
239
+ rb::method!(class, c"unsubscribe", |this, filters| unsubscribe(
240
+ this, filters
241
+ ))?;
242
+ rb::method!(class, c"receive", |this, wake, max| receive(
243
+ this, wake, max
244
+ ))?;
245
+ rb::method!(class, c"disconnect", |this, reason| disconnect(
246
+ this, reason
247
+ ))?;
248
+ rb::method!(class, c"session_present?", |this| {
249
+ Ok(rb::bool_value(get(this)?.session_present))
250
+ })?;
251
+ rb::method!(class, c"assigned_client_id", |this| {
252
+ match &get(this)?.assigned_client_id {
253
+ Some(id) => rb::new_utf8_string(id),
254
+ None => Ok(rb::qnil()),
255
+ }
256
+ })?;
293
257
  Ok(())
294
258
  }
@@ -9,27 +9,8 @@ mod options;
9
9
  mod rb;
10
10
  mod wake;
11
11
 
12
- use rb_sys::VALUE;
13
-
14
12
  use crate::rb::RbResult;
15
13
 
16
- unsafe extern "C" fn io_threads(_module: VALUE) -> VALUE {
17
- rb::wrap(|| Ok(rb::u64_value(context::io_threads() as u64)))
18
- }
19
-
20
- unsafe extern "C" fn set_io_threads(_module: VALUE, n: VALUE) -> VALUE {
21
- rb::wrap(|| {
22
- let n = usize::try_from(rb::value_to_i64(n)?)
23
- .map_err(|_| rb::RubyErr::arg("io_threads must be positive"))?;
24
- if !context::set_io_threads(n) {
25
- return Err(error::zzq(
26
- "set io_threads before the first client or broker",
27
- ));
28
- }
29
- Ok(rb::qnil())
30
- })
31
- }
32
-
33
14
  /// Called once by Ruby when it loads the extension.
34
15
  #[unsafe(no_mangle)]
35
16
  pub extern "C" fn Init_zzq_native() {
@@ -42,8 +23,19 @@ fn init() -> RbResult<()> {
42
23
  let zzq = rb::define_module(c"ZZQ")?;
43
24
  let native = rb::define_module_under(zzq, c"Native")?;
44
25
  error::init(zzq)?;
45
- rb::define_singleton_method(native, c"io_threads", io_threads as *const (), 0)?;
46
- rb::define_singleton_method(native, c"io_threads=", set_io_threads as *const (), 1)?;
26
+ rb::singleton!(native, c"io_threads", |_module| {
27
+ Ok(rb::u64_value(context::io_threads() as u64))
28
+ })?;
29
+ rb::singleton!(native, c"io_threads=", |_module, n| {
30
+ let n = usize::try_from(rb::value_to_i64(n)?)
31
+ .map_err(|_| rb::RubyErr::arg("io_threads must be positive"))?;
32
+ if !context::set_io_threads(n) {
33
+ return Err(error::zzq(
34
+ "set io_threads before the first client or broker",
35
+ ));
36
+ }
37
+ Ok(rb::qnil())
38
+ })?;
47
39
  wake::register(native)?;
48
40
  op::register(native)?;
49
41
  client::register(zzq, native)?;
@@ -14,8 +14,7 @@ use crate::error::Error;
14
14
  use crate::rb::{self, DataType, RbResult};
15
15
  use crate::{broker, client, wake};
16
16
 
17
- static TYPE: DataType = DataType::new::<Op>(c"ZZQ::Native::Op");
18
- static CLASS: OnceLock<usize> = OnceLock::new();
17
+ static TYPE: DataType<Op> = DataType::new(c"ZZQ::Native::Op");
19
18
  static PENDING: OnceLock<usize> = OnceLock::new();
20
19
 
21
20
  /// What a finished op returns to Ruby.
@@ -65,11 +64,11 @@ fn wrap(task: Task) -> RbResult<VALUE> {
65
64
  let op = Op {
66
65
  task: Mutex::new(Some(task)),
67
66
  };
68
- TYPE.wrap(*CLASS.get().expect("registered") as VALUE, op)
67
+ TYPE.wrap(op)
69
68
  }
70
69
 
71
70
  fn get(value: VALUE) -> RbResult<&'static Op> {
72
- unsafe { TYPE.get(value) }
71
+ TYPE.get(value)
73
72
  }
74
73
 
75
74
  fn poll(this: VALUE, wake: VALUE) -> RbResult<VALUE> {
@@ -109,12 +108,10 @@ fn to_ruby(out: Out) -> RbResult<VALUE> {
109
108
  }
110
109
  }
111
110
 
112
- unsafe extern "C" fn op_poll(this: VALUE, wake: VALUE) -> VALUE {
113
- rb::wrap(|| poll(this, wake))
114
- }
115
-
116
- unsafe extern "C" fn op_cancel(this: VALUE) -> VALUE {
117
- rb::wrap(|| {
111
+ pub fn register(native: VALUE) -> RbResult<()> {
112
+ let class = TYPE.register(native, c"Op")?;
113
+ rb::method!(class, c"poll", |this, wake| poll(this, wake))?;
114
+ rb::method!(class, c"cancel", |this| {
118
115
  let task = get(this)?
119
116
  .task
120
117
  .lock()
@@ -122,14 +119,7 @@ unsafe extern "C" fn op_cancel(this: VALUE) -> VALUE {
122
119
  .take();
123
120
  drop(task);
124
121
  Ok(rb::qnil())
125
- })
126
- }
127
-
128
- pub fn register(native: VALUE) -> RbResult<()> {
129
- let class = rb::define_class_under(native, c"Op")?;
130
- let _ = CLASS.set(class as usize);
131
- rb::define_method(class, c"poll", op_poll as *const (), 1)?;
132
- rb::define_method(class, c"cancel", op_cancel as *const (), 0)?;
122
+ })?;
133
123
  let pending = rb::freeze(rb::protect(|| unsafe {
134
124
  rb_sys::rb_obj_alloc(rb_sys::rb_cObject)
135
125
  })?);
@@ -2,8 +2,10 @@
2
2
  //! `rb_protect`, so a Ruby exception never unwinds through Rust frames.
3
3
 
4
4
  use std::ffi::{CStr, CString, c_char, c_long, c_void};
5
+ use std::marker::PhantomData;
5
6
  use std::panic::{AssertUnwindSafe, catch_unwind};
6
7
  use std::ptr;
8
+ use std::sync::OnceLock;
7
9
 
8
10
  use rb_sys::{VALUE, rb_data_type_struct__bindgen_ty_1, rb_data_type_t, size_t};
9
11
 
@@ -310,47 +312,67 @@ pub fn const_get(module: VALUE, name: &CStr) -> RbResult<VALUE> {
310
312
  protect(|| unsafe { rb_sys::rb_const_get(module, rb_sys::rb_intern(name.as_ptr())) })
311
313
  }
312
314
 
313
- /// A Ruby type for a boxed `T`, which GC frees with the object.
314
- pub struct DataType(rb_data_type_t);
315
-
316
- unsafe impl Send for DataType {}
317
- unsafe impl Sync for DataType {}
318
-
319
- impl DataType {
320
- pub const fn new<T>(name: &'static CStr) -> Self {
321
- DataType(rb_data_type_t {
322
- wrap_struct_name: name.as_ptr(),
323
- function: rb_data_type_struct__bindgen_ty_1 {
324
- dmark: None,
325
- dfree: Some(free_box::<T>),
326
- dsize: Some(size_box::<T>),
327
- dcompact: None,
328
- reserved: [ptr::null_mut(); 1],
315
+ /// A Ruby class whose objects each hold a boxed `T`, which GC frees with
316
+ /// the object.
317
+ pub struct DataType<T> {
318
+ raw: rb_data_type_t,
319
+ /// The class, once [`DataType::register`] defined it.
320
+ class: OnceLock<usize>,
321
+ _value: PhantomData<fn() -> T>,
322
+ }
323
+
324
+ // SAFETY: the raw type holds only pointers to statics and functions,
325
+ // which Ruby reads and nothing writes.
326
+ unsafe impl<T> Send for DataType<T> {}
327
+ unsafe impl<T> Sync for DataType<T> {}
328
+
329
+ impl<T> DataType<T> {
330
+ pub const fn new(name: &'static CStr) -> Self {
331
+ DataType {
332
+ raw: rb_data_type_t {
333
+ wrap_struct_name: name.as_ptr(),
334
+ function: rb_data_type_struct__bindgen_ty_1 {
335
+ dmark: None,
336
+ dfree: Some(free_box::<T>),
337
+ dsize: Some(size_box::<T>),
338
+ dcompact: None,
339
+ reserved: [ptr::null_mut(); 1],
340
+ },
341
+ parent: ptr::null(),
342
+ data: ptr::null_mut(),
343
+ // RUBY_TYPED_FREE_IMMEDIATELY: dropping never blocks or calls Ruby.
344
+ flags: 1,
329
345
  },
330
- parent: ptr::null(),
331
- data: ptr::null_mut(),
332
- // RUBY_TYPED_FREE_IMMEDIATELY: dropping never blocks or calls Ruby.
333
- flags: 1,
334
- })
346
+ class: OnceLock::new(),
347
+ _value: PhantomData,
348
+ }
335
349
  }
336
350
 
337
351
  const fn ptr(&'static self) -> *const rb_data_type_t {
338
- &raw const self.0
352
+ &raw const self.raw
353
+ }
354
+
355
+ /// Defines the class, `name` under `outer`, whose objects hold a `T`.
356
+ pub fn register(&'static self, outer: VALUE, name: &CStr) -> RbResult<VALUE> {
357
+ let class = define_class_under(outer, name)?;
358
+ let _ = self.class.set(class as usize);
359
+ Ok(class)
339
360
  }
340
361
 
341
- /// Wraps `value` in a new object of `class`.
342
- pub fn wrap<T>(&'static self, class: VALUE, value: T) -> RbResult<VALUE> {
362
+ /// Wraps `value` in a new object of the class.
363
+ pub fn wrap(&'static self, value: T) -> RbResult<VALUE> {
364
+ let class = *self
365
+ .class
366
+ .get()
367
+ .ok_or_else(|| RubyErr::runtime("native class not registered"))?
368
+ as VALUE;
343
369
  let raw = Box::into_raw(Box::new(value));
344
370
  protect(|| unsafe { rb_sys::rb_data_typed_object_wrap(class, raw.cast(), self.ptr()) })
345
371
  .inspect_err(|_| drop(unsafe { Box::from_raw(raw) }))
346
372
  }
347
373
 
348
- /// The `T` inside `value`.
349
- ///
350
- /// # Safety
351
- ///
352
- /// `T` must be the type this data type was made for.
353
- pub unsafe fn get<T>(&'static self, value: VALUE) -> RbResult<&'static T> {
374
+ /// The `T` inside `value`, which must be an object of the class.
375
+ pub fn get(&'static self, value: VALUE) -> RbResult<&'static T> {
354
376
  let mut ptr = ptr::null_mut();
355
377
  protect(|| unsafe {
356
378
  ptr = rb_sys::rb_check_typeddata(value, self.ptr());
@@ -359,6 +381,8 @@ impl DataType {
359
381
  if ptr.is_null() {
360
382
  return Err(RubyErr::runtime("native object is empty"));
361
383
  }
384
+ // SAFETY: Ruby checked that `value` has this data type, whose
385
+ // objects hold a boxed `T` that lives as long as the object.
362
386
  Ok(unsafe { &*ptr.cast::<T>() })
363
387
  }
364
388
  }
@@ -403,27 +427,60 @@ pub fn define_const(module: VALUE, name: &CStr, value: VALUE) -> RbResult<()> {
403
427
  Ok(())
404
428
  }
405
429
 
406
- /// A method with `arity` arguments. `func` takes `self` and then each
407
- /// argument as a `VALUE`.
408
- pub fn define_method(class: VALUE, name: &CStr, func: *const (), arity: i32) -> RbResult<()> {
409
- let func = unsafe { std::mem::transmute::<*const (), unsafe extern "C" fn() -> VALUE>(func) };
410
- protect(|| unsafe {
411
- rb_sys::rb_define_method(class, name.as_ptr(), Some(func), arity);
412
- qnil()
413
- })?;
414
- Ok(())
430
+ /// Defines method `$name` of `$class`, which runs `$body` with the
431
+ /// receiver as `$this` and each argument as a `VALUE`. The macro writes
432
+ /// the C function and counts its arguments, so Ruby's arity matches it.
433
+ macro_rules! method {
434
+ ($class:expr, $name:expr, |$this:ident $(, $arg:ident)*| $body:expr) => {{
435
+ unsafe extern "C" fn shim(
436
+ $this: ::rb_sys::VALUE $(, $arg: ::rb_sys::VALUE)*
437
+ ) -> ::rb_sys::VALUE {
438
+ $crate::rb::wrap(|| $body)
439
+ }
440
+ let arity = 0 $(+ $crate::rb::one(stringify!($arg)))*;
441
+ $crate::rb::define(false, $class, $name, shim as *const (), arity)
442
+ }};
443
+ }
444
+
445
+ /// [`method!`] on the singleton class: `$this` is the class or module.
446
+ macro_rules! singleton {
447
+ ($object:expr, $name:expr, |$this:ident $(, $arg:ident)*| $body:expr) => {{
448
+ unsafe extern "C" fn shim(
449
+ $this: ::rb_sys::VALUE $(, $arg: ::rb_sys::VALUE)*
450
+ ) -> ::rb_sys::VALUE {
451
+ $crate::rb::wrap(|| $body)
452
+ }
453
+ let arity = 0 $(+ $crate::rb::one(stringify!($arg)))*;
454
+ $crate::rb::define(true, $object, $name, shim as *const (), arity)
455
+ }};
456
+ }
457
+
458
+ pub(crate) use {method, singleton};
459
+
460
+ /// One argument, for the arity [`method!`] counts.
461
+ pub const fn one(_: &str) -> i32 {
462
+ 1
415
463
  }
416
464
 
417
- /// A singleton method, as [`define_method`].
418
- pub fn define_singleton_method(
465
+ /// Defines `func`, a C function of `arity` arguments after the receiver,
466
+ /// as a method of `object`, or of its singleton class. Only [`method!`]
467
+ /// and [`singleton!`] call it, with a `func` they made for that arity.
468
+ pub fn define(
469
+ singleton: bool,
419
470
  object: VALUE,
420
471
  name: &CStr,
421
472
  func: *const (),
422
473
  arity: i32,
423
474
  ) -> RbResult<()> {
475
+ // SAFETY: Ruby calls `func` with the receiver and `arity` VALUEs,
476
+ // the parameters `method!` gave it.
424
477
  let func = unsafe { std::mem::transmute::<*const (), unsafe extern "C" fn() -> VALUE>(func) };
425
478
  protect(|| unsafe {
426
- rb_sys::rb_define_singleton_method(object, name.as_ptr(), Some(func), arity);
479
+ if singleton {
480
+ rb_sys::rb_define_singleton_method(object, name.as_ptr(), Some(func), arity);
481
+ } else {
482
+ rb_sys::rb_define_method(object, name.as_ptr(), Some(func), arity);
483
+ }
427
484
  qnil()
428
485
  })?;
429
486
  Ok(())
@@ -16,8 +16,7 @@ use rb_sys::VALUE;
16
16
 
17
17
  use crate::rb::{self, DataType, RbResult, RubyErr};
18
18
 
19
- static TYPE: DataType = DataType::new::<Wake>(c"ZZQ::Native::Wake");
20
- static CLASS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19
+ static TYPE: DataType<Wake> = DataType::new(c"ZZQ::Native::Wake");
21
20
 
22
21
  /// A pipe one fiber waits on.
23
22
  #[derive(Debug)]
@@ -124,32 +123,21 @@ unsafe fn pipe(fds: &mut [RawFd; 2]) -> i32 {
124
123
 
125
124
  /// The `Wake` inside `value`.
126
125
  pub fn get(value: VALUE) -> RbResult<&'static Wake> {
127
- unsafe { TYPE.get(value) }
126
+ TYPE.get(value)
128
127
  }
129
128
 
130
- unsafe extern "C" fn wake_new(_class: VALUE) -> VALUE {
131
- rb::wrap(|| {
129
+ pub fn register(native: VALUE) -> RbResult<()> {
130
+ let class = TYPE.register(native, c"Wake")?;
131
+ rb::singleton!(class, c"new", |_class| {
132
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(|| {
133
+ TYPE.wrap(wake)
134
+ })?;
135
+ rb::method!(class, c"fd", |this| Ok(rb::u64_value(
136
+ get(this)?.pipe.read_fd as u64
137
+ )))?;
138
+ rb::method!(class, c"clear", |this| {
143
139
  get(this)?.clear();
144
140
  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)?;
141
+ })?;
154
142
  Ok(())
155
143
  }
data/lib/zzq/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ZZQ
4
4
  # Gem version.
5
- VERSION = "0.7.0"
5
+ VERSION = "0.8.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zzq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger