rubyx-py 0.1.0 → 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.
@@ -1,3 +1,4 @@
1
+ use crate::gvl::{rb_thread_call_without_gvl, rb_thread_check_ints, recv_loop, ubf_cancel};
1
2
  use crate::python_api::PythonApi;
2
3
  use crate::python_ffi::PyObject;
3
4
  use crate::ruby_helpers::runtime_error;
@@ -6,8 +7,23 @@ use crate::stream::SendableValue;
6
7
  use crossbeam_channel::{bounded, Receiver};
7
8
  use magnus::{Error, Value};
8
9
  use std::cell::RefCell;
10
+ use std::ffi::c_void;
11
+ use std::sync::atomic::AtomicBool;
12
+ use std::sync::Arc;
9
13
  use std::thread::{self, JoinHandle};
10
14
 
15
+ struct FutureRecvArgs {
16
+ receiver: Receiver<Result<SendableValue, String>>,
17
+ result: Option<Result<Result<SendableValue, String>, crossbeam_channel::RecvError>>,
18
+ cancel: Arc<AtomicBool>,
19
+ }
20
+
21
+ unsafe extern "C" fn future_recv_cb(args: *mut c_void) -> *mut c_void {
22
+ let args = &mut *(args as *mut FutureRecvArgs);
23
+ args.result = recv_loop(&args.receiver, &args.cancel);
24
+ std::ptr::null_mut()
25
+ }
26
+
11
27
  /// A future representing an async Python operation running on a background thread.
12
28
  ///
13
29
  /// The Python coroutine is executed via `asyncio.run()` on a dedicated thread.
@@ -49,22 +65,10 @@ impl RubyxFuture {
49
65
  }
50
66
  }
51
67
 
52
- /// Block until the result is ready and return it as a Ruby value.
68
+ /// Block until the future is ready and releasing the GVL so other Ruby threads can run.
53
69
  /// Can only be called once — subsequent calls return an error.
54
70
  pub fn value(&self) -> Result<Value, Error> {
55
- // Join the worker thread first
56
- if let Some(handle) = self.handle.borrow_mut().take() {
57
- let _ = handle.join();
58
- }
59
-
60
- match self.receiver.try_recv() {
61
- Ok(Ok(sendable)) => sendable.try_into(),
62
- Ok(Err(err)) => Err(Error::new(runtime_error(), err)),
63
- Err(_) => Err(Error::new(
64
- runtime_error(),
65
- "Future already consumed or worker failed",
66
- )),
67
- }
71
+ value_nonblocking(self)
68
72
  }
69
73
 
70
74
  pub fn is_ready(&self) -> bool {
@@ -80,6 +84,49 @@ impl Drop for RubyxFuture {
80
84
  }
81
85
  }
82
86
 
87
+ /// Block until the future is ready, releasing the GVL so other Ruby threads can run.
88
+ pub(crate) fn value_nonblocking(future: &RubyxFuture) -> Result<Value, Error> {
89
+ if let Ok(result) = future.receiver.try_recv() {
90
+ if let Some(handle) = future.handle.borrow_mut().take() {
91
+ let _ = handle.join();
92
+ }
93
+ return match result {
94
+ Ok(sendable) => sendable.try_into(),
95
+ Err(err) => Err(Error::new(runtime_error(), err)),
96
+ };
97
+ }
98
+
99
+ let cancel = Arc::new(AtomicBool::new(false));
100
+ let mut args = FutureRecvArgs {
101
+ receiver: future.receiver.clone(),
102
+ result: None,
103
+ cancel: cancel.clone(),
104
+ };
105
+
106
+ unsafe {
107
+ rb_thread_call_without_gvl(
108
+ future_recv_cb,
109
+ &mut args as *mut FutureRecvArgs as *mut c_void,
110
+ Some(ubf_cancel),
111
+ Arc::as_ptr(&cancel) as *mut c_void,
112
+ );
113
+ rb_thread_check_ints();
114
+ }
115
+
116
+ if let Some(handle) = future.handle.borrow_mut().take() {
117
+ let _ = handle.join();
118
+ }
119
+
120
+ match args.result {
121
+ Some(Ok(Ok(sendable))) => sendable.try_into(),
122
+ Some(Ok(Err(err))) => Err(Error::new(runtime_error(), err)),
123
+ _ => Err(Error::new(
124
+ runtime_error(),
125
+ "Future cancelled or worker failed",
126
+ )),
127
+ }
128
+ }
129
+
83
130
  /// Run asyncio.run(coroutine) and convert the result to a SendableValue
84
131
  /// (thread-safe). Runs on the worker thread with the GIL held.
85
132
  fn run_asyncio_sendable(
@@ -124,3 +171,476 @@ fn run_asyncio_sendable(
124
171
  api.decref(result);
125
172
  sendable
126
173
  }
174
+
175
+ #[cfg(test)]
176
+ mod tests {
177
+ use super::*;
178
+ use crate::stream::SendableValue;
179
+ use crate::test_helpers::with_ruby_python;
180
+ use crossbeam_channel::{bounded, unbounded};
181
+ use magnus::value::ReprValue;
182
+ use magnus::TryConvert;
183
+ use serial_test::serial;
184
+ use std::sync::atomic::Ordering;
185
+ use std::time::{Duration, Instant};
186
+
187
+ // ========== future_recv_cb (pure Rust, no Ruby needed) ==========
188
+
189
+ #[test]
190
+ fn test_future_recv_cb_delivers_ok_value() {
191
+ let (tx, rx) = unbounded();
192
+ tx.send(Ok(SendableValue::Integer(42))).unwrap();
193
+
194
+ let cancel = Arc::new(AtomicBool::new(false));
195
+ let mut args = FutureRecvArgs {
196
+ receiver: rx,
197
+ result: None,
198
+ cancel,
199
+ };
200
+
201
+ unsafe {
202
+ future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
203
+ }
204
+
205
+ match args.result.unwrap().unwrap() {
206
+ Ok(SendableValue::Integer(n)) => assert_eq!(n, 42),
207
+ other => panic!("expected Ok(Integer(42)), got {other:?}"),
208
+ }
209
+ }
210
+
211
+ #[test]
212
+ fn test_future_recv_cb_delivers_err_value() {
213
+ let (tx, rx) = unbounded();
214
+ tx.send(Err("python exploded".to_string())).unwrap();
215
+
216
+ let cancel = Arc::new(AtomicBool::new(false));
217
+ let mut args = FutureRecvArgs {
218
+ receiver: rx,
219
+ result: None,
220
+ cancel,
221
+ };
222
+
223
+ unsafe {
224
+ future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
225
+ }
226
+
227
+ match args.result.unwrap().unwrap() {
228
+ Err(msg) => assert_eq!(msg, "python exploded"),
229
+ other => panic!("expected Err, got {other:?}"),
230
+ }
231
+ }
232
+
233
+ #[test]
234
+ fn test_future_recv_cb_handles_disconnect() {
235
+ let (tx, rx) = bounded::<Result<SendableValue, String>>(1);
236
+ drop(tx);
237
+
238
+ let cancel = Arc::new(AtomicBool::new(false));
239
+ let mut args = FutureRecvArgs {
240
+ receiver: rx,
241
+ result: None,
242
+ cancel,
243
+ };
244
+
245
+ unsafe {
246
+ future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
247
+ }
248
+
249
+ assert!(args.result.unwrap().is_err());
250
+ }
251
+
252
+ #[test]
253
+ fn test_future_recv_cb_respects_cancel_flag() {
254
+ let (_tx, rx) = bounded::<Result<SendableValue, String>>(1);
255
+
256
+ let cancel = Arc::new(AtomicBool::new(false));
257
+ let cancel_clone = cancel.clone();
258
+
259
+ let mut args = FutureRecvArgs {
260
+ receiver: rx,
261
+ result: None,
262
+ cancel,
263
+ };
264
+
265
+ // Set cancel from another thread after a short delay
266
+ std::thread::spawn(move || {
267
+ std::thread::sleep(Duration::from_millis(30));
268
+ cancel_clone.store(true, Ordering::Relaxed);
269
+ });
270
+
271
+ let start = Instant::now();
272
+ unsafe {
273
+ future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
274
+ }
275
+ let elapsed = start.elapsed();
276
+
277
+ assert!(args.result.is_none(), "expected None on cancel");
278
+ assert!(
279
+ elapsed < Duration::from_millis(200),
280
+ "cancel took {elapsed:?}, expected < 200ms"
281
+ );
282
+ }
283
+
284
+ #[test]
285
+ fn test_future_recv_cb_cancel_flag_already_set() {
286
+ let (_tx, rx) = bounded::<Result<SendableValue, String>>(1);
287
+
288
+ let cancel = Arc::new(AtomicBool::new(true)); // pre-set
289
+ let mut args = FutureRecvArgs {
290
+ receiver: rx,
291
+ result: None,
292
+ cancel,
293
+ };
294
+
295
+ let start = Instant::now();
296
+ unsafe {
297
+ future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
298
+ }
299
+ let elapsed = start.elapsed();
300
+
301
+ assert!(args.result.is_none(), "expected None on pre-set cancel");
302
+ assert!(
303
+ elapsed < Duration::from_millis(10),
304
+ "pre-set cancel took {elapsed:?}, expected near-instant"
305
+ );
306
+ }
307
+
308
+ #[test]
309
+ fn test_future_recv_cb_with_delayed_producer() {
310
+ let (tx, rx) = bounded::<Result<SendableValue, String>>(1);
311
+
312
+ let cancel = Arc::new(AtomicBool::new(false));
313
+ let mut args = FutureRecvArgs {
314
+ receiver: rx,
315
+ result: None,
316
+ cancel,
317
+ };
318
+
319
+ // Send value after a delay (simulates slow Python computation)
320
+ std::thread::spawn(move || {
321
+ std::thread::sleep(Duration::from_millis(100));
322
+ tx.send(Ok(SendableValue::Str("delayed".to_string())))
323
+ .unwrap();
324
+ });
325
+
326
+ let start = Instant::now();
327
+ unsafe {
328
+ future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
329
+ }
330
+ let elapsed = start.elapsed();
331
+
332
+ match args.result.unwrap().unwrap() {
333
+ Ok(SendableValue::Str(s)) => assert_eq!(s, "delayed"),
334
+ other => panic!("expected Ok(Str), got {other:?}"),
335
+ }
336
+ assert!(
337
+ elapsed >= Duration::from_millis(80),
338
+ "should have waited for producer, elapsed: {elapsed:?}"
339
+ );
340
+ }
341
+
342
+ #[test]
343
+ fn test_future_recv_cb_all_sendable_types() {
344
+ let (tx, rx) = unbounded();
345
+ tx.send(Ok(SendableValue::Nil)).unwrap();
346
+ tx.send(Ok(SendableValue::Integer(99))).unwrap();
347
+ tx.send(Ok(SendableValue::Float(3.14))).unwrap();
348
+ tx.send(Ok(SendableValue::Str("hello".to_string())))
349
+ .unwrap();
350
+ tx.send(Ok(SendableValue::Bool(true))).unwrap();
351
+
352
+ let cancel = Arc::new(AtomicBool::new(false));
353
+
354
+ // Nil
355
+ let mut args = FutureRecvArgs {
356
+ receiver: rx.clone(),
357
+ result: None,
358
+ cancel: cancel.clone(),
359
+ };
360
+ unsafe {
361
+ future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
362
+ }
363
+ assert!(matches!(
364
+ args.result.unwrap().unwrap(),
365
+ Ok(SendableValue::Nil)
366
+ ));
367
+
368
+ // Integer
369
+ let mut args = FutureRecvArgs {
370
+ receiver: rx.clone(),
371
+ result: None,
372
+ cancel: cancel.clone(),
373
+ };
374
+ unsafe {
375
+ future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
376
+ }
377
+ match args.result.unwrap().unwrap() {
378
+ Ok(SendableValue::Integer(n)) => assert_eq!(n, 99),
379
+ other => panic!("expected Integer(99), got {other:?}"),
380
+ }
381
+
382
+ // Float
383
+ let mut args = FutureRecvArgs {
384
+ receiver: rx.clone(),
385
+ result: None,
386
+ cancel: cancel.clone(),
387
+ };
388
+ unsafe {
389
+ future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
390
+ }
391
+ match args.result.unwrap().unwrap() {
392
+ Ok(SendableValue::Float(f)) => assert!((f - 3.14).abs() < f64::EPSILON),
393
+ other => panic!("expected Float(3.14), got {other:?}"),
394
+ }
395
+
396
+ // Str
397
+ let mut args = FutureRecvArgs {
398
+ receiver: rx.clone(),
399
+ result: None,
400
+ cancel: cancel.clone(),
401
+ };
402
+ unsafe {
403
+ future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
404
+ }
405
+ match args.result.unwrap().unwrap() {
406
+ Ok(SendableValue::Str(s)) => assert_eq!(s, "hello"),
407
+ other => panic!("expected Str(\"hello\"), got {other:?}"),
408
+ }
409
+
410
+ // Bool
411
+ let mut args = FutureRecvArgs {
412
+ receiver: rx.clone(),
413
+ result: None,
414
+ cancel: cancel.clone(),
415
+ };
416
+ unsafe {
417
+ future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
418
+ }
419
+ match args.result.unwrap().unwrap() {
420
+ Ok(SendableValue::Bool(b)) => assert!(b),
421
+ other => panic!("expected Bool(true), got {other:?}"),
422
+ }
423
+ }
424
+
425
+ // ========== RubyxFuture::value (needs Ruby for SendableValue → Value) ==========
426
+
427
+ /// Helper: create a RubyxFuture backed by a channel (no Python needed)
428
+ fn make_future(rx: Receiver<Result<SendableValue, String>>) -> RubyxFuture {
429
+ RubyxFuture {
430
+ receiver: rx,
431
+ handle: RefCell::new(None),
432
+ }
433
+ }
434
+
435
+ /// Helper: create a RubyxFuture with a JoinHandle
436
+ fn make_future_with_handle(
437
+ rx: Receiver<Result<SendableValue, String>>,
438
+ handle: JoinHandle<()>,
439
+ ) -> RubyxFuture {
440
+ RubyxFuture {
441
+ receiver: rx,
442
+ handle: RefCell::new(Some(handle)),
443
+ }
444
+ }
445
+
446
+ /// Test helper: read from future without going through value()/value_nonblocking().
447
+ /// Avoids rb_thread_call_without_gvl which deadlocks in the embedded Ruby test harness.
448
+ fn test_recv(future: &RubyxFuture) -> Result<Value, Error> {
449
+ if let Some(handle) = future.handle.borrow_mut().take() {
450
+ let _ = handle.join();
451
+ }
452
+ match future.receiver.try_recv() {
453
+ Ok(Ok(sendable)) => sendable.try_into(),
454
+ Ok(Err(err)) => Err(Error::new(runtime_error(), err)),
455
+ Err(_) => Err(Error::new(
456
+ runtime_error(),
457
+ "Future already consumed or worker failed",
458
+ )),
459
+ }
460
+ }
461
+
462
+ #[test]
463
+ #[serial]
464
+ fn test_value_fast_path_already_ready() {
465
+ with_ruby_python(|_ruby, _api| {
466
+ let (tx, rx) = bounded(1);
467
+ tx.send(Ok(SendableValue::Integer(7))).unwrap();
468
+
469
+ let future = make_future(rx);
470
+ let val = test_recv(&future).unwrap();
471
+ assert_eq!(i64::try_convert(val).unwrap(), 7);
472
+ });
473
+ }
474
+
475
+ #[test]
476
+ #[serial]
477
+ fn test_value_fast_path_error() {
478
+ with_ruby_python(|_ruby, _api| {
479
+ let (tx, rx) = bounded(1);
480
+ tx.send(Err("boom".to_string())).unwrap();
481
+
482
+ let future = make_future(rx);
483
+ let err = test_recv(&future).unwrap_err();
484
+ assert!(err.to_string().contains("boom"));
485
+ });
486
+ }
487
+
488
+ #[test]
489
+ #[serial]
490
+ fn test_value_slow_path_waits_for_producer() {
491
+ with_ruby_python(|_ruby, _api| {
492
+ let (tx, rx) = bounded(1);
493
+
494
+ let handle = std::thread::spawn(move || {
495
+ std::thread::sleep(Duration::from_millis(100));
496
+ tx.send(Ok(SendableValue::Str("from worker".to_string())))
497
+ .unwrap();
498
+ });
499
+
500
+ let future = make_future_with_handle(rx, handle);
501
+
502
+ let start = Instant::now();
503
+ let val = test_recv(&future).unwrap();
504
+ let elapsed = start.elapsed();
505
+
506
+ let s: String = TryConvert::try_convert(val).unwrap();
507
+ assert_eq!(s, "from worker");
508
+ assert!(
509
+ elapsed >= Duration::from_millis(80),
510
+ "should have waited, elapsed: {elapsed:?}"
511
+ );
512
+ });
513
+ }
514
+
515
+ #[test]
516
+ #[serial]
517
+ fn test_value_returns_all_sendable_types() {
518
+ with_ruby_python(|_ruby, _api| {
519
+ // Integer
520
+ let (tx, rx) = bounded(1);
521
+ tx.send(Ok(SendableValue::Integer(42))).unwrap();
522
+ let val = test_recv(&make_future(rx)).unwrap();
523
+ assert_eq!(i64::try_convert(val).unwrap(), 42);
524
+
525
+ // Float
526
+ let (tx, rx) = bounded(1);
527
+ tx.send(Ok(SendableValue::Float(2.5))).unwrap();
528
+ let val = test_recv(&make_future(rx)).unwrap();
529
+ assert_eq!(f64::try_convert(val).unwrap(), 2.5);
530
+
531
+ // String
532
+ let (tx, rx) = bounded(1);
533
+ tx.send(Ok(SendableValue::Str("hello".to_string())))
534
+ .unwrap();
535
+ let val = test_recv(&make_future(rx)).unwrap();
536
+ let s: String = TryConvert::try_convert(val).unwrap();
537
+ assert_eq!(s, "hello");
538
+
539
+ // Bool
540
+ let (tx, rx) = bounded(1);
541
+ tx.send(Ok(SendableValue::Bool(true))).unwrap();
542
+ let val = test_recv(&make_future(rx)).unwrap();
543
+ assert_eq!(bool::try_convert(val).unwrap(), true);
544
+
545
+ // Nil
546
+ let (tx, rx) = bounded(1);
547
+ tx.send(Ok(SendableValue::Nil)).unwrap();
548
+ let val = test_recv(&make_future(rx)).unwrap();
549
+ assert!(val.is_nil());
550
+ });
551
+ }
552
+
553
+ #[test]
554
+ #[serial]
555
+ fn test_value_consumed_twice_returns_error() {
556
+ with_ruby_python(|_ruby, _api| {
557
+ let (tx, rx) = bounded(1);
558
+ tx.send(Ok(SendableValue::Integer(1))).unwrap();
559
+
560
+ let future = make_future(rx);
561
+ let _ = test_recv(&future).unwrap();
562
+ let err = test_recv(&future).unwrap_err();
563
+ assert!(err.to_string().contains("consumed") || err.to_string().contains("failed"));
564
+ });
565
+ }
566
+
567
+ #[test]
568
+ fn test_is_ready_before_and_after_send() {
569
+ let (tx, rx) = bounded(1);
570
+ let future = make_future(rx);
571
+
572
+ assert!(!future.is_ready());
573
+ tx.send(Ok(SendableValue::Integer(1))).unwrap();
574
+ assert!(future.is_ready());
575
+ }
576
+
577
+ #[test]
578
+ fn test_drop_joins_handle() {
579
+ let (tx, rx) = bounded(1);
580
+ let flag = Arc::new(AtomicBool::new(false));
581
+ let flag_clone = flag.clone();
582
+
583
+ let handle = std::thread::spawn(move || {
584
+ std::thread::sleep(Duration::from_millis(50));
585
+ flag_clone.store(true, Ordering::Relaxed);
586
+ let _ = tx.send(Ok(SendableValue::Nil));
587
+ });
588
+
589
+ let future = make_future_with_handle(rx, handle);
590
+ drop(future); // should join the handle
591
+
592
+ assert!(
593
+ flag.load(Ordering::Relaxed),
594
+ "worker should have completed before drop returned"
595
+ );
596
+ }
597
+
598
+ // ========== PyObjectRef: future.value() returns RubyxObject ==========
599
+
600
+ #[test]
601
+ #[serial]
602
+ fn test_value_returns_rubyx_object_for_py_object_ref() {
603
+ with_ruby_python(|_ruby, api| {
604
+ let os = api.import_module("os").expect("os should import");
605
+ api.incref(os); // incref for PyObjectRef
606
+
607
+ let (tx, rx) = bounded(1);
608
+ tx.send(Ok(SendableValue::PyObjectRef(os as usize)))
609
+ .unwrap();
610
+
611
+ let future = make_future(rx);
612
+ let val = test_recv(&future).unwrap();
613
+
614
+ // Should be a RubyxObject, not a primitive
615
+ assert!(!val.is_nil());
616
+ assert!(i64::try_convert(val).is_err(), "should not be Integer");
617
+ assert!(String::try_convert(val).is_err(), "should not be String");
618
+
619
+ api.decref(os);
620
+ });
621
+ }
622
+
623
+ #[test]
624
+ fn test_future_recv_cb_delivers_py_object_ref() {
625
+ let (tx, rx) = unbounded();
626
+ // Use a fake address — we're just testing the callback delivers it through
627
+ let fake_addr = 0xDEADBEEFusize;
628
+ tx.send(Ok(SendableValue::PyObjectRef(fake_addr))).unwrap();
629
+
630
+ let cancel = Arc::new(AtomicBool::new(false));
631
+ let mut args = FutureRecvArgs {
632
+ receiver: rx,
633
+ result: None,
634
+ cancel,
635
+ };
636
+
637
+ unsafe {
638
+ future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
639
+ }
640
+
641
+ match args.result.unwrap().unwrap() {
642
+ Ok(SendableValue::PyObjectRef(addr)) => assert_eq!(addr, fake_addr),
643
+ other => panic!("expected PyObjectRef(0xDEADBEEF), got {other:?}"),
644
+ }
645
+ }
646
+ }
@@ -0,0 +1,70 @@
1
+ use crossbeam_channel::Receiver;
2
+ use std::ffi::c_void;
3
+ use std::sync::atomic::AtomicBool;
4
+
5
+ extern "C" {
6
+ /// Release the GVL, call `func(data1)`, then reacquire the GVL.
7
+ ///
8
+ /// While `func` runs, other Ruby threads can execute. Inside `func`,
9
+ /// you must NOT call any Ruby C API or access any Ruby VALUE.
10
+ ///
11
+ /// If `ubf` is provided, Ruby may call it from another thread to
12
+ /// interrupt `func` (e.g., on Thread#kill or signal delivery).
13
+ ///
14
+ /// # Safety
15
+ ///
16
+ /// - `func` must not touch Ruby objects (GVL is not held).
17
+ /// - `data1` must remain valid for the duration of `func`.
18
+ /// - `data2` must remain valid for the duration of `ubf` (if provided).
19
+ pub(crate) fn rb_thread_call_without_gvl(
20
+ func: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
21
+ data1: *mut c_void,
22
+ ubf: Option<unsafe extern "C" fn(*mut c_void)>,
23
+ data2: *mut c_void,
24
+ ) -> *mut c_void;
25
+
26
+ /// Check for pending Ruby interrupts (Thread#kill, signals, etc.).
27
+ ///
28
+ /// Must be called WITH the GVL held. If an interrupt is pending,
29
+ /// this raises a Ruby exception (longjmp). Call this immediately
30
+ /// after `rb_thread_call_without_gvl` returns to deliver any
31
+ /// interrupts that arrived while the GVL was released.
32
+ pub(crate) fn rb_thread_check_ints();
33
+
34
+ }
35
+
36
+ pub(crate) unsafe extern "C" fn ubf_cancel(data: *mut c_void) {
37
+ let cancel = &*(data as *const AtomicBool);
38
+ cancel.store(true, std::sync::atomic::Ordering::Relaxed);
39
+ }
40
+
41
+ /// Generic receive loop for use inside `rb_thread_call_without_gvl` callbacks.
42
+ ///
43
+ /// Polls the channel with a 50ms timeout between cancel-flag checks.
44
+ /// Returns `None` if cancelled, `Some(Ok(item))` on success, or
45
+ /// `Some(Err(_))` if the sender disconnected.
46
+ pub(crate) fn recv_loop<T>(
47
+ receiver: &Receiver<T>,
48
+ cancel: &AtomicBool,
49
+ ) -> Option<Result<T, crossbeam_channel::RecvError>> {
50
+ loop {
51
+ match receiver.try_recv() {
52
+ Ok(item) => return Some(Ok(item)),
53
+ Err(crossbeam_channel::TryRecvError::Empty) => {
54
+ if cancel.load(std::sync::atomic::Ordering::Relaxed) {
55
+ return None;
56
+ }
57
+ match receiver.recv_timeout(std::time::Duration::from_millis(50)) {
58
+ Ok(item) => return Some(Ok(item)),
59
+ Err(crossbeam_channel::RecvTimeoutError::Timeout) => continue,
60
+ Err(crossbeam_channel::RecvTimeoutError::Disconnected) => {
61
+ return Some(Err(crossbeam_channel::RecvError));
62
+ }
63
+ }
64
+ }
65
+ Err(crossbeam_channel::TryRecvError::Disconnected) => {
66
+ return Some(Err(crossbeam_channel::RecvError));
67
+ }
68
+ }
69
+ }
70
+ }