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.
- checksums.yaml +4 -4
- data/README.md +38 -24
- 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 +82 -32
- data/ext/rubyx/src/nonblocking_stream.rs +5 -78
- data/ext/rubyx/src/rubyx_object.rs +91 -3
- data/ext/rubyx/src/stream.rs +77 -1
- data/lib/generators/rubyx/templates/example.py +5 -0
- data/lib/generators/rubyx/templates/pyproject.toml +5 -0
- data/lib/rubyx/version.rb +1 -1
- metadata +5 -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;
|
|
@@ -155,7 +156,8 @@ fn init(ruby: &magnus::Ruby) -> Result<(), magnus::Error> {
|
|
|
155
156
|
|
|
156
157
|
// Rubyx::Future class
|
|
157
158
|
let future_class = rubyx_module.define_class("Future", ruby.class_object())?;
|
|
158
|
-
|
|
159
|
+
// value() instead of await since await is a reserved keyword
|
|
160
|
+
future_class.define_method("await", method!(crate::future::RubyxFuture::value, 0))?;
|
|
159
161
|
future_class.define_method("ready?", method!(crate::future::RubyxFuture::is_ready, 0))?;
|
|
160
162
|
|
|
161
163
|
Ok(())
|
|
@@ -2968,13 +2970,17 @@ mod tests {
|
|
|
2968
2970
|
|
|
2969
2971
|
#[test]
|
|
2970
2972
|
#[serial]
|
|
2971
|
-
fn
|
|
2973
|
+
fn test_to_ruby_wraps_module_via_pyobjectref() {
|
|
2972
2974
|
with_ruby_python(|_ruby, api| {
|
|
2973
|
-
//
|
|
2975
|
+
// Modules have __dict__ and are not callable, so python_to_sendable
|
|
2976
|
+
// returns PyObjectRef → wrapped as RubyxObject.
|
|
2974
2977
|
let module = api.import_module("sys").expect("sys should import");
|
|
2975
2978
|
let wrapper = RubyxObject::new(module, api).unwrap();
|
|
2976
2979
|
let result = wrapper.to_ruby();
|
|
2977
|
-
assert!(
|
|
2980
|
+
assert!(
|
|
2981
|
+
result.is_ok(),
|
|
2982
|
+
"module should wrap as RubyxObject via PyObjectRef"
|
|
2983
|
+
);
|
|
2978
2984
|
|
|
2979
2985
|
drop(wrapper);
|
|
2980
2986
|
api.decref(module);
|
|
@@ -3008,6 +3014,17 @@ mod tests {
|
|
|
3008
3014
|
|
|
3009
3015
|
// ========== Rubyx::Future / async_await tests ==========
|
|
3010
3016
|
|
|
3017
|
+
/// Spin-wait then call value() — ensures the fast path is hit in tests,
|
|
3018
|
+
/// avoiding rb_thread_call_without_gvl which deadlocks in embedded Ruby.
|
|
3019
|
+
fn test_future_value(
|
|
3020
|
+
future: &crate::future::RubyxFuture,
|
|
3021
|
+
) -> Result<magnus::Value, magnus::Error> {
|
|
3022
|
+
while !future.is_ready() {
|
|
3023
|
+
std::thread::sleep(std::time::Duration::from_millis(1));
|
|
3024
|
+
}
|
|
3025
|
+
future.value()
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3011
3028
|
/// Helper: define an async function in globals, create coroutine,
|
|
3012
3029
|
/// release GIL, run future, restore GIL, return result.
|
|
3013
3030
|
fn run_future_test(
|
|
@@ -3025,7 +3042,7 @@ mod tests {
|
|
|
3025
3042
|
|
|
3026
3043
|
let tstate = api.save_thread();
|
|
3027
3044
|
let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
|
|
3028
|
-
let result = future
|
|
3045
|
+
let result = test_future_value(&future);
|
|
3029
3046
|
drop(future);
|
|
3030
3047
|
api.restore_thread(tstate);
|
|
3031
3048
|
|
|
@@ -3197,14 +3214,18 @@ mod tests {
|
|
|
3197
3214
|
.run_string("get_val()", PY_EVAL_INPUT, globals, globals)
|
|
3198
3215
|
.expect("should create coroutine");
|
|
3199
3216
|
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3217
|
+
// Manually do what rubyx_await does, but with proper GIL management
|
|
3218
|
+
// for the test environment (can't call rubyx_await directly because
|
|
3219
|
+
// its ensure_gil/release_gil nests incorrectly with with_ruby_python's GIL)
|
|
3220
|
+
let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
|
|
3204
3221
|
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3222
|
+
// Release GIL so the background thread can acquire it
|
|
3223
|
+
let tstate = api.save_thread();
|
|
3224
|
+
let result = test_future_value(&future).expect("await should succeed");
|
|
3225
|
+
drop(future);
|
|
3226
|
+
api.restore_thread(tstate);
|
|
3227
|
+
|
|
3228
|
+
assert_eq!(i64::try_convert(result).unwrap(), 99);
|
|
3208
3229
|
|
|
3209
3230
|
api.decref(globals);
|
|
3210
3231
|
});
|
|
@@ -3223,12 +3244,16 @@ mod tests {
|
|
|
3223
3244
|
)
|
|
3224
3245
|
.expect("should define async function");
|
|
3225
3246
|
|
|
3226
|
-
let
|
|
3247
|
+
let future = crate::eval::await_eval_with_globals("double(21)", globals, api)
|
|
3227
3248
|
.expect("await_eval should succeed");
|
|
3228
3249
|
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3250
|
+
// Release GIL so background thread can run asyncio
|
|
3251
|
+
let tstate = api.save_thread();
|
|
3252
|
+
let result = test_future_value(&future).expect("future should resolve");
|
|
3253
|
+
drop(future);
|
|
3254
|
+
api.restore_thread(tstate);
|
|
3255
|
+
|
|
3256
|
+
assert_eq!(i64::try_convert(result).unwrap(), 42);
|
|
3232
3257
|
|
|
3233
3258
|
api.decref(globals);
|
|
3234
3259
|
});
|
|
@@ -3247,8 +3272,17 @@ mod tests {
|
|
|
3247
3272
|
)
|
|
3248
3273
|
.expect("should define async function");
|
|
3249
3274
|
|
|
3250
|
-
let
|
|
3251
|
-
|
|
3275
|
+
let future_result = crate::eval::await_eval_with_globals("fail()", globals, api);
|
|
3276
|
+
match future_result {
|
|
3277
|
+
Err(_) => {} // eval itself failed — error propagated
|
|
3278
|
+
Ok(future) => {
|
|
3279
|
+
// Eval succeeded but asyncio.run should fail
|
|
3280
|
+
let tstate = api.save_thread();
|
|
3281
|
+
let result = test_future_value(&future);
|
|
3282
|
+
api.restore_thread(tstate);
|
|
3283
|
+
assert!(result.is_err(), "should propagate error");
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3252
3286
|
|
|
3253
3287
|
api.decref(globals);
|
|
3254
3288
|
});
|
|
@@ -3430,10 +3464,15 @@ mod tests {
|
|
|
3430
3464
|
api.decref(py_a);
|
|
3431
3465
|
api.decref(py_b);
|
|
3432
3466
|
|
|
3433
|
-
let
|
|
3467
|
+
let future = crate::eval::await_eval_with_globals("multiply(a, b)", globals, api)
|
|
3434
3468
|
.expect("await should succeed");
|
|
3435
|
-
|
|
3436
|
-
|
|
3469
|
+
|
|
3470
|
+
let tstate = api.save_thread();
|
|
3471
|
+
let result = test_future_value(&future).expect("future should resolve");
|
|
3472
|
+
drop(future);
|
|
3473
|
+
api.restore_thread(tstate);
|
|
3474
|
+
|
|
3475
|
+
assert_eq!(i64::try_convert(result).unwrap(), 42);
|
|
3437
3476
|
|
|
3438
3477
|
api.decref(globals);
|
|
3439
3478
|
});
|
|
@@ -3460,13 +3499,16 @@ mod tests {
|
|
|
3460
3499
|
api.decref(key);
|
|
3461
3500
|
api.decref(py_who);
|
|
3462
3501
|
|
|
3463
|
-
let
|
|
3502
|
+
let future = crate::eval::await_eval_with_globals("greet(who)", globals, api)
|
|
3464
3503
|
.expect("await should succeed");
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
);
|
|
3504
|
+
|
|
3505
|
+
let tstate = api.save_thread();
|
|
3506
|
+
let result = test_future_value(&future).expect("future should resolve");
|
|
3507
|
+
drop(future);
|
|
3508
|
+
api.restore_thread(tstate);
|
|
3509
|
+
|
|
3510
|
+
let s: String = TryConvert::try_convert(result).unwrap();
|
|
3511
|
+
assert_eq!(s, "hi world");
|
|
3470
3512
|
|
|
3471
3513
|
api.decref(globals);
|
|
3472
3514
|
});
|
|
@@ -3493,8 +3535,16 @@ mod tests {
|
|
|
3493
3535
|
api.decref(key);
|
|
3494
3536
|
api.decref(py_val);
|
|
3495
3537
|
|
|
3496
|
-
let
|
|
3497
|
-
|
|
3538
|
+
let future_result = crate::eval::await_eval_with_globals("check(val)", globals, api);
|
|
3539
|
+
match future_result {
|
|
3540
|
+
Err(_) => {} // eval itself failed
|
|
3541
|
+
Ok(future) => {
|
|
3542
|
+
let tstate = api.save_thread();
|
|
3543
|
+
let result = test_future_value(&future);
|
|
3544
|
+
api.restore_thread(tstate);
|
|
3545
|
+
assert!(result.is_err(), "should propagate ValueError");
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3498
3548
|
|
|
3499
3549
|
api.decref(globals);
|
|
3500
3550
|
});
|
|
@@ -3537,7 +3587,7 @@ mod tests {
|
|
|
3537
3587
|
// Release GIL so the background thread can acquire it
|
|
3538
3588
|
let tstate = api.save_thread();
|
|
3539
3589
|
let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
|
|
3540
|
-
let result = future
|
|
3590
|
+
let result = test_future_value(&future).expect("future should resolve");
|
|
3541
3591
|
drop(future);
|
|
3542
3592
|
api.restore_thread(tstate);
|
|
3543
3593
|
|
|
@@ -3582,7 +3632,7 @@ mod tests {
|
|
|
3582
3632
|
|
|
3583
3633
|
let tstate = api.save_thread();
|
|
3584
3634
|
let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
|
|
3585
|
-
let result = future
|
|
3635
|
+
let result = test_future_value(&future).expect("future should resolve");
|
|
3586
3636
|
drop(future);
|
|
3587
3637
|
api.restore_thread(tstate);
|
|
3588
3638
|
|
|
@@ -3626,7 +3676,7 @@ mod tests {
|
|
|
3626
3676
|
|
|
3627
3677
|
let tstate = api.save_thread();
|
|
3628
3678
|
let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
|
|
3629
|
-
let result = future
|
|
3679
|
+
let result = test_future_value(&future);
|
|
3630
3680
|
drop(future);
|
|
3631
3681
|
api.restore_thread(tstate);
|
|
3632
3682
|
|
|
@@ -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>>,
|
|
@@ -91,6 +91,14 @@ pub(crate) fn python_to_sendable(
|
|
|
91
91
|
if py_val == api.py_false {
|
|
92
92
|
return Ok(SendableValue::Bool(false));
|
|
93
93
|
}
|
|
94
|
+
let has_dict = {
|
|
95
|
+
let name = std::ffi::CString::new("__dict__").unwrap();
|
|
96
|
+
api.object_has_attr_string(py_val, name.as_ptr()) != 0
|
|
97
|
+
};
|
|
98
|
+
if api.callable_check(py_val) == 0 && has_dict {
|
|
99
|
+
api.incref(py_val);
|
|
100
|
+
return Ok(SendableValue::PyObjectRef(py_val as usize));
|
|
101
|
+
}
|
|
94
102
|
Err("Cannot convert Python value to Ruby".to_string())
|
|
95
103
|
}
|
|
96
104
|
pub(crate) fn ruby_to_python(
|
|
@@ -1183,13 +1191,13 @@ mod tests {
|
|
|
1183
1191
|
|
|
1184
1192
|
#[test]
|
|
1185
1193
|
#[serial]
|
|
1186
|
-
fn
|
|
1194
|
+
fn test_to_ruby_wraps_module_as_rubyx_object() {
|
|
1187
1195
|
with_ruby_python(|_ruby, api| {
|
|
1188
1196
|
let module = api.import_module("os").expect("os should import");
|
|
1189
1197
|
let wrapper = RubyxObject::new(module, api).unwrap();
|
|
1190
1198
|
assert!(
|
|
1191
|
-
wrapper.to_ruby().
|
|
1192
|
-
"module should
|
|
1199
|
+
wrapper.to_ruby().is_ok(),
|
|
1200
|
+
"module should convert to RubyxObject via PyObjectRef"
|
|
1193
1201
|
);
|
|
1194
1202
|
drop(wrapper);
|
|
1195
1203
|
api.decref(module);
|
|
@@ -1928,4 +1936,84 @@ mod tests {
|
|
|
1928
1936
|
drop(w);
|
|
1929
1937
|
api.decref(os);
|
|
1930
1938
|
}
|
|
1939
|
+
|
|
1940
|
+
// ========== python_to_sendable: PyObjectRef fallback ==========
|
|
1941
|
+
|
|
1942
|
+
#[test]
|
|
1943
|
+
#[serial]
|
|
1944
|
+
fn test_python_to_sendable_module_returns_py_object_ref() {
|
|
1945
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1946
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1947
|
+
return;
|
|
1948
|
+
};
|
|
1949
|
+
let api = guard.api();
|
|
1950
|
+
let os = api.import_module("os").expect("os should import");
|
|
1951
|
+
let sendable = python_to_sendable(os, api).unwrap();
|
|
1952
|
+
match &sendable {
|
|
1953
|
+
SendableValue::PyObjectRef(addr) => {
|
|
1954
|
+
assert_eq!(*addr, os as usize);
|
|
1955
|
+
}
|
|
1956
|
+
other => panic!("expected PyObjectRef, got {other:?}"),
|
|
1957
|
+
}
|
|
1958
|
+
// Clean up: decref once for the sendable's incref, once for import_module
|
|
1959
|
+
api.decref(os);
|
|
1960
|
+
api.decref(os);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
#[test]
|
|
1964
|
+
#[serial]
|
|
1965
|
+
fn test_python_to_sendable_set_returns_err() {
|
|
1966
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1967
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1968
|
+
return;
|
|
1969
|
+
};
|
|
1970
|
+
let api = guard.api();
|
|
1971
|
+
// Sets don't have __dict__, so they return Err (not PyObjectRef)
|
|
1972
|
+
let globals = api.dict_new();
|
|
1973
|
+
let builtins = api
|
|
1974
|
+
.import_module("builtins")
|
|
1975
|
+
.expect("builtins should import");
|
|
1976
|
+
let key = api.string_from_str("__builtins__");
|
|
1977
|
+
api.dict_set_item(globals, key, builtins);
|
|
1978
|
+
api.decref(key);
|
|
1979
|
+
let result = api.run_string("{1, 2, 3}", 258, globals, globals);
|
|
1980
|
+
let py_set = result.expect("set eval should succeed");
|
|
1981
|
+
assert!(!py_set.is_null());
|
|
1982
|
+
|
|
1983
|
+
let sendable = python_to_sendable(py_set, api);
|
|
1984
|
+
assert!(sendable.is_err(), "set should return Err (no __dict__)");
|
|
1985
|
+
api.decref(py_set);
|
|
1986
|
+
api.decref(builtins);
|
|
1987
|
+
api.decref(globals);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
#[test]
|
|
1991
|
+
#[serial]
|
|
1992
|
+
fn test_python_to_sendable_primitives_not_py_object_ref() {
|
|
1993
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1994
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1995
|
+
return;
|
|
1996
|
+
};
|
|
1997
|
+
let api = guard.api();
|
|
1998
|
+
|
|
1999
|
+
// int → Integer, not PyObjectRef
|
|
2000
|
+
let py_int = api.long_from_i64(42);
|
|
2001
|
+
assert!(matches!(
|
|
2002
|
+
python_to_sendable(py_int, api),
|
|
2003
|
+
Ok(SendableValue::Integer(42))
|
|
2004
|
+
));
|
|
2005
|
+
|
|
2006
|
+
// str → Str, not PyObjectRef
|
|
2007
|
+
let py_str = api.string_from_str("hello");
|
|
2008
|
+
assert!(matches!(
|
|
2009
|
+
python_to_sendable(py_str, api),
|
|
2010
|
+
Ok(SendableValue::Str(_))
|
|
2011
|
+
));
|
|
2012
|
+
|
|
2013
|
+
// None → Nil, not PyObjectRef
|
|
2014
|
+
assert!(matches!(
|
|
2015
|
+
python_to_sendable(api.py_none, api),
|
|
2016
|
+
Ok(SendableValue::Nil)
|
|
2017
|
+
));
|
|
2018
|
+
}
|
|
1931
2019
|
}
|
data/ext/rubyx/src/stream.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use crate::api;
|
|
2
2
|
use crate::python_ffi::PyObject;
|
|
3
3
|
use crate::ruby_helpers::runtime_error;
|
|
4
|
-
use crate::rubyx_object::python_to_sendable;
|
|
4
|
+
use crate::rubyx_object::{python_to_sendable, RubyxObject};
|
|
5
5
|
use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
|
|
6
6
|
use magnus::value::ReprValue;
|
|
7
7
|
use magnus::{IntoValue, Value};
|
|
@@ -23,6 +23,7 @@ pub(crate) enum SendableValue {
|
|
|
23
23
|
Bool(bool),
|
|
24
24
|
List(Vec<SendableValue>),
|
|
25
25
|
Dict(Vec<(SendableValue, SendableValue)>),
|
|
26
|
+
PyObjectRef(usize),
|
|
26
27
|
}
|
|
27
28
|
impl TryInto<magnus::Value> for SendableValue {
|
|
28
29
|
type Error = magnus::Error;
|
|
@@ -54,6 +55,19 @@ impl TryInto<magnus::Value> for SendableValue {
|
|
|
54
55
|
}
|
|
55
56
|
hash.as_value()
|
|
56
57
|
}
|
|
58
|
+
SendableValue::PyObjectRef(addr) => {
|
|
59
|
+
let py_obj = addr as *mut PyObject;
|
|
60
|
+
let api = crate::API.get().ok_or_else(|| {
|
|
61
|
+
magnus::Error::new(runtime_error(), "Python API not initialized")
|
|
62
|
+
})?;
|
|
63
|
+
// RubyxObject::new increfs internally and python_to_sendable also incref
|
|
64
|
+
let wrapper = RubyxObject::new(py_obj, api).ok_or_else(|| {
|
|
65
|
+
magnus::Error::new(runtime_error(), "Failed to wrap Python object")
|
|
66
|
+
})?;
|
|
67
|
+
// Balance the extra incref from python_to_sendable
|
|
68
|
+
api.decref(py_obj);
|
|
69
|
+
wrapper.into_value_with(&ruby)
|
|
70
|
+
}
|
|
57
71
|
};
|
|
58
72
|
Ok(result)
|
|
59
73
|
}
|
|
@@ -710,4 +724,66 @@ mod tests {
|
|
|
710
724
|
assert_eq!(sum, (0..1000i64).sum::<i64>());
|
|
711
725
|
});
|
|
712
726
|
}
|
|
727
|
+
|
|
728
|
+
// ========== PyObjectRef: SendableValue → RubyxObject ==========
|
|
729
|
+
|
|
730
|
+
#[test]
|
|
731
|
+
#[serial]
|
|
732
|
+
fn test_py_object_ref_converts_to_rubyx_object() {
|
|
733
|
+
with_ruby_python(|_ruby, api| {
|
|
734
|
+
let os = api.import_module("os").expect("os should import");
|
|
735
|
+
api.incref(os); // incref for PyObjectRef (simulates what python_to_sendable does)
|
|
736
|
+
let sendable = SendableValue::PyObjectRef(os as usize);
|
|
737
|
+
|
|
738
|
+
let val: Value = sendable.try_into().expect("PyObjectRef should convert");
|
|
739
|
+
// The result should be a RubyxObject, not a primitive
|
|
740
|
+
assert!(!val.is_nil());
|
|
741
|
+
// Verify it's not a primitive type
|
|
742
|
+
assert!(i64::try_convert(val).is_err(), "should not be an Integer");
|
|
743
|
+
assert!(String::try_convert(val).is_err(), "should not be a String");
|
|
744
|
+
|
|
745
|
+
api.decref(os); // balance the import_module refcount
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
#[test]
|
|
750
|
+
#[serial]
|
|
751
|
+
fn test_py_object_ref_in_stream() {
|
|
752
|
+
with_ruby_python(|_ruby, api| {
|
|
753
|
+
let os = api.import_module("os").expect("os should import");
|
|
754
|
+
api.incref(os);
|
|
755
|
+
|
|
756
|
+
let (tx, rx) = unbounded();
|
|
757
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
758
|
+
let addr = os as usize;
|
|
759
|
+
|
|
760
|
+
thread::spawn(move || {
|
|
761
|
+
tx.send(Some(SendableValue::Integer(1))).ok();
|
|
762
|
+
tx.send(Some(SendableValue::PyObjectRef(addr))).ok();
|
|
763
|
+
tx.send(Some(SendableValue::Str("after".to_string()))).ok();
|
|
764
|
+
tx.send(None).ok();
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
768
|
+
|
|
769
|
+
// First: integer
|
|
770
|
+
let val = stream.next().unwrap().unwrap();
|
|
771
|
+
assert_eq!(i64::try_convert(val).unwrap(), 1);
|
|
772
|
+
|
|
773
|
+
// Second: PyObjectRef → RubyxObject
|
|
774
|
+
let val = stream.next().unwrap().unwrap();
|
|
775
|
+
assert!(!val.is_nil());
|
|
776
|
+
assert!(
|
|
777
|
+
i64::try_convert(val).is_err(),
|
|
778
|
+
"should be RubyxObject, not Integer"
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
// Third: string
|
|
782
|
+
let val = stream.next().unwrap().unwrap();
|
|
783
|
+
assert_eq!(String::try_convert(val).unwrap(), "after");
|
|
784
|
+
|
|
785
|
+
assert!(stream.next().is_none());
|
|
786
|
+
api.decref(os);
|
|
787
|
+
});
|
|
788
|
+
}
|
|
713
789
|
}
|
data/lib/rubyx/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubyx-py
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Naiker
|
|
@@ -62,6 +62,7 @@ extra_rdoc_files: []
|
|
|
62
62
|
files:
|
|
63
63
|
- Cargo.toml
|
|
64
64
|
- README.md
|
|
65
|
+
- docs/assets/logo.png
|
|
65
66
|
- ext/rubyx/Cargo.toml
|
|
66
67
|
- ext/rubyx/extconf.rb
|
|
67
68
|
- ext/rubyx/src/async_gen.rs
|
|
@@ -70,6 +71,7 @@ files:
|
|
|
70
71
|
- ext/rubyx/src/eval.rs
|
|
71
72
|
- ext/rubyx/src/exception.rs
|
|
72
73
|
- ext/rubyx/src/future.rs
|
|
74
|
+
- ext/rubyx/src/gvl.rs
|
|
73
75
|
- ext/rubyx/src/import.rs
|
|
74
76
|
- ext/rubyx/src/lib.rs
|
|
75
77
|
- ext/rubyx/src/nonblocking_stream.rs
|
|
@@ -85,6 +87,8 @@ files:
|
|
|
85
87
|
- ext/rubyx/src/stream.rs
|
|
86
88
|
- ext/rubyx/src/test_helpers.rs
|
|
87
89
|
- lib/generators/rubyx/install_generator.rb
|
|
90
|
+
- lib/generators/rubyx/templates/example.py
|
|
91
|
+
- lib/generators/rubyx/templates/pyproject.toml
|
|
88
92
|
- lib/generators/rubyx/templates/rubyx_initializer.rb
|
|
89
93
|
- lib/rubyx-py.rb
|
|
90
94
|
- lib/rubyx.rb
|