rubyx-py 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/ext/rubyx/src/lib.rs CHANGED
@@ -18,6 +18,7 @@ mod convert;
18
18
  mod eval;
19
19
  mod exception;
20
20
  mod future;
21
+ mod gvl;
21
22
  mod import;
22
23
  mod nonblocking_stream;
23
24
  mod pipe_notify;
@@ -94,6 +95,7 @@ fn init(ruby: &magnus::Ruby) -> Result<(), magnus::Error> {
94
95
  py_object.define_method("truthy?", method!(RubyxObject::is_truthy, 0))?;
95
96
  py_object.define_method("falsy?", method!(RubyxObject::is_falsy, 0))?;
96
97
  py_object.define_method("callable?", method!(RubyxObject::is_callable, 0))?;
98
+ py_object.define_method("call", method!(RubyxObject::call, -1))?;
97
99
  py_object.define_method("py_type", method!(RubyxObject::py_type, 0))?;
98
100
  py_object.define_method("each", method!(RubyxObject::each, 0))?;
99
101
  py_object.include_module(ruby.module_enumerable())?;
@@ -155,7 +157,8 @@ fn init(ruby: &magnus::Ruby) -> Result<(), magnus::Error> {
155
157
 
156
158
  // Rubyx::Future class
157
159
  let future_class = rubyx_module.define_class("Future", ruby.class_object())?;
158
- future_class.define_method("value", method!(crate::future::RubyxFuture::value, 0))?;
160
+ // value() instead of await since await is a reserved keyword
161
+ future_class.define_method("await", method!(crate::future::RubyxFuture::value, 0))?;
159
162
  future_class.define_method("ready?", method!(crate::future::RubyxFuture::is_ready, 0))?;
160
163
 
161
164
  Ok(())
@@ -2968,13 +2971,17 @@ mod tests {
2968
2971
 
2969
2972
  #[test]
2970
2973
  #[serial]
2971
- fn test_to_ruby_unconvertible_returns_error() {
2974
+ fn test_to_ruby_wraps_module_via_pyobjectref() {
2972
2975
  with_ruby_python(|_ruby, api| {
2973
- // Python module objects can't be converted to Ruby primitives
2976
+ // Modules have __dict__ and are not callable, so python_to_sendable
2977
+ // returns PyObjectRef → wrapped as RubyxObject.
2974
2978
  let module = api.import_module("sys").expect("sys should import");
2975
2979
  let wrapper = RubyxObject::new(module, api).unwrap();
2976
2980
  let result = wrapper.to_ruby();
2977
- assert!(result.is_err(), "module should not be convertible to Ruby");
2981
+ assert!(
2982
+ result.is_ok(),
2983
+ "module should wrap as RubyxObject via PyObjectRef"
2984
+ );
2978
2985
 
2979
2986
  drop(wrapper);
2980
2987
  api.decref(module);
@@ -3008,6 +3015,17 @@ mod tests {
3008
3015
 
3009
3016
  // ========== Rubyx::Future / async_await tests ==========
3010
3017
 
3018
+ /// Spin-wait then call value() — ensures the fast path is hit in tests,
3019
+ /// avoiding rb_thread_call_without_gvl which deadlocks in embedded Ruby.
3020
+ fn test_future_value(
3021
+ future: &crate::future::RubyxFuture,
3022
+ ) -> Result<magnus::Value, magnus::Error> {
3023
+ while !future.is_ready() {
3024
+ std::thread::sleep(std::time::Duration::from_millis(1));
3025
+ }
3026
+ future.value()
3027
+ }
3028
+
3011
3029
  /// Helper: define an async function in globals, create coroutine,
3012
3030
  /// release GIL, run future, restore GIL, return result.
3013
3031
  fn run_future_test(
@@ -3025,7 +3043,7 @@ mod tests {
3025
3043
 
3026
3044
  let tstate = api.save_thread();
3027
3045
  let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
3028
- let result = future.value();
3046
+ let result = test_future_value(&future);
3029
3047
  drop(future);
3030
3048
  api.restore_thread(tstate);
3031
3049
 
@@ -3197,14 +3215,18 @@ mod tests {
3197
3215
  .run_string("get_val()", PY_EVAL_INPUT, globals, globals)
3198
3216
  .expect("should create coroutine");
3199
3217
 
3200
- let wrapper = RubyxObject::new(coroutine, api).expect("should wrap coroutine");
3201
- let coro_value: magnus::Value = magnus::IntoValue::into_value_with(wrapper, ruby);
3202
- let result =
3203
- crate::eval::rubyx_await(coro_value).expect("blocking await should succeed");
3218
+ // Manually do what rubyx_await does, but with proper GIL management
3219
+ // for the test environment (can't call rubyx_await directly because
3220
+ // its ensure_gil/release_gil nests incorrectly with with_ruby_python's GIL)
3221
+ let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
3204
3222
 
3205
- let obj = magnus::typed_data::Obj::<RubyxObject>::try_convert(result)
3206
- .expect("should be RubyxObject");
3207
- assert_eq!(api.long_to_i64(obj.as_ptr()), 99);
3223
+ // Release GIL so the background thread can acquire it
3224
+ let tstate = api.save_thread();
3225
+ let result = test_future_value(&future).expect("await should succeed");
3226
+ drop(future);
3227
+ api.restore_thread(tstate);
3228
+
3229
+ assert_eq!(i64::try_convert(result).unwrap(), 99);
3208
3230
 
3209
3231
  api.decref(globals);
3210
3232
  });
@@ -3223,12 +3245,16 @@ mod tests {
3223
3245
  )
3224
3246
  .expect("should define async function");
3225
3247
 
3226
- let result = crate::eval::await_eval_with_globals("double(21)", globals, api)
3248
+ let future = crate::eval::await_eval_with_globals("double(21)", globals, api)
3227
3249
  .expect("await_eval should succeed");
3228
3250
 
3229
- let obj = magnus::typed_data::Obj::<RubyxObject>::try_convert(result)
3230
- .expect("should be RubyxObject");
3231
- assert_eq!(api.long_to_i64(obj.as_ptr()), 42);
3251
+ // Release GIL so background thread can run asyncio
3252
+ let tstate = api.save_thread();
3253
+ let result = test_future_value(&future).expect("future should resolve");
3254
+ drop(future);
3255
+ api.restore_thread(tstate);
3256
+
3257
+ assert_eq!(i64::try_convert(result).unwrap(), 42);
3232
3258
 
3233
3259
  api.decref(globals);
3234
3260
  });
@@ -3247,8 +3273,17 @@ mod tests {
3247
3273
  )
3248
3274
  .expect("should define async function");
3249
3275
 
3250
- let result = crate::eval::await_eval_with_globals("fail()", globals, api);
3251
- assert!(result.is_err(), "should propagate error");
3276
+ let future_result = crate::eval::await_eval_with_globals("fail()", globals, api);
3277
+ match future_result {
3278
+ Err(_) => {} // eval itself failed — error propagated
3279
+ Ok(future) => {
3280
+ // Eval succeeded but asyncio.run should fail
3281
+ let tstate = api.save_thread();
3282
+ let result = test_future_value(&future);
3283
+ api.restore_thread(tstate);
3284
+ assert!(result.is_err(), "should propagate error");
3285
+ }
3286
+ }
3252
3287
 
3253
3288
  api.decref(globals);
3254
3289
  });
@@ -3430,10 +3465,15 @@ mod tests {
3430
3465
  api.decref(py_a);
3431
3466
  api.decref(py_b);
3432
3467
 
3433
- let result = crate::eval::await_eval_with_globals("multiply(a, b)", globals, api)
3468
+ let future = crate::eval::await_eval_with_globals("multiply(a, b)", globals, api)
3434
3469
  .expect("await should succeed");
3435
- let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
3436
- assert_eq!(api.long_to_i64(obj.as_ptr()), 42);
3470
+
3471
+ let tstate = api.save_thread();
3472
+ let result = test_future_value(&future).expect("future should resolve");
3473
+ drop(future);
3474
+ api.restore_thread(tstate);
3475
+
3476
+ assert_eq!(i64::try_convert(result).unwrap(), 42);
3437
3477
 
3438
3478
  api.decref(globals);
3439
3479
  });
@@ -3460,13 +3500,16 @@ mod tests {
3460
3500
  api.decref(key);
3461
3501
  api.decref(py_who);
3462
3502
 
3463
- let result = crate::eval::await_eval_with_globals("greet(who)", globals, api)
3503
+ let future = crate::eval::await_eval_with_globals("greet(who)", globals, api)
3464
3504
  .expect("await should succeed");
3465
- let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
3466
- assert_eq!(
3467
- api.string_to_string(obj.as_ptr()),
3468
- Some("hi world".to_string())
3469
- );
3505
+
3506
+ let tstate = api.save_thread();
3507
+ let result = test_future_value(&future).expect("future should resolve");
3508
+ drop(future);
3509
+ api.restore_thread(tstate);
3510
+
3511
+ let s: String = TryConvert::try_convert(result).unwrap();
3512
+ assert_eq!(s, "hi world");
3470
3513
 
3471
3514
  api.decref(globals);
3472
3515
  });
@@ -3493,8 +3536,16 @@ mod tests {
3493
3536
  api.decref(key);
3494
3537
  api.decref(py_val);
3495
3538
 
3496
- let result = crate::eval::await_eval_with_globals("check(val)", globals, api);
3497
- assert!(result.is_err(), "should propagate ValueError");
3539
+ let future_result = crate::eval::await_eval_with_globals("check(val)", globals, api);
3540
+ match future_result {
3541
+ Err(_) => {} // eval itself failed
3542
+ Ok(future) => {
3543
+ let tstate = api.save_thread();
3544
+ let result = test_future_value(&future);
3545
+ api.restore_thread(tstate);
3546
+ assert!(result.is_err(), "should propagate ValueError");
3547
+ }
3548
+ }
3498
3549
 
3499
3550
  api.decref(globals);
3500
3551
  });
@@ -3537,7 +3588,7 @@ mod tests {
3537
3588
  // Release GIL so the background thread can acquire it
3538
3589
  let tstate = api.save_thread();
3539
3590
  let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
3540
- let result = future.value().expect("future should resolve");
3591
+ let result = test_future_value(&future).expect("future should resolve");
3541
3592
  drop(future);
3542
3593
  api.restore_thread(tstate);
3543
3594
 
@@ -3582,7 +3633,7 @@ mod tests {
3582
3633
 
3583
3634
  let tstate = api.save_thread();
3584
3635
  let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
3585
- let result = future.value().expect("future should resolve");
3636
+ let result = test_future_value(&future).expect("future should resolve");
3586
3637
  drop(future);
3587
3638
  api.restore_thread(tstate);
3588
3639
 
@@ -3626,7 +3677,7 @@ mod tests {
3626
3677
 
3627
3678
  let tstate = api.save_thread();
3628
3679
  let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
3629
- let result = future.value();
3680
+ let result = test_future_value(&future);
3630
3681
  drop(future);
3631
3682
  api.restore_thread(tstate);
3632
3683
 
@@ -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::pipe_notify::PipeNotify;
2
3
  use crate::ruby_helpers::runtime_error;
3
4
  use crate::stream::StreamItem;
@@ -9,37 +10,6 @@ use std::os::fd::RawFd;
9
10
  use std::sync::atomic::AtomicBool;
10
11
  use std::sync::Arc;
11
12
 
12
- extern "C" {
13
- /// Release the GVL, call `func(data1)`, then reacquire the GVL.
14
- ///
15
- /// While `func` runs, other Ruby threads can execute. Inside `func`,
16
- /// you must NOT call any Ruby C API or access any Ruby VALUE.
17
- ///
18
- /// If `ubf` is provided, Ruby may call it from another thread to
19
- /// interrupt `func` (e.g., on Thread#kill or signal delivery).
20
- ///
21
- /// # Safety
22
- ///
23
- /// - `func` must not touch Ruby objects (GVL is not held).
24
- /// - `data1` must remain valid for the duration of `func`.
25
- /// - `data2` must remain valid for the duration of `ubf` (if provided).
26
- fn rb_thread_call_without_gvl(
27
- func: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
28
- data1: *mut c_void,
29
- ubf: Option<unsafe extern "C" fn(*mut c_void)>,
30
- data2: *mut c_void,
31
- ) -> *mut c_void;
32
-
33
- /// Check for pending Ruby interrupts (Thread#kill, signals, etc.).
34
- ///
35
- /// Must be called WITH the GVL held. If an interrupt is pending,
36
- /// this raises a Ruby exception (longjmp). Call this immediately
37
- /// after `rb_thread_call_without_gvl` returns to deliver any
38
- /// interrupts that arrived while the GVL was released.
39
- fn rb_thread_check_ints();
40
-
41
- }
42
-
43
13
  fn create_ruby_io(ruby: &Ruby, fd: RawFd) -> Result<Value, Error> {
44
14
  let io_class: Value = ruby.class_object().const_get("IO")?;
45
15
  io_class.funcall("for_fd", (fd, magnus::kwargs!("autoclose" => false)))
@@ -55,54 +25,11 @@ fn has_fiber_scheduler(ruby: &Ruby) -> bool {
55
25
  .unwrap_or_else(|_| ruby.qnil().as_value());
56
26
  !scheduler.is_nil()
57
27
  }
58
-
59
- unsafe extern "C" fn ubf_cancel(data: *mut c_void) {
60
- let cancel = &*(data as *const AtomicBool);
61
- cancel.store(true, std::sync::atomic::Ordering::Relaxed);
62
- }
63
-
64
- unsafe extern "C" fn recv_without_gvl_cb(args: *mut c_void) -> *mut c_void {
65
- let args = &mut *(args as *mut RecvArgs);
66
- loop {
67
- match args.receiver.try_recv() {
68
- Ok(item) => {
69
- args.result = Some(Ok(item));
70
- return std::ptr::null_mut();
71
- }
72
- Err(crossbeam_channel::TryRecvError::Empty) => {
73
- // Check the cancel flag relaxed already
74
- if args.cancel.load(std::sync::atomic::Ordering::Relaxed) {
75
- args.result = None;
76
- return std::ptr::null_mut();
77
- }
78
-
79
- // Wait for timeout
80
- match args
81
- .receiver
82
- .recv_timeout(std::time::Duration::from_millis(50))
83
- {
84
- Ok(item) => {
85
- args.result = Some(Ok(item));
86
- return std::ptr::null_mut();
87
- }
88
- Err(crossbeam_channel::RecvTimeoutError::Timeout) => {
89
- // continue to check cancel
90
- continue;
91
- }
92
- Err(crossbeam_channel::RecvTimeoutError::Disconnected) => {
93
- args.result = Some(Err(crossbeam_channel::RecvError));
94
- return std::ptr::null_mut();
95
- }
96
- }
97
- }
98
- Err(crossbeam_channel::TryRecvError::Disconnected) => {
99
- args.result = Some(Err(crossbeam_channel::RecvError));
100
- return std::ptr::null_mut();
101
- }
102
- }
103
- }
28
+ pub(crate) unsafe extern "C" fn recv_without_gvl_cb(args: *mut c_void) -> *mut c_void {
29
+ let args = &mut *(args as *mut crate::nonblocking_stream::RecvArgs);
30
+ args.result = recv_loop(&args.receiver, &args.cancel);
31
+ std::ptr::null_mut()
104
32
  }
105
-
106
33
  struct RecvArgs {
107
34
  receiver: Receiver<StreamItem>,
108
35
  result: Option<Result<StreamItem, crossbeam_channel::RecvError>>,