From 570176c7e776919c4506e513a0642a9324878805 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 1 Jun 2026 11:20:09 +0200 Subject: [PATCH] Use PyO3 main & enable c-api tests Enable `abi3t` support in PyO3 Add left over c-api functions Fix PyLong_AsUnsignedLongLongMask Update pyo3 to `0.29` Add `PyInt::as_u64_mask` Fix `PyInt::as_u32_mask` --- .cspell.dict/cpython.txt | 1 + .github/workflows/ci.yaml | 1 + .github/workflows/update-caches.yml | 1 + crates/capi/Cargo.toml | 2 +- crates/capi/pyo3-rustpython.config | 6 +- crates/capi/src/abstract_.rs | 37 +++++++++ crates/capi/src/abstract_/iter.rs | 2 +- crates/capi/src/abstract_/mapping.rs | 2 +- crates/capi/src/abstract_/number.rs | 2 +- crates/capi/src/abstract_/sequence.rs | 2 +- crates/capi/src/bytearrayobject.rs | 2 +- crates/capi/src/bytesobject.rs | 9 +- crates/capi/src/ceval.rs | 6 +- crates/capi/src/complexobject.rs | 4 +- crates/capi/src/critical_section.rs | 31 +++++++ crates/capi/src/descrobject.rs | 4 +- crates/capi/src/dictobject.rs | 45 +++++++++- crates/capi/src/floatobject.rs | 4 +- crates/capi/src/genericaliasobject.rs | 15 ++++ crates/capi/src/import.rs | 60 +++++++++++++- crates/capi/src/lib.rs | 3 + crates/capi/src/listobject.rs | 2 +- crates/capi/src/longobject.rs | 9 +- crates/capi/src/methodobject.rs | 8 +- crates/capi/src/object.rs | 2 +- crates/capi/src/osmodule.rs | 12 +++ crates/capi/src/pycapsule.rs | 4 +- crates/capi/src/pyerrors.rs | 101 ++++++++++++++++++++++- crates/capi/src/pylifecycle.rs | 33 +++++++- crates/capi/src/pystate.rs | 26 +++++- crates/capi/src/refcount.rs | 31 ++++++- crates/capi/src/setobject.rs | 2 +- crates/capi/src/sliceobject.rs | 2 +- crates/capi/src/tupleobject.rs | 8 +- crates/capi/src/unicodeobject.rs | 2 +- crates/capi/src/warnings.rs | 2 +- crates/capi/src/weakrefobject.rs | 2 +- crates/vm/src/stdlib/_ctypes/function.rs | 23 ++++++ 38 files changed, 449 insertions(+), 59 deletions(-) create mode 100644 crates/capi/src/critical_section.rs create mode 100644 crates/capi/src/genericaliasobject.rs create mode 100644 crates/capi/src/osmodule.rs diff --git a/.cspell.dict/cpython.txt b/.cspell.dict/cpython.txt index 11440e30f8a..47ec7dd60c8 100644 --- a/.cspell.dict/cpython.txt +++ b/.cspell.dict/cpython.txt @@ -170,6 +170,7 @@ nvars opname opnames orelse +osmodule outparam outparm paramfunc diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9f76984bea9..7ac163e835c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -343,6 +343,7 @@ jobs: with: openssl: true + # Keep features in sync with update-caches.yml CARGO_ARGS. - name: build rustpython run: cargo build --release --verbose --features=threading,jit ${{ env.CARGO_ARGS }} diff --git a/.github/workflows/update-caches.yml b/.github/workflows/update-caches.yml index 3a48418a502..fd3dddb7ae2 100644 --- a/.github/workflows/update-caches.yml +++ b/.github/workflows/update-caches.yml @@ -19,6 +19,7 @@ env: CARGO_PROFILE_TEST_DEBUG: 0 CARGO_PROFILE_DEV_DEBUG: 0 CARGO_PROFILE_RELEASE_DEBUG: 0 + # Keep feature list in sync with CI's release build in .github/workflows/ci.yaml. CARGO_ARGS: --workspace --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls-aws-lc,host_env,threading,jit --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher jobs: diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index e408d7ab0fe..671c8d1640c 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -22,7 +22,7 @@ rustpython-stdlib = {workspace = true, features = ["threading"] } rustpython-pylib = { workspace = true } [dev-dependencies] -pyo3 = { workspace = true, features = ["auto-initialize", "abi3"] } +pyo3 = { workspace = true, features = ["auto-initialize", "abi3t"] } [lints] workspace = true diff --git a/crates/capi/pyo3-rustpython.config b/crates/capi/pyo3-rustpython.config index fe59e46e895..601b56440d7 100644 --- a/crates/capi/pyo3-rustpython.config +++ b/crates/capi/pyo3-rustpython.config @@ -1,5 +1,5 @@ -implementation=CPython -version=3.14 +implementation=RustPython +version=3.15 shared=true -abi3=true +target_abi=RustPython-abi3t-3.15 suppress_build_script_link_lines=true diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index d01e31e9626..cd390135be4 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -178,3 +178,40 @@ pub unsafe extern "C" fn PyObject_Size(obj: *mut PyObject) -> isize { obj.length(vm) }) } + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::{PyDict, PyString}; + + #[test] + fn call_method1() { + Python::attach(|py| { + let string = PyString::new(py, "Hello, World!"); + assert!( + string + .call_method1("endswith", ("!",)) + .unwrap() + .is_truthy() + .unwrap() + ); + }) + } + + #[test] + fn object_set_get_del_item() { + Python::attach(|py| { + let obj = PyDict::new(py).into_any(); + obj.set_item("key", "value").unwrap(); + assert_eq!( + obj.get_item("key") + .unwrap() + .cast_into::() + .unwrap(), + "value" + ); + obj.del_item("key").unwrap(); + assert!(obj.get_item("key").is_err()); + }) + } +} diff --git a/crates/capi/src/abstract_/iter.rs b/crates/capi/src/abstract_/iter.rs index 1ba5bd04d19..fbd1440e0d0 100644 --- a/crates/capi/src/abstract_/iter.rs +++ b/crates/capi/src/abstract_/iter.rs @@ -89,7 +89,7 @@ pub unsafe extern "C" fn PyIter_Send( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyAnyMethods, PyIterator, PyList, PySendResult}; diff --git a/crates/capi/src/abstract_/mapping.rs b/crates/capi/src/abstract_/mapping.rs index 143d5a97744..6fec18bffd6 100644 --- a/crates/capi/src/abstract_/mapping.rs +++ b/crates/capi/src/abstract_/mapping.rs @@ -194,7 +194,7 @@ pub unsafe extern "C" fn PyMapping_SetItemString( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyDict, PyMapping, PyMappingMethods, PyTuple}; diff --git a/crates/capi/src/abstract_/number.rs b/crates/capi/src/abstract_/number.rs index b1dc627d828..ffc78ea4e24 100644 --- a/crates/capi/src/abstract_/number.rs +++ b/crates/capi/src/abstract_/number.rs @@ -232,7 +232,7 @@ pub unsafe extern "C" fn PyNumber_Subtract(o1: *mut PyObject, o2: *mut PyObject) with_vm(|vm| vm._sub(unsafe { &*o1 }, unsafe { &*o2 })) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; diff --git a/crates/capi/src/abstract_/sequence.rs b/crates/capi/src/abstract_/sequence.rs index f6022b93e91..d011dfdb8eb 100644 --- a/crates/capi/src/abstract_/sequence.rs +++ b/crates/capi/src/abstract_/sequence.rs @@ -177,7 +177,7 @@ pub unsafe extern "C" fn PySequence_In(obj: *mut PyObject, value: *mut PyObject) unsafe { PySequence_Contains(obj, value) } } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyAnyMethods, PyDict, PyList, PySequence, PySequenceMethods, PyTuple}; diff --git a/crates/capi/src/bytearrayobject.rs b/crates/capi/src/bytearrayobject.rs index 2bc56895ba8..cc9db5dd50e 100644 --- a/crates/capi/src/bytearrayobject.rs +++ b/crates/capi/src/bytearrayobject.rs @@ -71,7 +71,7 @@ pub unsafe extern "C" fn PyByteArray_Resize(bytearray: *mut PyObject, len: isize }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyByteArray, PyBytes}; diff --git a/crates/capi/src/bytesobject.rs b/crates/capi/src/bytesobject.rs index 1fe535efba5..f4db16af6b3 100644 --- a/crates/capi/src/bytesobject.rs +++ b/crates/capi/src/bytesobject.rs @@ -1,6 +1,5 @@ -use crate::PyObject; use crate::object::define_py_check; -use crate::pystate::with_vm; +use crate::{PyObject, pystate::with_vm}; use core::ffi::c_char; use rustpython_vm::builtins::PyBytes; @@ -46,13 +45,13 @@ pub unsafe extern "C" fn PyBytes_AsString(bytes: *mut PyObject) -> *mut c_char { }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyBytes; #[test] - fn test_bytes() { + fn bytes() { Python::attach(|py| { let bytes = PyBytes::new(py, b"Hello, World!"); assert_eq!(bytes.as_bytes(), b"Hello, World!"); @@ -60,7 +59,7 @@ mod tests { } #[test] - fn test_bytes_uninit() { + fn bytes_uninit() { Python::attach(|py| { let bytes = PyBytes::new_with(py, 13, |data| { data.copy_from_slice(b"Hello, World!"); diff --git a/crates/capi/src/ceval.rs b/crates/capi/src/ceval.rs index 867c39e0388..d137cb17dab 100644 --- a/crates/capi/src/ceval.rs +++ b/crates/capi/src/ceval.rs @@ -52,13 +52,13 @@ pub extern "C" fn PyEval_GetBuiltins() -> *mut PyObject { }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::exceptions::PyException; use pyo3::prelude::*; #[test] - fn test_code_eval() { + fn code_eval() { Python::attach(|py| { let result = py.eval(c"1 + 1", None, None).unwrap(); assert_eq!(result.extract::().unwrap(), 2); @@ -66,7 +66,7 @@ mod tests { } #[test] - fn test_code_run_exception() { + fn code_run_exception() { Python::attach(|py| { let err = py.run(c"raise Exception()", None, None).unwrap_err(); assert!(err.is_instance_of::(py)); diff --git a/crates/capi/src/complexobject.rs b/crates/capi/src/complexobject.rs index a6b2bb731a0..79f1804d1bd 100644 --- a/crates/capi/src/complexobject.rs +++ b/crates/capi/src/complexobject.rs @@ -36,13 +36,13 @@ pub unsafe extern "C" fn PyComplex_ImagAsDouble(obj: *mut PyObject) -> c_double with_vm(|vm| try_to_complex(vm, unsafe { &*obj }).map(|complex| complex.im)) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyComplex; #[test] - fn test_py_int() { + fn py_int() { Python::attach(|py| { let number = PyComplex::from_doubles(py, 1.0, 2.0); assert_eq!(number.real(), 1.0); diff --git a/crates/capi/src/critical_section.rs b/crates/capi/src/critical_section.rs new file mode 100644 index 00000000000..f50f2b61607 --- /dev/null +++ b/crates/capi/src/critical_section.rs @@ -0,0 +1,31 @@ +use crate::PyObject; + +#[repr(C)] +pub struct PyCriticalSection; + +#[repr(C)] +pub struct PyCriticalSection2; + +#[unsafe(no_mangle)] +pub extern "C" fn PyCriticalSection_Begin(c: *mut PyCriticalSection, op: *mut PyObject) { + let _ = (c, op); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCriticalSection_End(c: *mut PyCriticalSection) { + let _ = c; +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCriticalSection2_Begin( + c: *mut PyCriticalSection2, + a: *mut PyObject, + b: *mut PyObject, +) { + let _ = (c, a, b); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCriticalSection2_End(c: *mut PyCriticalSection2) { + let _ = c; +} diff --git a/crates/capi/src/descrobject.rs b/crates/capi/src/descrobject.rs index 5232634fabb..b0d24667dc7 100644 --- a/crates/capi/src/descrobject.rs +++ b/crates/capi/src/descrobject.rs @@ -11,7 +11,7 @@ pub unsafe extern "C" fn PyDictProxy_New(mapping: *mut PyObject) -> *mut PyObjec }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyDict, PyInt, PyMappingProxy}; @@ -23,7 +23,7 @@ mod tests { dict.set_item("x", 7).unwrap(); let mapping = dict.as_mapping(); - let proxy = PyMappingProxy::new(py, &mapping); + let proxy = PyMappingProxy::new(py, mapping); let value = proxy.get_item("x").unwrap().cast_into::().unwrap(); assert_eq!(value, 7); }) diff --git a/crates/capi/src/dictobject.rs b/crates/capi/src/dictobject.rs index ebc4a827f36..e326ba87e3a 100644 --- a/crates/capi/src/dictobject.rs +++ b/crates/capi/src/dictobject.rs @@ -57,6 +57,43 @@ pub unsafe extern "C" fn PyDict_GetItemRef( }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDict_SetDefaultRef( + dict: *mut PyObject, + key: *mut PyObject, + default_value: *mut PyObject, + result: *mut *mut PyObject, +) -> c_int { + with_vm(|vm| { + let result = NonNull::new(result); + if let Some(result) = result { + unsafe { + result.write(core::ptr::null_mut()); + } + } + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + let key = unsafe { &*key }; + + if let Some(value) = dict.inner_getitem_opt(key, vm)? { + if let Some(result) = result { + unsafe { + result.write(value.into_raw().as_ptr()); + } + } + Ok(true) + } else { + let value = unsafe { &*default_value }.to_owned(); + dict.inner_setitem(key, value.clone(), vm)?; + if let Some(result) = result { + unsafe { + result.write(value.into_raw().as_ptr()); + } + } + Ok(false) + } + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyDict_Size(dict: *mut PyObject) -> isize { with_vm(|vm| { @@ -187,13 +224,13 @@ pub unsafe extern "C" fn PyDict_Next( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyDict, PyDictMethods, PyInt, PyList}; #[test] - fn test_create_empty_dict() { + fn create_empty_dict() { Python::attach(|py| { let dict = PyDict::new(py); assert!(dict.is_instance_of::()); @@ -201,7 +238,7 @@ mod tests { } #[test] - fn test_create_dict_with_items() { + fn create_dict_with_items() { Python::attach(|py| { let dict = [(1, 2), (3, 4)].into_py_dict(py)?; let value = dict.get_item(1)?.unwrap().cast_into::()?; @@ -214,7 +251,7 @@ mod tests { } #[test] - fn test_dict_iter() { + fn dict_iter() { Python::attach(|py| { let dict = [(1, 2), (3, 4)].into_py_dict(py).unwrap(); let values = dict diff --git a/crates/capi/src/floatobject.rs b/crates/capi/src/floatobject.rs index 831ca32af1e..ead09c219bb 100644 --- a/crates/capi/src/floatobject.rs +++ b/crates/capi/src/floatobject.rs @@ -57,14 +57,14 @@ pub unsafe extern "C" fn PyFloat_FromString(obj: *mut PyObject) -> *mut PyObject }) } -#[cfg(false)] +#[cfg(test)] mod tests { use core::f64::consts::PI; use pyo3::prelude::*; use pyo3::types::PyFloat; #[test] - fn test_py_float() { + fn py_float() { Python::attach(|py| { let pi = PyFloat::new(py, PI); assert!(pi.is_instance_of::()); diff --git a/crates/capi/src/genericaliasobject.rs b/crates/capi/src/genericaliasobject.rs new file mode 100644 index 00000000000..bcd31308679 --- /dev/null +++ b/crates/capi/src/genericaliasobject.rs @@ -0,0 +1,15 @@ +use crate::{PyObject, pystate::with_vm}; +use rustpython_vm::PyPayload; +use rustpython_vm::builtins::PyGenericAlias; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_GenericAlias( + origin: *mut PyObject, + args: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let origin = unsafe { &*origin }.to_owned(); + let args = unsafe { &*args }.to_owned(); + PyGenericAlias::from_args(origin, args, vm).into_pyobject(vm) + }) +} diff --git a/crates/capi/src/import.rs b/crates/capi/src/import.rs index 3550baa1440..c6d5ce85ed6 100644 --- a/crates/capi/src/import.rs +++ b/crates/capi/src/import.rs @@ -1,5 +1,7 @@ use crate::{PyObject, pystate::with_vm}; -use rustpython_vm::builtins::PyStr; +use core::ffi::{CStr, c_char}; +use rustpython_vm::builtins::{PyCode, PyDict, PyModule, PyStr}; +use rustpython_vm::import::import_code_obj; #[unsafe(no_mangle)] pub unsafe extern "C" fn PyImport_Import(name: *mut PyObject) -> *mut PyObject { @@ -9,12 +11,64 @@ pub unsafe extern "C" fn PyImport_Import(name: *mut PyObject) -> *mut PyObject { }) } -#[cfg(false)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyImport_AddModuleRef(name: *const c_char) -> *mut PyObject { + with_vm(|vm| { + let name = unsafe { CStr::from_ptr(name) } + .to_str() + .map_err(|_| vm.new_system_error("PyImport_AddModuleRef called with non utf8 name"))?; + + let sys_modules = vm + .sys_module + .get_attr(rustpython_vm::identifier!(vm, modules), vm)?; + + sys_modules + .try_downcast_ref::(vm)? + .get_item_opt(name, vm)? + .map_or_else( + || { + let module = vm.new_module(name, vm.ctx.new_dict(), None); + sys_modules.set_item(name, module.clone().into(), vm)?; + Ok(module) + }, + |module| { + let module = module.try_downcast_ref::(vm)?; + Ok(module.to_owned()) + }, + ) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyImport_ExecCodeModuleEx( + name: *const c_char, + co: *mut PyObject, + pathname: *const c_char, +) -> *mut PyObject { + with_vm(|vm| { + let name = unsafe { CStr::from_ptr(name) }.to_str().map_err(|_| { + vm.new_system_error("PyImport_ExecCodeModuleEx called with non utf8 name") + })?; + let code = unsafe { &*co }.try_downcast_ref::(vm)?; + let module = import_code_obj(vm, name, code.to_owned(), false)?; + + if !pathname.is_null() { + let pathname = unsafe { CStr::from_ptr(pathname) }.to_str().map_err(|_| { + vm.new_system_error("PyImport_ExecCodeModuleEx called with non utf8 pathname") + })?; + module.set_attr("__file__", vm.ctx.new_str(pathname), vm)?; + } + + Ok(module) + }) +} + +#[cfg(test)] mod tests { use pyo3::prelude::*; #[test] - fn test_import() { + fn import() { Python::attach(|py| { let _module = py.import("sys").unwrap(); }) diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 638a78d6327..eba7de2786d 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -14,15 +14,18 @@ pub mod bytearrayobject; pub mod bytesobject; pub mod ceval; pub mod complexobject; +pub mod critical_section; pub mod descrobject; pub mod dictobject; pub mod floatobject; +pub mod genericaliasobject; pub mod import; pub mod listobject; pub mod longobject; pub mod methodobject; pub mod moduleobject; pub mod object; +pub mod osmodule; pub mod pycapsule; pub mod pyerrors; pub mod pylifecycle; diff --git a/crates/capi/src/listobject.rs b/crates/capi/src/listobject.rs index 796720f99d7..03069b0495b 100644 --- a/crates/capi/src/listobject.rs +++ b/crates/capi/src/listobject.rs @@ -165,7 +165,7 @@ pub unsafe extern "C" fn PyList_Sort(list: *mut PyObject) -> c_int { }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::exceptions::PyIndexError; use pyo3::prelude::*; diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index 0668f8df643..d88523a7ddc 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -1,6 +1,5 @@ -use crate::PyObject; use crate::object::define_py_check; -use crate::pystate::with_vm; +use crate::{PyObject, pystate::with_vm}; use bitflags::bitflags; use core::ffi::{CStr, c_char, c_double, c_int, c_long, c_longlong, c_ulong, c_ulonglong, c_void}; use malachite_bigint::{BigInt, Sign}; @@ -373,13 +372,13 @@ pub unsafe extern "C" fn PyLong_AsUnsignedLongLong(obj: *mut PyObject) -> c_ulon }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyInt; #[test] - fn test_py_int_u32() { + fn py_int_u32() { Python::attach(|py| { let number = PyInt::new(py, 123); assert!(number.is_instance_of::()); @@ -388,7 +387,7 @@ mod tests { } #[test] - fn test_py_int_u64() { + fn py_int_u64() { Python::attach(|py| { let number = PyInt::new(py, 123u64); assert!(number.is_instance_of::()); diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index c0a6611a01f..b234ba76a9c 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -306,7 +306,7 @@ pub unsafe extern "C" fn PyCFunction_NewEx( unsafe { PyCMethod_New(ml, slf, module, core::ptr::null_mut()) } } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::exceptions::PyException; use pyo3::ffi::{PyLong_FromLong, PyObject}; @@ -314,7 +314,7 @@ mod tests { use pyo3::types::{PyCFunction, PyInt, PyString}; #[test] - fn test_closure_function() { + fn closure_function() { Python::attach(|py| { let f = PyCFunction::new_closure(py, None, None, |_args, _kwargs| "Hello from Rust!") .unwrap(); @@ -327,7 +327,7 @@ mod tests { } #[test] - fn test_function_no_args() { + fn function_no_args() { Python::attach(|py| { unsafe extern "C" fn c_fn(_self: *mut PyObject, _args: *mut PyObject) -> *mut PyObject { assert!(_self.is_null()); @@ -352,7 +352,7 @@ mod tests { } #[test] - fn test_closure_function_error() { + fn closure_function_error() { Python::attach(|py| { let f = PyCFunction::new_closure(py, None, None, |_args, _kwargs| { Err::<(), _>(PyException::new_err("Something went wrong")) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index d92e9977f2c..9a5d2682ee2 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -541,7 +541,7 @@ pub unsafe extern "C" fn PyObject_GenericSetDict( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::class::basic::CompareOp; use pyo3::prelude::*; diff --git a/crates/capi/src/osmodule.rs b/crates/capi/src/osmodule.rs new file mode 100644 index 00000000000..132935ee8af --- /dev/null +++ b/crates/capi/src/osmodule.rs @@ -0,0 +1,12 @@ +use crate::{PyObject, pystate::with_vm}; +use rustpython_vm::convert::ToPyObject; +use rustpython_vm::function::FsPath; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyOS_FSPath(path: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let path = unsafe { &*path }.to_owned(); + let fspath = FsPath::try_from_path_like(path, false, vm)?; + Ok(fspath.to_pyobject(vm)) + }) +} diff --git a/crates/capi/src/pycapsule.rs b/crates/capi/src/pycapsule.rs index 7a5d599c851..a1b5effd88c 100644 --- a/crates/capi/src/pycapsule.rs +++ b/crates/capi/src/pycapsule.rs @@ -142,13 +142,13 @@ fn checked_capsule<'a>( Ok(capsule) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyCapsule; #[test] - fn test_capsule_new() { + fn capsule_new() { Python::attach(|py| { let value = String::from("Some data"); let capsule = PyCapsule::new_with_value(py, value, c"my_capsule").unwrap(); diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index b767b7c4090..d7efd1a0d6f 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -3,6 +3,7 @@ use crate::{PyObject, pystate::with_vm}; use core::convert::Infallible; use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; +use core::slice; use rustpython_vm::builtins::{PyBaseException, PyTuple, PyType}; use rustpython_vm::convert::IntoObject; use rustpython_vm::exceptions::ExceptionZoo; @@ -299,9 +300,90 @@ pub unsafe extern "C" fn PyException_GetContext(exc: *mut PyObject) -> *mut PyOb }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyException_SetCause(exc: *mut PyObject, cause: *mut PyObject) { + with_vm(|vm| { + let exc = unsafe { &*exc }.try_downcast_ref::(vm)?; + let cause = NonNull::new(cause) + .map(|obj| unsafe { PyObjectRef::from_raw(obj).downcast_unchecked() }); + exc.set___cause__(cause); + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyException_SetContext(exc: *mut PyObject, context: *mut PyObject) { + with_vm(|vm| { + let exc = unsafe { &*exc }.try_downcast_ref::(vm)?; + let context = NonNull::new(context) + .map(|obj| unsafe { PyObjectRef::from_raw(obj).downcast_unchecked() }); + exc.set___context__(context); + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicodeDecodeError_Create( + encoding: *const c_char, + object: *const c_char, + length: isize, + start: isize, + end: isize, + reason: *const c_char, +) -> *mut PyObject { + with_vm(|vm| { + let encoding = unsafe { CStr::from_ptr(encoding) } + .to_str() + .map_err(|_| vm.new_system_error("encoding must be valid UTF-8"))?; + let reason = unsafe { CStr::from_ptr(reason) } + .to_str() + .map_err(|_| vm.new_system_error("reason must be valid UTF-8"))?; + let length: usize = length + .try_into() + .map_err(|_| vm.new_system_error("length must be non-negative"))?; + let start: usize = start + .try_into() + .map_err(|_| vm.new_system_error("start must be non-negative"))?; + let end: usize = end + .try_into() + .map_err(|_| vm.new_system_error("end must be non-negative"))?; + + let bytes = if object.is_null() { + if length != 0 { + return Err(vm.new_system_error( + "PyUnicodeDecodeError_Create called with null object and non-zero length", + )); + } + Vec::new() + } else { + unsafe { slice::from_raw_parts(object.cast::(), length) }.to_vec() + }; + + let exc = vm.new_unicode_decode_error_real( + vm.ctx.new_str(encoding), + vm.ctx.new_bytes(bytes), + start, + end, + vm.ctx.new_str(reason), + ); + Ok(exc) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyException_SetTraceback(exc: *mut PyObject, tb: *mut PyObject) -> c_int { + with_vm(|vm| { + let exc = unsafe { &*exc }.try_downcast_ref::(vm)?; + let traceback = unsafe { tb.as_ref() }.map(|obj| obj.to_owned()); + exc.set___traceback__(vm.unwrap_or_none(traceback), vm) + }) +} + #[cfg(test)] mod tests { - use pyo3::exceptions::PyTypeError; + use pyo3::PyTypeInfo; + use pyo3::create_exception; + use pyo3::exceptions::{PyException, PyTypeError}; use pyo3::prelude::*; #[test] @@ -309,7 +391,7 @@ mod tests { Python::attach(|py| { PyTypeError::new_err(py.None()).restore(py); assert!(PyErr::occurred(py)); - assert!(unsafe { !pyo3::ffi::PyErr_GetRaisedException().is_null() }); + assert!(PyErr::take(py).is_some()); assert!(!PyErr::occurred(py)); }) } @@ -321,4 +403,19 @@ mod tests { assert!(err.is_instance_of::(py)); }) } + + #[test] + fn new_exception_type() { + create_exception!(my_module, MyError, PyException, "Some description."); + + Python::attach(|py| { + let exc = MyError::new_err("This is a new exception"); + assert!(exc.is_instance_of::(py)); + let exc_type = MyError::type_object(py); + assert_eq!( + exc_type.fully_qualified_name().unwrap(), + "my_module.MyError" + ); + }) + } } diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index df964d11753..de673b33f0a 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,11 +1,13 @@ use crate::get_main_interpreter; use crate::pyerrors::init_exception_statics; use crate::pystate::ensure_thread_has_vm_attached; -use core::ffi::c_int; +use alloc::ffi::CString; +use core::ffi::{c_char, c_int, c_ulong}; use rustpython_vm::common::rc::PyRc; +use rustpython_vm::version::{MAJOR, MICRO, MINOR, VERSION_HEX}; use rustpython_vm::vm::thread::ThreadedVirtualMachine; use rustpython_vm::{Context, Interpreter}; -use std::sync::Mutex; +use std::sync::{LazyLock, Mutex}; pub(crate) static MAIN_INTERP: Mutex> = Mutex::new(None); @@ -17,6 +19,9 @@ pub(crate) fn request_vm_from_interpreter() -> ThreadedVirtualMachine { .enter(|vm| vm.new_thread()) } +#[unsafe(no_mangle)] +pub static Py_Version: c_ulong = VERSION_HEX as c_ulong; + #[unsafe(no_mangle)] pub extern "C" fn Py_IsInitialized() -> c_int { get_main_interpreter().is_some() as c_int @@ -65,3 +70,27 @@ pub extern "C" fn Py_FinalizeEx() -> c_int { pub extern "C" fn Py_IsFinalizing() -> c_int { 0 } + +#[unsafe(no_mangle)] +pub extern "C" fn Py_GetVersion() -> *const c_char { + static VERSION: LazyLock = LazyLock::new(|| { + CString::new(format!("{MAJOR}.{MINOR}.{MICRO}")) + .expect("version string must not contain interior NULs") + }); + VERSION.as_ptr() +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + + #[test] + fn get_version() { + Python::attach(|py| { + let version = py.version_info(); + assert!(version >= (3, 14)); + }); + + assert!(unsafe { pyo3::ffi::Py_Version } >= 0x030d0000); + } +} diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index f3d12f04a0b..1a2f66de9f1 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -1,11 +1,12 @@ +use crate::get_main_interpreter; use crate::pylifecycle::request_vm_from_interpreter; use crate::util::FfiResult; use core::ffi::c_int; use core::ptr; -use rustpython_vm::VirtualMachine; use rustpython_vm::vm::thread::{ CurrentVmAttachState, attach_current_thread, release_current_thread, with_current_vm, }; +use rustpython_vm::{Interpreter, VirtualMachine}; pub(crate) fn with_vm, O>(f: impl FnOnce(&VirtualMachine) -> R) -> O { with_current_vm(|vm| f(vm).into_output(vm)) @@ -16,9 +17,11 @@ type PyGILState_STATE = c_int; const PYGILSTATE_LOCKED: PyGILState_STATE = 0; const PYGILSTATE_UNLOCKED: PyGILState_STATE = 1; +pub type PyInterpreterState = Interpreter; + #[repr(C)] pub struct PyThreadState { - _interp: *mut core::ffi::c_void, + pub interp: *mut PyInterpreterState, } /// Make sure this thread has a running vm attached. This only creates a new vm if we don't already @@ -50,6 +53,25 @@ pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState { #[unsafe(no_mangle)] pub extern "C" fn PyEval_RestoreThread(_state: *mut PyThreadState) {} +#[unsafe(no_mangle)] +pub extern "C" fn PyInterpreterState_Get() -> *mut PyInterpreterState { + get_main_interpreter() + .as_ref() + .map(|interp| interp as *const PyInterpreterState) + .expect("PyInterpreterState_Get called but no main interpreter was found") + .cast_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyInterpreterState_GetID(interp: *mut PyInterpreterState) -> i64 { + with_vm(|vm| { + if interp.is_null() { + return Err(vm.new_system_error("PyInterpreterState_GetID called with null interp")); + } + Ok(interp as usize as i64) + }) +} + #[cfg(test)] mod tests { use crate::get_main_interpreter; diff --git a/crates/capi/src/refcount.rs b/crates/capi/src/refcount.rs index 917dfeec2b9..849b130e292 100644 --- a/crates/capi/src/refcount.rs +++ b/crates/capi/src/refcount.rs @@ -1,4 +1,4 @@ -use crate::PyObject; +use crate::{PyObject, pystate::with_vm}; use core::ptr::NonNull; use rustpython_vm::PyObjectRef; @@ -13,3 +13,32 @@ pub unsafe extern "C" fn _Py_IncRef(op: *mut PyObject) { // Don't drop the owned value, as we just want to increment the refcount. core::mem::forget(unsafe { (*op).to_owned() }); } + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_NewRef(op: *mut PyObject) -> *mut PyObject { + with_vm(|_vm| unsafe { (*op).to_owned() }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_REFCNT(op: *mut PyObject) -> isize { + with_vm(|_vm| unsafe { &*op }.strong_count()) +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::PyInt; + use pyo3::{PyTypeInfo, ffi}; + + #[test] + fn refcount() { + Python::attach(|py| unsafe { + let obj = PyInt::type_object(py); + let ref_count = ffi::Py_REFCNT(obj.as_ptr()); + let obj_clone = obj.clone(); + assert_eq!(ffi::Py_REFCNT(obj.as_ptr()), ref_count + 1); + drop(obj_clone); + assert_eq!(ffi::Py_REFCNT(obj.as_ptr()), ref_count); + }); + } +} diff --git a/crates/capi/src/setobject.rs b/crates/capi/src/setobject.rs index cc479371b27..1036bb1473a 100644 --- a/crates/capi/src/setobject.rs +++ b/crates/capi/src/setobject.rs @@ -116,7 +116,7 @@ pub unsafe extern "C" fn PySet_Size(anyset: *mut PyObject) -> isize { }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyFrozenSet, PyInt, PySet}; diff --git a/crates/capi/src/sliceobject.rs b/crates/capi/src/sliceobject.rs index 2a625fab523..fb588181531 100644 --- a/crates/capi/src/sliceobject.rs +++ b/crates/capi/src/sliceobject.rs @@ -72,7 +72,7 @@ pub unsafe extern "C" fn PySlice_AdjustIndices( slice_len as isize } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PySlice, PySliceMethods}; diff --git a/crates/capi/src/tupleobject.rs b/crates/capi/src/tupleobject.rs index 985141f6d4c..60c4b81b370 100644 --- a/crates/capi/src/tupleobject.rs +++ b/crates/capi/src/tupleobject.rs @@ -90,13 +90,13 @@ pub unsafe extern "C" fn PyTuple_GetSlice( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyTuple; #[test] - fn test_empty_tuple() { + fn empty_tuple() { Python::attach(|py| { let tuple = PyTuple::empty(py); assert_eq!(tuple.len(), 0); @@ -104,7 +104,7 @@ mod tests { } #[test] - fn test_tuple_into_python() { + fn tuple_into_python() { Python::attach(|py| { let tuple = (1, 2, 3).into_pyobject(py).unwrap(); assert_eq!(tuple.len(), 3); @@ -112,7 +112,7 @@ mod tests { } #[test] - fn test_tuple_get_slice() { + fn tuple_get_slice() { Python::attach(|py| { let tuple = (1, 2, 3).into_pyobject(py).unwrap(); let slice = tuple.get_slice(1, 2); diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index 33d46692602..787e31ea571 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -241,7 +241,7 @@ pub unsafe extern "C" fn PyUnicode_EqualToUTF8AndSize( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use std::ffi::{OsStr, OsString}; diff --git a/crates/capi/src/warnings.rs b/crates/capi/src/warnings.rs index 22ffd6ab939..4966cd60d6d 100644 --- a/crates/capi/src/warnings.rs +++ b/crates/capi/src/warnings.rs @@ -92,7 +92,7 @@ pub unsafe extern "C" fn PyErr_WarnExplicit( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::exceptions::{PyRuntimeWarning, PyUserWarning}; use pyo3::prelude::*; diff --git a/crates/capi/src/weakrefobject.rs b/crates/capi/src/weakrefobject.rs index 706de84b53d..de095e5e594 100644 --- a/crates/capi/src/weakrefobject.rs +++ b/crates/capi/src/weakrefobject.rs @@ -65,7 +65,7 @@ pub unsafe extern "C" fn PyWeakref_NewRef( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyAnyMethods; diff --git a/crates/vm/src/stdlib/_ctypes/function.rs b/crates/vm/src/stdlib/_ctypes/function.rs index 25cbcdcd9a1..2cf3eda13e1 100644 --- a/crates/vm/src/stdlib/_ctypes/function.rs +++ b/crates/vm/src/stdlib/_ctypes/function.rs @@ -19,6 +19,7 @@ use crate::{ use alloc::borrow::Cow; use core::ffi::c_void; use core::fmt::Debug; +use core::ptr::NonNull; use num_traits::{Signed, ToPrimitive}; use rustpython_common::lock::PyRwLock; #[cfg(windows)] @@ -1432,6 +1433,28 @@ fn convert_raw_result( let info = stg_info.unwrap(); + // py_object: interpret return value as PyObject* and materialize it. + if let Ok(type_attr) = restype_type + .as_object() + .get_attr(vm.ctx.intern_str("_type_"), vm) + && let Some(type_str) = type_attr.downcast_ref::() + && type_str.to_str() == Some("O") + { + let ptr = match raw_result { + RawResult::Pointer(p) => *p, + RawResult::Value(v) => *v as usize, + RawResult::Void => 0, + }; + let ptr = NonNull::new(ptr as *mut PyObject).or_else(|| { + vm.set_exception(Some(vm.new_value_error("PyObject is NULL"))); + None + })?; + unsafe { + let obj = PyObjectRef::from_raw(ptr); + return Some(obj); + } + } + // 5. Simple type with getfunc → use bytes_to_pyobject (info->getfunc) // is_simple_instance returns TRUE for c_int, c_void_p, etc. if super::base::is_simple_instance(&restype_type) {