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.
- checksums.yaml +4 -4
- data/README.md +97 -28
- data/docs/assets/logo.png +0 -0
- data/ext/rubyx/src/context.rs +45 -14
- data/ext/rubyx/src/eval.rs +14 -60
- data/ext/rubyx/src/future.rs +534 -14
- data/ext/rubyx/src/gvl.rs +70 -0
- data/ext/rubyx/src/lib.rs +83 -32
- data/ext/rubyx/src/nonblocking_stream.rs +5 -78
- data/ext/rubyx/src/python_api.rs +1027 -0
- data/ext/rubyx/src/rubyx_object.rs +1159 -4
- data/ext/rubyx/src/rubyx_stream.rs +168 -0
- data/ext/rubyx/src/stream.rs +225 -3
- data/lib/rubyx/railtie.rb +2 -1
- data/lib/rubyx/tasks/rubyx.rake +127 -0
- data/lib/rubyx/version.rb +2 -2
- metadata +4 -1
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
|
-
|
|
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
|
|
2974
|
+
fn test_to_ruby_wraps_module_via_pyobjectref() {
|
|
2972
2975
|
with_ruby_python(|_ruby, api| {
|
|
2973
|
-
//
|
|
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!(
|
|
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
|
|
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
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
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
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
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
|
|
3248
|
+
let future = crate::eval::await_eval_with_globals("double(21)", globals, api)
|
|
3227
3249
|
.expect("await_eval should succeed");
|
|
3228
3250
|
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
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
|
|
3251
|
-
|
|
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
|
|
3468
|
+
let future = crate::eval::await_eval_with_globals("multiply(a, b)", globals, api)
|
|
3434
3469
|
.expect("await should succeed");
|
|
3435
|
-
|
|
3436
|
-
|
|
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
|
|
3503
|
+
let future = crate::eval::await_eval_with_globals("greet(who)", globals, api)
|
|
3464
3504
|
.expect("await should succeed");
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
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
|
|
3497
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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>>,
|