From 54156f638f7f6c3599751c6db9c06ecde8ee13c6 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 4 Apr 2026 17:21:44 +0200 Subject: [PATCH 001/135] export c api --- Cargo.lock | 27 ++++-- crates/capi/Cargo.toml | 18 ++++ crates/capi/src/bytesobject.rs | 16 ++++ crates/capi/src/import.rs | 9 ++ crates/capi/src/lib.rs | 33 +++++++ crates/capi/src/longobject.rs | 40 ++++++++ crates/capi/src/object.rs | 94 +++++++++++++++++++ crates/capi/src/pyerrors.rs | 58 ++++++++++++ crates/capi/src/pylifecycle.rs | 49 ++++++++++ crates/capi/src/pystate.rs | 28 ++++++ crates/capi/src/refcount.rs | 35 +++++++ crates/capi/src/traceback.rs | 9 ++ crates/capi/src/tupleobject.rs | 15 +++ crates/capi/src/unicodeobject.rs | 34 +++++++ .../pyo3_embed/.cargo/config.toml | 2 + example_projects/pyo3_embed/Cargo.toml | 12 +++ example_projects/pyo3_embed/README.md | 11 +++ example_projects/pyo3_embed/build.rs | 6 ++ .../pyo3_embed/pyo3-rustpython.config | 6 ++ example_projects/pyo3_embed/src/main.rs | 10 ++ 20 files changed, 502 insertions(+), 10 deletions(-) create mode 100644 crates/capi/Cargo.toml create mode 100644 crates/capi/src/bytesobject.rs create mode 100644 crates/capi/src/import.rs create mode 100644 crates/capi/src/lib.rs create mode 100644 crates/capi/src/longobject.rs create mode 100644 crates/capi/src/object.rs create mode 100644 crates/capi/src/pyerrors.rs create mode 100644 crates/capi/src/pylifecycle.rs create mode 100644 crates/capi/src/pystate.rs create mode 100644 crates/capi/src/refcount.rs create mode 100644 crates/capi/src/traceback.rs create mode 100644 crates/capi/src/tupleobject.rs create mode 100644 crates/capi/src/unicodeobject.rs create mode 100644 example_projects/pyo3_embed/.cargo/config.toml create mode 100644 example_projects/pyo3_embed/Cargo.toml create mode 100644 example_projects/pyo3_embed/README.md create mode 100644 example_projects/pyo3_embed/build.rs create mode 100644 example_projects/pyo3_embed/pyo3-rustpython.config create mode 100644 example_projects/pyo3_embed/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 5fd981fd135..1eca00b9fbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2643,9 +2643,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1" +checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" dependencies = [ "libc", "once_cell", @@ -2657,18 +2657,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7" +checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc" +checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" dependencies = [ "libc", "pyo3-build-config", @@ -2676,9 +2676,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e" +checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2688,9 +2688,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a" +checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" dependencies = [ "heck", "proc-macro2", @@ -3104,6 +3104,13 @@ dependencies = [ "winresource", ] +[[package]] +name = "rustpython-capi" +version = "0.5.0" +dependencies = [ + "rustpython-vm", +] + [[package]] name = "rustpython-codegen" version = "0.5.0" diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml new file mode 100644 index 00000000000..bf3bc8ca285 --- /dev/null +++ b/crates/capi/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rustpython-capi" +description = "Minimal CPython C-API compatibility exports for RustPython" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +crate-type = ["staticlib"] + +[dependencies] +rustpython-vm = { workspace = true } + +[lints] +workspace = true diff --git a/crates/capi/src/bytesobject.rs b/crates/capi/src/bytesobject.rs new file mode 100644 index 00000000000..a5164bc8487 --- /dev/null +++ b/crates/capi/src/bytesobject.rs @@ -0,0 +1,16 @@ +use core::ffi::c_char; +use core::ptr; + +use crate::PyObject; + +#[unsafe(no_mangle)] +pub extern "C" fn PyBytes_Size(_bytes: *mut PyObject) -> isize { + crate::log_stub("PyBytes_Size"); + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyBytes_AsString(_bytes: *mut PyObject) -> *mut c_char { + crate::log_stub("PyBytes_AsString"); + ptr::null_mut() +} diff --git a/crates/capi/src/import.rs b/crates/capi/src/import.rs new file mode 100644 index 00000000000..29ea7996687 --- /dev/null +++ b/crates/capi/src/import.rs @@ -0,0 +1,9 @@ +use core::ptr; + +use crate::PyObject; + +#[unsafe(no_mangle)] +pub extern "C" fn PyImport_Import(_name: *mut PyObject) -> *mut PyObject { + crate::log_stub("PyImport_Import"); + ptr::null_mut() +} diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs new file mode 100644 index 00000000000..6da5d0e04fc --- /dev/null +++ b/crates/capi/src/lib.rs @@ -0,0 +1,33 @@ +use core::ffi::c_long; +pub use rustpython_vm::{PyObject}; + +extern crate alloc; + +pub mod bytesobject; +pub mod import; +pub mod longobject; +pub mod object; +pub mod pyerrors; +pub mod pylifecycle; +pub mod pystate; +pub mod refcount; +pub mod traceback; +pub mod tupleobject; +pub mod unicodeobject; + + +#[repr(C)] +pub struct PyThreadState { + _private: [u8; 0], +} + +#[repr(C)] +pub struct PyLongObject { + ob_base: PyObject, + value: c_long, +} + +#[inline] +pub(crate) fn log_stub(name: &str) { + eprintln!("[rustpython-capi stub] {name} called"); +} diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs new file mode 100644 index 00000000000..586b8c4497a --- /dev/null +++ b/crates/capi/src/longobject.rs @@ -0,0 +1,40 @@ +use core::ffi::{c_long, c_longlong, c_ulong, c_ulonglong}; +use core::ptr; + +use crate::PyObject; + +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_FromLong(_value: c_long) -> *mut PyObject { + crate::log_stub("PyLong_FromLong"); + ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_FromLongLong(_value: c_longlong) -> *mut PyObject { + crate::log_stub("PyLong_FromLongLong"); + ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_FromSsize_t(_value: isize) -> *mut PyObject { + crate::log_stub("PyLong_FromSsize_t"); + ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_FromSize_t(_value: usize) -> *mut PyObject { + crate::log_stub("PyLong_FromSize_t"); + ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_FromUnsignedLong(_value: c_ulong) -> *mut PyObject { + crate::log_stub("PyLong_FromUnsignedLong"); + ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_FromUnsignedLongLong(_value: c_ulonglong) -> *mut PyObject { + crate::log_stub("PyLong_FromUnsignedLongLong"); + ptr::null_mut() +} diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs new file mode 100644 index 00000000000..f457606bac2 --- /dev/null +++ b/crates/capi/src/object.rs @@ -0,0 +1,94 @@ +use std::mem::{transmute, MaybeUninit}; +use std::ptr::NonNull; +use rustpython_vm::{PyObjectRef, VirtualMachine, Context}; +use rustpython_vm::builtins::PyType; +use crate::{PyObject}; + +type PyTypeObject = MaybeUninit<&'static PyType>; + +#[unsafe(no_mangle)] +pub static mut PyType_Type: PyTypeObject = MaybeUninit::uninit(); + +#[unsafe(no_mangle)] +pub static mut PyLong_Type: PyTypeObject = MaybeUninit::uninit(); + +#[unsafe(no_mangle)] +pub static mut PyTuple_Type: PyTypeObject = MaybeUninit::uninit(); + +#[unsafe(no_mangle)] +pub static mut PyUnicode_Type: PyTypeObject = MaybeUninit::uninit(); + +unsafe fn setup_type_pointers(ctx: &Context) { + let zoo = &ctx.types; + + unsafe { + PyType_Type.write(zoo.type_type.payload()); + PyLong_Type.write(zoo.int_type.payload()); + PyTuple_Type.write(zoo.tuple_type.payload()); + PyUnicode_Type.write(zoo.str_type.payload()); + } +} + +#[unsafe(no_mangle)] +#[allow(clippy::not_unsafe_ptr_arg_deref)] +pub extern "C" fn _Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { + if op.is_null() { + return std::ptr::null_mut(); + } + + // SAFETY: op is non-null and expected to be a valid pointer for this shim. + unsafe { transmute((*op).class()) } +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { + _Py_TYPE(op) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyType_GetFlags(_ty: *mut PyTypeObject) -> usize { + crate::log_stub("PyType_GetFlags"); + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyType_GetName(_ty: *mut PyTypeObject) -> *mut PyObject { + crate::log_stub("PyType_GetName"); + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyType_GetQualName(_ty: *mut PyTypeObject) -> *mut PyObject { + crate::log_stub("PyType_GetQualName"); + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_CallNoArgs(_callable: *mut PyObject) -> *mut PyObject { + crate::log_stub("PyObject_CallNoArgs"); + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_GetAttr(_obj: *mut PyObject, _name: *mut PyObject) -> *mut PyObject { + crate::log_stub("PyObject_GetAttr"); + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_Repr(_obj: *mut PyObject) -> *mut PyObject { + crate::log_stub("PyObject_Repr"); + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_Str(_obj: *mut PyObject) -> *mut PyObject { + crate::log_stub("PyObject_Str"); + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_GetConstantBorrowed(_constant_id: core::ffi::c_uint) -> *mut PyObject { + crate::log_stub("Py_GetConstantBorrowed"); + std::ptr::null_mut() +} diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs new file mode 100644 index 00000000000..fba1e4a14b4 --- /dev/null +++ b/crates/capi/src/pyerrors.rs @@ -0,0 +1,58 @@ +use core::ffi::{c_char, c_int}; +use core::ptr; + +use crate::PyObject; + +#[unsafe(no_mangle)] +pub static mut PyExc_BaseException: *mut PyObject = ptr::null_mut(); + +#[unsafe(no_mangle)] +pub static mut PyExc_TypeError: *mut PyObject = ptr::null_mut(); + +#[unsafe(no_mangle)] +pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject { + crate::log_stub("PyErr_GetRaisedException"); + ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyErr_SetRaisedException(_exc: *mut PyObject) { + crate::log_stub("PyErr_SetRaisedException"); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyErr_SetObject(_exception: *mut PyObject, _value: *mut PyObject) { + crate::log_stub("PyErr_SetObject"); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyErr_SetString(_exception: *mut PyObject, _message: *const c_char) { + crate::log_stub("PyErr_SetString"); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyErr_PrintEx(_set_sys_last_vars: c_int) { + crate::log_stub("PyErr_PrintEx"); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyErr_WriteUnraisable(_obj: *mut PyObject) { + crate::log_stub("PyErr_WriteUnraisable"); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyErr_NewExceptionWithDoc( + _name: *const c_char, + _doc: *const c_char, + _base: *mut PyObject, + _dict: *mut PyObject, +) -> *mut PyObject { + crate::log_stub("PyErr_NewExceptionWithDoc"); + ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyException_GetTraceback(_exc: *mut PyObject) -> *mut PyObject { + crate::log_stub("PyException_GetTraceback"); + ptr::null_mut() +} diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs new file mode 100644 index 00000000000..96fbc7b81d5 --- /dev/null +++ b/crates/capi/src/pylifecycle.rs @@ -0,0 +1,49 @@ +use core::ffi::c_int; +use std::cell::RefCell; +use std::mem::ManuallyDrop; +use rustpython_vm::Interpreter; + +thread_local! { + pub static INTERP: RefCell>> = const { RefCell::new(None) }; +} + +#[unsafe(no_mangle)] +pub extern "C-unwind" fn Py_IsInitialized() -> c_int { + INTERP.with(|interp| interp.borrow().is_some() as c_int) +} + +#[unsafe(no_mangle)] +pub extern "C-unwind" fn Py_Initialize() { + Py_InitializeEx(0); +} + +#[unsafe(no_mangle)] +pub extern "C-unwind" fn Py_InitializeEx(_initsigs: c_int) { + if INTERP.with(|interp| interp.borrow().is_none()) { + let interp = Interpreter::with_init(Default::default(), |_vm| { + }); + + INTERP.with(|interp_ref| { + *interp_ref.borrow_mut() = Some(ManuallyDrop::new(interp)); + }); + } +} + +#[unsafe(no_mangle)] +pub extern "C-unwind" fn Py_Finalize() { + let _ = Py_FinalizeEx(); +} + +#[unsafe(no_mangle)] +pub extern "C-unwind" fn Py_FinalizeEx() -> c_int { + INTERP.with(|interp_ref| { + let interp = ManuallyDrop::into_inner(interp_ref.borrow_mut().take() + .expect("Py_FinalizeEx called without an active interpreter")); + interp.finalize(None) + }) as _ +} + +#[unsafe(no_mangle)] +pub extern "C-unwind" fn Py_IsFinalizing() -> c_int { + 0 +} diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs new file mode 100644 index 00000000000..24ce27f6f43 --- /dev/null +++ b/crates/capi/src/pystate.rs @@ -0,0 +1,28 @@ +use crate::{PyThreadState, log_stub}; +use core::ffi::c_int; +use core::ptr; + +#[allow(non_camel_case_types)] +type PyGILState_STATE = c_int; + +#[unsafe(no_mangle)] +pub extern "C" fn PyGILState_Ensure() -> PyGILState_STATE { + log_stub("PyGILState_Ensure"); + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyGILState_Release(_state: PyGILState_STATE) { + log_stub("PyGILState_Release"); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState { + log_stub("PyEval_SaveThread"); + ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyEval_RestoreThread(_tstate: *mut PyThreadState) { + log_stub("PyEval_RestoreThread"); +} diff --git a/crates/capi/src/refcount.rs b/crates/capi/src/refcount.rs new file mode 100644 index 00000000000..1f26304b871 --- /dev/null +++ b/crates/capi/src/refcount.rs @@ -0,0 +1,35 @@ +use alloc::boxed::Box; +use std::ptr::NonNull; +use rustpython_vm::PyObjectRef; +use crate::object::PyLong_Type; +use crate::{PyLongObject, PyObject}; + +#[unsafe(no_mangle)] +#[allow(clippy::not_unsafe_ptr_arg_deref)] +pub extern "C" fn _Py_DecRef(op: *mut PyObject) { + let Some(ptr) = NonNull::new(op) else { + return; + }; + + let owned = unsafe { + PyObjectRef::from_raw(ptr) + }; + + // Dropping so we decrement the refcount + drop(owned); +} + +#[unsafe(no_mangle)] +#[allow(clippy::not_unsafe_ptr_arg_deref)] +pub extern "C" fn _Py_IncRef(op: *mut PyObject) { + if op.is_null() { + return; + } + + // SAFETY: op is non-null and expected to be a valid pointer for this shim. + let owned = unsafe { + (*op).to_owned() + }; + + std::mem::forget(owned); +} diff --git a/crates/capi/src/traceback.rs b/crates/capi/src/traceback.rs new file mode 100644 index 00000000000..c73c952d5e7 --- /dev/null +++ b/crates/capi/src/traceback.rs @@ -0,0 +1,9 @@ +use core::ffi::c_int; + +use crate::PyObject; + +#[unsafe(no_mangle)] +pub extern "C" fn PyTraceBack_Print(_tb: *mut PyObject, _file: *mut PyObject) -> c_int { + crate::log_stub("PyTraceBack_Print"); + -1 +} diff --git a/crates/capi/src/tupleobject.rs b/crates/capi/src/tupleobject.rs new file mode 100644 index 00000000000..996a835bb78 --- /dev/null +++ b/crates/capi/src/tupleobject.rs @@ -0,0 +1,15 @@ +use core::ptr; + +use crate::PyObject; + +#[unsafe(no_mangle)] +pub extern "C" fn PyTuple_Size(_tuple: *mut PyObject) -> isize { + crate::log_stub("PyTuple_Size"); + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyTuple_GetItem(_tuple: *mut PyObject, _pos: isize) -> *mut PyObject { + crate::log_stub("PyTuple_GetItem"); + ptr::null_mut() +} diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs new file mode 100644 index 00000000000..2b4fb081d66 --- /dev/null +++ b/crates/capi/src/unicodeobject.rs @@ -0,0 +1,34 @@ +use core::ffi::c_char; +use core::ptr; + +use crate::PyObject; + +#[unsafe(no_mangle)] +pub extern "C" fn PyUnicode_FromStringAndSize(_s: *const c_char, _len: isize) -> *mut PyObject { + crate::log_stub("PyUnicode_FromStringAndSize"); + ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyUnicode_AsUTF8AndSize( + _unicode: *mut PyObject, + _size: *mut isize, +) -> *const c_char { + crate::log_stub("PyUnicode_AsUTF8AndSize"); + ptr::null() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyUnicode_AsEncodedString( + _unicode: *mut PyObject, + _encoding: *const c_char, + _errors: *const c_char, +) -> *mut PyObject { + crate::log_stub("PyUnicode_AsEncodedString"); + ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyUnicode_InternInPlace(_string: *mut *mut PyObject) { + crate::log_stub("PyUnicode_InternInPlace"); +} diff --git a/example_projects/pyo3_embed/.cargo/config.toml b/example_projects/pyo3_embed/.cargo/config.toml new file mode 100644 index 00000000000..54a884e2458 --- /dev/null +++ b/example_projects/pyo3_embed/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +PYO3_CONFIG_FILE = { value = "pyo3-rustpython.config", relative = true } diff --git a/example_projects/pyo3_embed/Cargo.toml b/example_projects/pyo3_embed/Cargo.toml new file mode 100644 index 00000000000..6174170c143 --- /dev/null +++ b/example_projects/pyo3_embed/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "example_pyo3_embed" +version = "0.1.0" +edition = "2021" + +[dependencies] +pyo3 = { path = "../../../pyo3", default-features = false, features = ["abi3-py314"] } +rustpython-capi = { path = "../../crates/capi" } + +[workspace] + +[patch.crates-io] diff --git a/example_projects/pyo3_embed/README.md b/example_projects/pyo3_embed/README.md new file mode 100644 index 00000000000..7d9cc2cf98b --- /dev/null +++ b/example_projects/pyo3_embed/README.md @@ -0,0 +1,11 @@ +# PyO3 embed against RustPython C-API + +This example demonstrates linking `pyo3` against RustPython's minimal C-API shim (`rustpython-capi`) instead of a system CPython library. + +From this directory, run: + +```shell +cargo run +``` + +The local `.cargo/config.toml` sets `PYO3_CONFIG_FILE` automatically. diff --git a/example_projects/pyo3_embed/build.rs b/example_projects/pyo3_embed/build.rs new file mode 100644 index 00000000000..8d379be42c0 --- /dev/null +++ b/example_projects/pyo3_embed/build.rs @@ -0,0 +1,6 @@ +fn main() { + println!( + "cargo:rustc-link-arg=/Users/basschoenmaeckers/repo/RustPython/target/debug/librustpython_capi.a" + ); + println!("cargo:rustc-link-lib=framework=CoreFoundation"); +} diff --git a/example_projects/pyo3_embed/pyo3-rustpython.config b/example_projects/pyo3_embed/pyo3-rustpython.config new file mode 100644 index 00000000000..2cf06cd886c --- /dev/null +++ b/example_projects/pyo3_embed/pyo3-rustpython.config @@ -0,0 +1,6 @@ +implementation=CPython +version=3.14 +shared=false +abi3=true +build_flags= +suppress_build_script_link_lines=true diff --git a/example_projects/pyo3_embed/src/main.rs b/example_projects/pyo3_embed/src/main.rs new file mode 100644 index 00000000000..72ba168a907 --- /dev/null +++ b/example_projects/pyo3_embed/src/main.rs @@ -0,0 +1,10 @@ +use pyo3::prelude::*; +use pyo3::types::PyInt; + +fn main() { + Python::initialize(); + + Python::attach(|py| { + // let _x = PyInt::new(py, 123); + }); +} From 2250a1c50923847f737eb15df8b73737616f5c48 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 5 Apr 2026 10:36:24 +0200 Subject: [PATCH 002/135] type mapping v1 --- crates/capi/src/object.rs | 51 +++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index f457606bac2..6a382bb7fa2 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,47 +1,56 @@ use std::mem::{transmute, MaybeUninit}; use std::ptr::NonNull; -use rustpython_vm::{PyObjectRef, VirtualMachine, Context}; +use rustpython_vm::{PyObjectRef, VirtualMachine, Context, AsObject}; use rustpython_vm::builtins::PyType; use crate::{PyObject}; +use crate::object::PyTypeObject::*; -type PyTypeObject = MaybeUninit<&'static PyType>; -#[unsafe(no_mangle)] -pub static mut PyType_Type: PyTypeObject = MaybeUninit::uninit(); -#[unsafe(no_mangle)] -pub static mut PyLong_Type: PyTypeObject = MaybeUninit::uninit(); +pub enum PyTypeObject { + Type, + Long, + Tuple, + Unicode, +} #[unsafe(no_mangle)] -pub static mut PyTuple_Type: PyTypeObject = MaybeUninit::uninit(); +pub static mut PyType_Type: PyTypeObject = Type; #[unsafe(no_mangle)] -pub static mut PyUnicode_Type: PyTypeObject = MaybeUninit::uninit(); +pub static mut PyLong_Type: PyTypeObject = Long; -unsafe fn setup_type_pointers(ctx: &Context) { - let zoo = &ctx.types; +#[unsafe(no_mangle)] +pub static mut PyTuple_Type: PyTypeObject = Tuple; - unsafe { - PyType_Type.write(zoo.type_type.payload()); - PyLong_Type.write(zoo.int_type.payload()); - PyTuple_Type.write(zoo.tuple_type.payload()); - PyUnicode_Type.write(zoo.str_type.payload()); - } -} +#[unsafe(no_mangle)] +pub static mut PyUnicode_Type: PyTypeObject = Unicode; #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "C" fn _Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { +pub extern "C-unwind" fn _Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { if op.is_null() { return std::ptr::null_mut(); } - // SAFETY: op is non-null and expected to be a valid pointer for this shim. - unsafe { transmute((*op).class()) } + let zoo = &Context::genesis().types; + let ty = unsafe { (*op).class()}; + + if ty.is(zoo.type_type) { + unsafe { &raw mut PyType_Type } + } else if ty.is(zoo.int_type){ + unsafe { &raw mut PyLong_Type } + } else if ty.is(zoo.tuple_type){ + unsafe { &raw mut PyTuple_Type } + } else if ty.is(zoo.str_type){ + unsafe { &raw mut PyUnicode_Type } + } else { + todo!("Unsupported type: {:?}", ty.name()); + } } #[unsafe(no_mangle)] -pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { +pub extern "C-unwind" fn Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { _Py_TYPE(op) } From bc96211cab0abeead51ba565ed11cd6951788154 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 5 Apr 2026 10:59:08 +0200 Subject: [PATCH 003/135] Type mapping v2 --- crates/capi/src/object.rs | 69 +++++++++++++++++++++++-------------- crates/capi/src/refcount.rs | 4 +-- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 6a382bb7fa2..7f60a8753aa 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,30 +1,46 @@ -use std::mem::{transmute, MaybeUninit}; -use std::ptr::NonNull; -use rustpython_vm::{PyObjectRef, VirtualMachine, Context, AsObject}; +use std::sync::LazyLock; +use rustpython_vm::{Context, AsObject, Py}; use rustpython_vm::builtins::PyType; use crate::{PyObject}; -use crate::object::PyTypeObject::*; -pub enum PyTypeObject { - Type, - Long, - Tuple, - Unicode, +pub struct PyTypeObject { + ty: LazyLock<&'static Py>, +} + +impl PyTypeObject { + const fn new(f: fn() -> &'static Py) -> PyTypeObject + { + PyTypeObject { + ty: LazyLock::new(f), + } + } } #[unsafe(no_mangle)] -pub static mut PyType_Type: PyTypeObject = Type; +pub static mut PyType_Type: PyTypeObject = PyTypeObject::new(|| { + let zoo = &Context::genesis().types; + zoo.type_type +}); #[unsafe(no_mangle)] -pub static mut PyLong_Type: PyTypeObject = Long; +pub static mut PyLong_Type: PyTypeObject = PyTypeObject::new(|| { + let zoo = &Context::genesis().types; + zoo.int_type +}); #[unsafe(no_mangle)] -pub static mut PyTuple_Type: PyTypeObject = Tuple; +pub static mut PyTuple_Type: PyTypeObject = PyTypeObject::new(|| { + let zoo = &Context::genesis().types; + zoo.tuple_type +}); #[unsafe(no_mangle)] -pub static mut PyUnicode_Type: PyTypeObject = Unicode; +pub static mut PyUnicode_Type: PyTypeObject = PyTypeObject::new(|| { + let zoo = &Context::genesis().types; + zoo.union_type +}); #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] @@ -33,20 +49,21 @@ pub extern "C-unwind" fn _Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { return std::ptr::null_mut(); } - let zoo = &Context::genesis().types; - let ty = unsafe { (*op).class()}; - - if ty.is(zoo.type_type) { - unsafe { &raw mut PyType_Type } - } else if ty.is(zoo.int_type){ - unsafe { &raw mut PyLong_Type } - } else if ty.is(zoo.tuple_type){ - unsafe { &raw mut PyTuple_Type } - } else if ty.is(zoo.str_type){ - unsafe { &raw mut PyUnicode_Type } - } else { - todo!("Unsupported type: {:?}", ty.name()); + unsafe { + let ty = (*op).class(); + if ty.is(*PyTuple_Type.ty) { + &raw mut PyType_Type + } else if ty.is(*PyTuple_Type.ty){ + &raw mut PyLong_Type + } else if ty.is(*PyTuple_Type.ty){ + &raw mut PyTuple_Type + } else if ty.is(*PyTuple_Type.ty){ + &raw mut PyUnicode_Type + } else { + todo!("Unsupported type: {:?}", ty.name()); + } } + } #[unsafe(no_mangle)] diff --git a/crates/capi/src/refcount.rs b/crates/capi/src/refcount.rs index 1f26304b871..76652175617 100644 --- a/crates/capi/src/refcount.rs +++ b/crates/capi/src/refcount.rs @@ -1,8 +1,6 @@ -use alloc::boxed::Box; use std::ptr::NonNull; use rustpython_vm::PyObjectRef; -use crate::object::PyLong_Type; -use crate::{PyLongObject, PyObject}; +use crate::{PyObject}; #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] From 46307f6cebc83d6b1cdf850e551b9f6f8ae4d161 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 5 Apr 2026 11:18:11 +0200 Subject: [PATCH 004/135] Implement type flags --- crates/capi/src/longobject.rs | 17 ++++- crates/capi/src/object.rs | 97 ++++++++++++++++--------- example_projects/pyo3_embed/src/main.rs | 5 +- 3 files changed, 81 insertions(+), 38 deletions(-) diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index 586b8c4497a..a7d620908b7 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -2,11 +2,22 @@ use core::ffi::{c_long, c_longlong, c_ulong, c_ulonglong}; use core::ptr; use crate::PyObject; +use crate::pylifecycle::INTERP; +use rustpython_vm::PyObjectRef; #[unsafe(no_mangle)] -pub extern "C" fn PyLong_FromLong(_value: c_long) -> *mut PyObject { - crate::log_stub("PyLong_FromLong"); - ptr::null_mut() +pub extern "C" fn PyLong_FromLong(value: c_long) -> *mut PyObject { + INTERP.with(|interp_ref| { + let interp = interp_ref.borrow(); + let interp = interp + .as_ref() + .expect("PyLong_FromLong called before Py_InitializeEx"); + + interp.enter(|vm| { + let obj: PyObjectRef = vm.ctx.new_int(value).into(); + obj.into_raw().as_ptr() + }) + }) } #[unsafe(no_mangle)] diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 7f60a8753aa..1e14f4c3f1b 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,46 +1,73 @@ -use std::sync::LazyLock; -use rustpython_vm::{Context, AsObject, Py}; -use rustpython_vm::builtins::PyType; -use crate::{PyObject}; - +use core::ffi::c_ulong; +use crate::PyObject; +use rustpython_vm::builtins::PyType; +use rustpython_vm::{AsObject, Context, Py}; +use std::sync::LazyLock; -pub struct PyTypeObject { +pub struct PyTypeObject { ty: LazyLock<&'static Py>, + flags: c_ulong, } impl PyTypeObject { - const fn new(f: fn() -> &'static Py) -> PyTypeObject - { + const fn new(f: fn() -> &'static Py, flags: c_ulong) -> PyTypeObject { PyTypeObject { ty: LazyLock::new(f), + flags, } } } +const PY_TPFLAGS_HAVE_STACKLESS_EXTENSION: c_ulong = 0; +const PY_TPFLAGS_HAVE_VERSION_TAG: c_ulong = 1 << 18; +const PY_TPFLAGS_DEFAULT: c_ulong = + PY_TPFLAGS_HAVE_STACKLESS_EXTENSION | PY_TPFLAGS_HAVE_VERSION_TAG; +const PY_TPFLAGS_IMMUTABLETYPE: c_ulong = 1 << 8; +const PY_TPFLAGS_BASETYPE: c_ulong = 1 << 10; +const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; +const PY_TPFLAGS_TUPLE_SUBCLASS: c_ulong = 1 << 26; +const PY_TPFLAGS_UNICODE_SUBCLASS: c_ulong = 1 << 28; +const PY_TPFLAGS_TYPE_SUBCLASS: c_ulong = 1 << 31; + #[unsafe(no_mangle)] -pub static mut PyType_Type: PyTypeObject = PyTypeObject::new(|| { - let zoo = &Context::genesis().types; - zoo.type_type -}); +pub static mut PyType_Type: PyTypeObject = PyTypeObject::new( + || { + let zoo = &Context::genesis().types; + zoo.type_type + }, + PY_TPFLAGS_DEFAULT | PY_TPFLAGS_IMMUTABLETYPE | PY_TPFLAGS_BASETYPE | PY_TPFLAGS_TYPE_SUBCLASS, +); #[unsafe(no_mangle)] -pub static mut PyLong_Type: PyTypeObject = PyTypeObject::new(|| { - let zoo = &Context::genesis().types; - zoo.int_type -}); +pub static mut PyLong_Type: PyTypeObject = PyTypeObject::new( + || { + let zoo = &Context::genesis().types; + zoo.int_type + }, + PY_TPFLAGS_DEFAULT | PY_TPFLAGS_IMMUTABLETYPE | PY_TPFLAGS_BASETYPE | PY_TPFLAGS_LONG_SUBCLASS, +); #[unsafe(no_mangle)] -pub static mut PyTuple_Type: PyTypeObject = PyTypeObject::new(|| { - let zoo = &Context::genesis().types; - zoo.tuple_type -}); +pub static mut PyTuple_Type: PyTypeObject = PyTypeObject::new( + || { + let zoo = &Context::genesis().types; + zoo.tuple_type + }, + PY_TPFLAGS_DEFAULT | PY_TPFLAGS_IMMUTABLETYPE | PY_TPFLAGS_BASETYPE | PY_TPFLAGS_TUPLE_SUBCLASS, +); #[unsafe(no_mangle)] -pub static mut PyUnicode_Type: PyTypeObject = PyTypeObject::new(|| { - let zoo = &Context::genesis().types; - zoo.union_type -}); +pub static mut PyUnicode_Type: PyTypeObject = PyTypeObject::new( + || { + let zoo = &Context::genesis().types; + zoo.str_type + }, + PY_TPFLAGS_DEFAULT + | PY_TPFLAGS_IMMUTABLETYPE + | PY_TPFLAGS_BASETYPE + | PY_TPFLAGS_UNICODE_SUBCLASS, +); #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] @@ -51,19 +78,18 @@ pub extern "C-unwind" fn _Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { unsafe { let ty = (*op).class(); - if ty.is(*PyTuple_Type.ty) { - &raw mut PyType_Type - } else if ty.is(*PyTuple_Type.ty){ + if ty.is(*PyType_Type.ty) { + &raw mut PyType_Type + } else if ty.is(*PyLong_Type.ty) { &raw mut PyLong_Type - } else if ty.is(*PyTuple_Type.ty){ + } else if ty.is(*PyTuple_Type.ty) { &raw mut PyTuple_Type - } else if ty.is(*PyTuple_Type.ty){ + } else if ty.is(*PyUnicode_Type.ty) { &raw mut PyUnicode_Type } else { todo!("Unsupported type: {:?}", ty.name()); } } - } #[unsafe(no_mangle)] @@ -72,9 +98,14 @@ pub extern "C-unwind" fn Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { } #[unsafe(no_mangle)] -pub extern "C" fn PyType_GetFlags(_ty: *mut PyTypeObject) -> usize { - crate::log_stub("PyType_GetFlags"); - 0 +#[allow(clippy::not_unsafe_ptr_arg_deref)] +pub extern "C-unwind" fn PyType_GetFlags(ty: *mut PyTypeObject) -> c_ulong { + if ty.is_null() { + panic!("PyType_GetFlags called with null type pointer"); + } + + // SAFETY: caller guarantees this is a valid exported type object pointer. + unsafe { (*ty).flags } } #[unsafe(no_mangle)] diff --git a/example_projects/pyo3_embed/src/main.rs b/example_projects/pyo3_embed/src/main.rs index 72ba168a907..8c24df57817 100644 --- a/example_projects/pyo3_embed/src/main.rs +++ b/example_projects/pyo3_embed/src/main.rs @@ -1,10 +1,11 @@ use pyo3::prelude::*; -use pyo3::types::PyInt; +use pyo3::types::{PyInt, PyString}; fn main() { Python::initialize(); Python::attach(|py| { - // let _x = PyInt::new(py, 123); + let number = PyInt::new(py, 123); + assert!(number.is_instance_of::()); }); } From b08d41ab2703917e9a67b7240a0d6fc5f42413bf Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 5 Apr 2026 11:22:46 +0200 Subject: [PATCH 005/135] Implement `PyUnicode_FromStringAndSize` --- crates/capi/src/unicodeobject.rs | 31 ++++++++++++++++++++++--- example_projects/pyo3_embed/src/main.rs | 3 +++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index 2b4fb081d66..0d61b4da465 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -1,12 +1,37 @@ use core::ffi::c_char; use core::ptr; +use core::slice; +use core::str; use crate::PyObject; +use crate::pylifecycle::INTERP; +use rustpython_vm::PyObjectRef; #[unsafe(no_mangle)] -pub extern "C" fn PyUnicode_FromStringAndSize(_s: *const c_char, _len: isize) -> *mut PyObject { - crate::log_stub("PyUnicode_FromStringAndSize"); - ptr::null_mut() +pub extern "C" fn PyUnicode_FromStringAndSize(s: *const c_char, len: isize) -> *mut PyObject { + let len = usize::try_from(len).expect("PyUnicode_FromStringAndSize called with negative len"); + let text = if s.is_null() { + if len != 0 { + panic!("PyUnicode_FromStringAndSize called with null data and non-zero len"); + } + "" + } else { + // SAFETY: caller passes a valid C buffer of length `len`. + let bytes = unsafe { slice::from_raw_parts(s.cast::(), len) }; + str::from_utf8(bytes).expect("PyUnicode_FromStringAndSize got non-UTF8 data") + }; + + INTERP.with(|interp_ref| { + let interp = interp_ref.borrow(); + let interp = interp + .as_ref() + .expect("PyUnicode_FromStringAndSize called before Py_InitializeEx"); + + interp.enter(|vm| { + let obj: PyObjectRef = vm.ctx.new_str(text).into(); + obj.into_raw().as_ptr() + }) + }) } #[unsafe(no_mangle)] diff --git a/example_projects/pyo3_embed/src/main.rs b/example_projects/pyo3_embed/src/main.rs index 8c24df57817..892ea65b94e 100644 --- a/example_projects/pyo3_embed/src/main.rs +++ b/example_projects/pyo3_embed/src/main.rs @@ -7,5 +7,8 @@ fn main() { Python::attach(|py| { let number = PyInt::new(py, 123); assert!(number.is_instance_of::()); + + let string = PyString::new(py, "Hello, World!"); + assert!(string.is_instance_of::()); }); } From 34c13f496a134128f153ca5bb60316231e8537ae Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 5 Apr 2026 11:26:02 +0200 Subject: [PATCH 006/135] Do not use `C-unwind` --- crates/capi/src/object.rs | 6 +++--- crates/capi/src/pylifecycle.rs | 25 ++++++++++++++----------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 1e14f4c3f1b..e09ecdcd74f 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -71,7 +71,7 @@ pub static mut PyUnicode_Type: PyTypeObject = PyTypeObject::new( #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "C-unwind" fn _Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { +pub extern "C" fn _Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { if op.is_null() { return std::ptr::null_mut(); } @@ -93,13 +93,13 @@ pub extern "C-unwind" fn _Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { } #[unsafe(no_mangle)] -pub extern "C-unwind" fn Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { +pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { _Py_TYPE(op) } #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "C-unwind" fn PyType_GetFlags(ty: *mut PyTypeObject) -> c_ulong { +pub extern "C" fn PyType_GetFlags(ty: *mut PyTypeObject) -> c_ulong { if ty.is_null() { panic!("PyType_GetFlags called with null type pointer"); } diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 96fbc7b81d5..b70fe05fba9 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,27 +1,26 @@ use core::ffi::c_int; +use rustpython_vm::Interpreter; use std::cell::RefCell; use std::mem::ManuallyDrop; -use rustpython_vm::Interpreter; thread_local! { pub static INTERP: RefCell>> = const { RefCell::new(None) }; } #[unsafe(no_mangle)] -pub extern "C-unwind" fn Py_IsInitialized() -> c_int { +pub extern "C" fn Py_IsInitialized() -> c_int { INTERP.with(|interp| interp.borrow().is_some() as c_int) } #[unsafe(no_mangle)] -pub extern "C-unwind" fn Py_Initialize() { +pub extern "C" fn Py_Initialize() { Py_InitializeEx(0); } #[unsafe(no_mangle)] -pub extern "C-unwind" fn Py_InitializeEx(_initsigs: c_int) { +pub extern "C" fn Py_InitializeEx(_initsigs: c_int) { if INTERP.with(|interp| interp.borrow().is_none()) { - let interp = Interpreter::with_init(Default::default(), |_vm| { - }); + let interp = Interpreter::with_init(Default::default(), |_vm| {}); INTERP.with(|interp_ref| { *interp_ref.borrow_mut() = Some(ManuallyDrop::new(interp)); @@ -30,20 +29,24 @@ pub extern "C-unwind" fn Py_InitializeEx(_initsigs: c_int) { } #[unsafe(no_mangle)] -pub extern "C-unwind" fn Py_Finalize() { +pub extern "C" fn Py_Finalize() { let _ = Py_FinalizeEx(); } #[unsafe(no_mangle)] -pub extern "C-unwind" fn Py_FinalizeEx() -> c_int { +pub extern "C" fn Py_FinalizeEx() -> c_int { INTERP.with(|interp_ref| { - let interp = ManuallyDrop::into_inner(interp_ref.borrow_mut().take() - .expect("Py_FinalizeEx called without an active interpreter")); + let interp = ManuallyDrop::into_inner( + interp_ref + .borrow_mut() + .take() + .expect("Py_FinalizeEx called without an active interpreter"), + ); interp.finalize(None) }) as _ } #[unsafe(no_mangle)] -pub extern "C-unwind" fn Py_IsFinalizing() -> c_int { +pub extern "C" fn Py_IsFinalizing() -> c_int { 0 } From 24b2c6384b82aa3ad5ac54154f854c7330e953c2 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 5 Apr 2026 11:41:57 +0200 Subject: [PATCH 007/135] implement `PyLong_AsLong` --- Cargo.lock | 1 + crates/capi/Cargo.toml | 1 + crates/capi/src/longobject.rs | 28 +++++++++++++++++++++++++ crates/capi/src/object.rs | 8 +------ crates/capi/src/pyerrors.rs | 3 +++ example_projects/pyo3_embed/src/main.rs | 1 + 6 files changed, 35 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1eca00b9fbb..5d534fb6651 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3108,6 +3108,7 @@ dependencies = [ name = "rustpython-capi" version = "0.5.0" dependencies = [ + "num-traits", "rustpython-vm", ] diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index bf3bc8ca285..422b47aaa31 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true crate-type = ["staticlib"] [dependencies] +num-traits = { workspace = true } rustpython-vm = { workspace = true } [lints] diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index a7d620908b7..8c4cb3df1fb 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -4,6 +4,7 @@ use core::ptr; use crate::PyObject; use crate::pylifecycle::INTERP; use rustpython_vm::PyObjectRef; +use rustpython_vm::builtins::PyInt; #[unsafe(no_mangle)] pub extern "C" fn PyLong_FromLong(value: c_long) -> *mut PyObject { @@ -49,3 +50,30 @@ pub extern "C" fn PyLong_FromUnsignedLongLong(_value: c_ulonglong) -> *mut PyObj crate::log_stub("PyLong_FromUnsignedLongLong"); ptr::null_mut() } + +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_AsLong(obj: *mut PyObject) -> c_long { + if obj.is_null() { + panic!("PyLong_AsLong called with null object"); + } + + INTERP.with(|interp_ref| { + let interp = interp_ref.borrow(); + let interp = interp + .as_ref() + .expect("PyLong_AsLong called before Py_InitializeEx"); + + interp.enter(|_vm| { + // SAFETY: non-null checked above; caller promises a valid PyObject pointer. + let obj_ref = unsafe { &*obj }; + let int_obj = obj_ref + .downcast_ref::() + .expect("PyLong_AsLong currently only accepts int instances"); + + int_obj + .as_bigint() + .try_into() + .expect("PyLong_AsLong: value out of range for c_long") + }) + }) +} diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index e09ecdcd74f..f041d2774e3 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -70,8 +70,7 @@ pub static mut PyUnicode_Type: PyTypeObject = PyTypeObject::new( ); #[unsafe(no_mangle)] -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "C" fn _Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { +pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { if op.is_null() { return std::ptr::null_mut(); } @@ -92,11 +91,6 @@ pub extern "C" fn _Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { } } -#[unsafe(no_mangle)] -pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { - _Py_TYPE(op) -} - #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] pub extern "C" fn PyType_GetFlags(ty: *mut PyTypeObject) -> c_ulong { diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index fba1e4a14b4..3a567bcfece 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -9,6 +9,9 @@ pub static mut PyExc_BaseException: *mut PyObject = ptr::null_mut(); #[unsafe(no_mangle)] pub static mut PyExc_TypeError: *mut PyObject = ptr::null_mut(); +#[unsafe(no_mangle)] +pub static mut PyExc_OverflowError: *mut PyObject = ptr::null_mut(); + #[unsafe(no_mangle)] pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject { crate::log_stub("PyErr_GetRaisedException"); diff --git a/example_projects/pyo3_embed/src/main.rs b/example_projects/pyo3_embed/src/main.rs index 892ea65b94e..ca79a80340a 100644 --- a/example_projects/pyo3_embed/src/main.rs +++ b/example_projects/pyo3_embed/src/main.rs @@ -7,6 +7,7 @@ fn main() { Python::attach(|py| { let number = PyInt::new(py, 123); assert!(number.is_instance_of::()); + assert_eq!(number.extract::().unwrap(), 123); let string = PyString::new(py, "Hello, World!"); assert!(string.is_instance_of::()); From 384b378a63d30c360c6b19efafd77b3c7ec8928a Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 5 Apr 2026 11:48:52 +0200 Subject: [PATCH 008/135] Add `with_vm` helper --- crates/capi/src/longobject.rs | 42 +++++++++++--------------------- crates/capi/src/pylifecycle.rs | 9 +++++++ crates/capi/src/unicodeobject.rs | 15 +++--------- 3 files changed, 27 insertions(+), 39 deletions(-) diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index 8c4cb3df1fb..8af4ed9e51e 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -2,22 +2,15 @@ use core::ffi::{c_long, c_longlong, c_ulong, c_ulonglong}; use core::ptr; use crate::PyObject; -use crate::pylifecycle::INTERP; +use crate::pylifecycle::with_vm; use rustpython_vm::PyObjectRef; use rustpython_vm::builtins::PyInt; #[unsafe(no_mangle)] pub extern "C" fn PyLong_FromLong(value: c_long) -> *mut PyObject { - INTERP.with(|interp_ref| { - let interp = interp_ref.borrow(); - let interp = interp - .as_ref() - .expect("PyLong_FromLong called before Py_InitializeEx"); - - interp.enter(|vm| { - let obj: PyObjectRef = vm.ctx.new_int(value).into(); - obj.into_raw().as_ptr() - }) + with_vm(|vm| { + let obj: PyObjectRef = vm.ctx.new_int(value).into(); + obj.into_raw().as_ptr() }) } @@ -57,23 +50,16 @@ pub extern "C" fn PyLong_AsLong(obj: *mut PyObject) -> c_long { panic!("PyLong_AsLong called with null object"); } - INTERP.with(|interp_ref| { - let interp = interp_ref.borrow(); - let interp = interp - .as_ref() - .expect("PyLong_AsLong called before Py_InitializeEx"); - - interp.enter(|_vm| { - // SAFETY: non-null checked above; caller promises a valid PyObject pointer. - let obj_ref = unsafe { &*obj }; - let int_obj = obj_ref - .downcast_ref::() - .expect("PyLong_AsLong currently only accepts int instances"); + with_vm(|_vm| { + // SAFETY: non-null checked above; caller promises a valid PyObject pointer. + let obj_ref = unsafe { &*obj }; + let int_obj = obj_ref + .downcast_ref::() + .expect("PyLong_AsLong currently only accepts int instances"); - int_obj - .as_bigint() - .try_into() - .expect("PyLong_AsLong: value out of range for c_long") - }) + int_obj + .as_bigint() + .try_into() + .expect("PyLong_AsLong: value out of range for c_long") }) } diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index b70fe05fba9..d1a27ec4381 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,5 +1,6 @@ use core::ffi::c_int; use rustpython_vm::Interpreter; +use rustpython_vm::VirtualMachine; use std::cell::RefCell; use std::mem::ManuallyDrop; @@ -7,6 +8,14 @@ thread_local! { pub static INTERP: RefCell>> = const { RefCell::new(None) }; } +pub(crate) fn with_vm(f: impl FnOnce(&VirtualMachine) -> R) -> R { + INTERP.with(|interp_ref| { + let interp = interp_ref.borrow(); + let interp = interp.as_ref().expect("VM access before Py_InitializeEx"); + interp.enter(f) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn Py_IsInitialized() -> c_int { INTERP.with(|interp| interp.borrow().is_some() as c_int) diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index 0d61b4da465..4a241f6c91f 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -4,7 +4,7 @@ use core::slice; use core::str; use crate::PyObject; -use crate::pylifecycle::INTERP; +use crate::pylifecycle::with_vm; use rustpython_vm::PyObjectRef; #[unsafe(no_mangle)] @@ -21,16 +21,9 @@ pub extern "C" fn PyUnicode_FromStringAndSize(s: *const c_char, len: isize) -> * str::from_utf8(bytes).expect("PyUnicode_FromStringAndSize got non-UTF8 data") }; - INTERP.with(|interp_ref| { - let interp = interp_ref.borrow(); - let interp = interp - .as_ref() - .expect("PyUnicode_FromStringAndSize called before Py_InitializeEx"); - - interp.enter(|vm| { - let obj: PyObjectRef = vm.ctx.new_str(text).into(); - obj.into_raw().as_ptr() - }) + with_vm(|vm| { + let obj: PyObjectRef = vm.ctx.new_str(text).into(); + obj.into_raw().as_ptr() }) } From f49e8592f9c1c415be54d62b7054b61f8852d8f8 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 5 Apr 2026 12:03:44 +0200 Subject: [PATCH 009/135] Move `PyThreadState` to `pystate.rs` --- crates/capi/src/lib.rs | 15 +-------------- crates/capi/src/pystate.rs | 7 ++++++- crates/capi/src/refcount.rs | 12 ++++-------- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 6da5d0e04fc..7080bdbae49 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -1,5 +1,4 @@ -use core::ffi::c_long; -pub use rustpython_vm::{PyObject}; +pub use rustpython_vm::PyObject; extern crate alloc; @@ -15,18 +14,6 @@ pub mod traceback; pub mod tupleobject; pub mod unicodeobject; - -#[repr(C)] -pub struct PyThreadState { - _private: [u8; 0], -} - -#[repr(C)] -pub struct PyLongObject { - ob_base: PyObject, - value: c_long, -} - #[inline] pub(crate) fn log_stub(name: &str) { eprintln!("[rustpython-capi stub] {name} called"); diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index 24ce27f6f43..9879348f8a7 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -1,10 +1,15 @@ -use crate::{PyThreadState, log_stub}; +use crate::log_stub; use core::ffi::c_int; use core::ptr; #[allow(non_camel_case_types)] type PyGILState_STATE = c_int; +#[repr(C)] +pub struct PyThreadState { + _interp: *mut std::ffi::c_void, +} + #[unsafe(no_mangle)] pub extern "C" fn PyGILState_Ensure() -> PyGILState_STATE { log_stub("PyGILState_Ensure"); diff --git a/crates/capi/src/refcount.rs b/crates/capi/src/refcount.rs index 76652175617..5e6f55f0460 100644 --- a/crates/capi/src/refcount.rs +++ b/crates/capi/src/refcount.rs @@ -1,6 +1,6 @@ -use std::ptr::NonNull; +use crate::PyObject; use rustpython_vm::PyObjectRef; -use crate::{PyObject}; +use std::ptr::NonNull; #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] @@ -9,9 +9,7 @@ pub extern "C" fn _Py_DecRef(op: *mut PyObject) { return; }; - let owned = unsafe { - PyObjectRef::from_raw(ptr) - }; + let owned = unsafe { PyObjectRef::from_raw(ptr) }; // Dropping so we decrement the refcount drop(owned); @@ -25,9 +23,7 @@ pub extern "C" fn _Py_IncRef(op: *mut PyObject) { } // SAFETY: op is non-null and expected to be a valid pointer for this shim. - let owned = unsafe { - (*op).to_owned() - }; + let owned = unsafe { (*op).to_owned() }; std::mem::forget(owned); } From e18eef7684987995c8fa0b5991de3cb4f7f9233b Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 5 Apr 2026 13:33:05 +0200 Subject: [PATCH 010/135] Basic multi-threading support --- crates/capi/Cargo.toml | 2 +- crates/capi/src/longobject.rs | 2 +- crates/capi/src/pylifecycle.rs | 61 ++++++++++++++----------- crates/capi/src/pystate.rs | 28 ++++++++++-- crates/capi/src/unicodeobject.rs | 2 +- example_projects/pyo3_embed/src/main.rs | 8 ++++ 6 files changed, 68 insertions(+), 35 deletions(-) diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index 422b47aaa31..5a55536902b 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -13,7 +13,7 @@ crate-type = ["staticlib"] [dependencies] num-traits = { workspace = true } -rustpython-vm = { workspace = true } +rustpython-vm = { workspace = true, features = ["threading"]} [lints] workspace = true diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index 8af4ed9e51e..efaa0fc2cfc 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -2,7 +2,7 @@ use core::ffi::{c_long, c_longlong, c_ulong, c_ulonglong}; use core::ptr; use crate::PyObject; -use crate::pylifecycle::with_vm; +use crate::pystate::with_vm; use rustpython_vm::PyObjectRef; use rustpython_vm::builtins::PyInt; diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index d1a27ec4381..2a6277ef95a 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,24 +1,26 @@ +use crate::log_stub; use core::ffi::c_int; use rustpython_vm::Interpreter; -use rustpython_vm::VirtualMachine; -use std::cell::RefCell; -use std::mem::ManuallyDrop; +use rustpython_vm::vm::thread::ThreadedVirtualMachine; +use std::sync::{Once, OnceLock, mpsc}; -thread_local! { - pub static INTERP: RefCell>> = const { RefCell::new(None) }; -} +static VM_REQUEST_TX: OnceLock>> = + OnceLock::new(); +static INITIALIZED: Once = Once::new(); -pub(crate) fn with_vm(f: impl FnOnce(&VirtualMachine) -> R) -> R { - INTERP.with(|interp_ref| { - let interp = interp_ref.borrow(); - let interp = interp.as_ref().expect("VM access before Py_InitializeEx"); - interp.enter(f) - }) +/// Request a vm from the main interpreter +pub(crate) fn request_vm_from_interpreter() -> ThreadedVirtualMachine { + let tx = VM_REQUEST_TX + .get() + .expect("VM request channel not initialized"); + let (response_tx, response_rx) = mpsc::channel(); + tx.send(response_tx).expect("Failed to send VM request"); + response_rx.recv().expect("Failed to receive VM response") } #[unsafe(no_mangle)] pub extern "C" fn Py_IsInitialized() -> c_int { - INTERP.with(|interp| interp.borrow().is_some() as c_int) + INITIALIZED.is_completed() as _ } #[unsafe(no_mangle)] @@ -28,13 +30,25 @@ pub extern "C" fn Py_Initialize() { #[unsafe(no_mangle)] pub extern "C" fn Py_InitializeEx(_initsigs: c_int) { - if INTERP.with(|interp| interp.borrow().is_none()) { - let interp = Interpreter::with_init(Default::default(), |_vm| {}); + if INITIALIZED.is_completed() { + panic!("Initialize called multiple times"); + } + + INITIALIZED.call_once(|| { + let (tx, rx) = mpsc::channel(); + VM_REQUEST_TX.set(tx).expect("VM request channel was already initialized"); - INTERP.with(|interp_ref| { - *interp_ref.borrow_mut() = Some(ManuallyDrop::new(interp)); + std::thread::spawn(move || { + let interp = Interpreter::with_init(Default::default(), |_vm| {}); + interp.enter(|vm| { + while let Ok(request) = rx.recv() { + request + .send(vm.new_thread()) + .expect("Failed to send VM response"); + } + }) }); - } + }); } #[unsafe(no_mangle)] @@ -44,15 +58,8 @@ pub extern "C" fn Py_Finalize() { #[unsafe(no_mangle)] pub extern "C" fn Py_FinalizeEx() -> c_int { - INTERP.with(|interp_ref| { - let interp = ManuallyDrop::into_inner( - interp_ref - .borrow_mut() - .take() - .expect("Py_FinalizeEx called without an active interpreter"), - ); - interp.finalize(None) - }) as _ + log_stub("Py_FinalizeEx"); + 0 } #[unsafe(no_mangle)] diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index 9879348f8a7..bedcc610dd8 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -1,6 +1,23 @@ -use crate::log_stub; +use crate::pylifecycle::request_vm_from_interpreter; use core::ffi::c_int; use core::ptr; +use rustpython_vm::VirtualMachine; +use rustpython_vm::vm::thread::ThreadedVirtualMachine; +use std::cell::RefCell; + +thread_local! { + static VM: RefCell> = const { RefCell::new(None) }; +} + +pub fn with_vm(f: impl FnOnce(&VirtualMachine) -> R) -> R { + VM.with(|vm_ref| { + let vm = vm_ref.borrow(); + let vm = vm + .as_ref() + .expect("Thread was not attached to an interpreter"); + vm.run(f) + }) +} #[allow(non_camel_case_types)] type PyGILState_STATE = c_int; @@ -12,22 +29,23 @@ pub struct PyThreadState { #[unsafe(no_mangle)] pub extern "C" fn PyGILState_Ensure() -> PyGILState_STATE { - log_stub("PyGILState_Ensure"); + VM.with(|vm| { + vm.borrow_mut() + .get_or_insert_with(|| request_vm_from_interpreter()); + }); + 0 } #[unsafe(no_mangle)] pub extern "C" fn PyGILState_Release(_state: PyGILState_STATE) { - log_stub("PyGILState_Release"); } #[unsafe(no_mangle)] pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState { - log_stub("PyEval_SaveThread"); ptr::null_mut() } #[unsafe(no_mangle)] pub extern "C" fn PyEval_RestoreThread(_tstate: *mut PyThreadState) { - log_stub("PyEval_RestoreThread"); } diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index 4a241f6c91f..27240521110 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -4,7 +4,7 @@ use core::slice; use core::str; use crate::PyObject; -use crate::pylifecycle::with_vm; +use crate::pystate::with_vm; use rustpython_vm::PyObjectRef; #[unsafe(no_mangle)] diff --git a/example_projects/pyo3_embed/src/main.rs b/example_projects/pyo3_embed/src/main.rs index ca79a80340a..3ba2c549411 100644 --- a/example_projects/pyo3_embed/src/main.rs +++ b/example_projects/pyo3_embed/src/main.rs @@ -11,5 +11,13 @@ fn main() { let string = PyString::new(py, "Hello, World!"); assert!(string.is_instance_of::()); + + let number = number.unbind(); + std::thread::spawn(move || { + Python::attach(|py| { + let number = number.bind(py); + assert!(number.is_instance_of::()); + }); + }).join().unwrap(); }); } From 565bf5ebfbc8c56ad5a450832fdbd85b997c41ac Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 5 Apr 2026 14:07:16 +0200 Subject: [PATCH 011/135] Cleanup --- Cargo.lock | 1 - crates/capi/Cargo.toml | 1 - example_projects/pyo3_embed/Cargo.toml | 2 +- example_projects/pyo3_embed/build.rs | 6 ------ example_projects/pyo3_embed/pyo3-rustpython.config | 3 +++ 5 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 example_projects/pyo3_embed/build.rs diff --git a/Cargo.lock b/Cargo.lock index 5d534fb6651..1eca00b9fbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3108,7 +3108,6 @@ dependencies = [ name = "rustpython-capi" version = "0.5.0" dependencies = [ - "num-traits", "rustpython-vm", ] diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index 5a55536902b..964a996143a 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -12,7 +12,6 @@ license.workspace = true crate-type = ["staticlib"] [dependencies] -num-traits = { workspace = true } rustpython-vm = { workspace = true, features = ["threading"]} [lints] diff --git a/example_projects/pyo3_embed/Cargo.toml b/example_projects/pyo3_embed/Cargo.toml index 6174170c143..d424a486184 100644 --- a/example_projects/pyo3_embed/Cargo.toml +++ b/example_projects/pyo3_embed/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -pyo3 = { path = "../../../pyo3", default-features = false, features = ["abi3-py314"] } +pyo3 = { version = "0.28.3", features = ["abi3-py314"] } rustpython-capi = { path = "../../crates/capi" } [workspace] diff --git a/example_projects/pyo3_embed/build.rs b/example_projects/pyo3_embed/build.rs deleted file mode 100644 index 8d379be42c0..00000000000 --- a/example_projects/pyo3_embed/build.rs +++ /dev/null @@ -1,6 +0,0 @@ -fn main() { - println!( - "cargo:rustc-link-arg=/Users/basschoenmaeckers/repo/RustPython/target/debug/librustpython_capi.a" - ); - println!("cargo:rustc-link-lib=framework=CoreFoundation"); -} diff --git a/example_projects/pyo3_embed/pyo3-rustpython.config b/example_projects/pyo3_embed/pyo3-rustpython.config index 2cf06cd886c..2b097508e4f 100644 --- a/example_projects/pyo3_embed/pyo3-rustpython.config +++ b/example_projects/pyo3_embed/pyo3-rustpython.config @@ -4,3 +4,6 @@ shared=false abi3=true build_flags= suppress_build_script_link_lines=true +extra_build_script_line=cargo:rustc-link-search=native=/Users/basschoenmaeckers/repo/RustPython/target/debug +extra_build_script_line=cargo:rustc-link-lib=static=rustpython_capi +extra_build_script_line=cargo:rustc-link-lib=framework=CoreFoundation From 85b292811971ee6bbda46049d219ff4b374d99d5 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 5 Apr 2026 14:53:57 +0200 Subject: [PATCH 012/135] Attach thread in `Py_InitializeEx` --- crates/capi/src/pylifecycle.rs | 7 ++++++- crates/capi/src/pystate.rs | 14 ++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 2a6277ef95a..d8b3fc2ae41 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,4 +1,5 @@ use crate::log_stub; +use crate::pystate::attach_vm_to_thread; use core::ffi::c_int; use rustpython_vm::Interpreter; use rustpython_vm::vm::thread::ThreadedVirtualMachine; @@ -36,7 +37,9 @@ pub extern "C" fn Py_InitializeEx(_initsigs: c_int) { INITIALIZED.call_once(|| { let (tx, rx) = mpsc::channel(); - VM_REQUEST_TX.set(tx).expect("VM request channel was already initialized"); + VM_REQUEST_TX + .set(tx) + .expect("VM request channel was already initialized"); std::thread::spawn(move || { let interp = Interpreter::with_init(Default::default(), |_vm| {}); @@ -49,6 +52,8 @@ pub extern "C" fn Py_InitializeEx(_initsigs: c_int) { }) }); }); + + attach_vm_to_thread(); } #[unsafe(no_mangle)] diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index bedcc610dd8..de25dba790a 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -27,19 +27,22 @@ pub struct PyThreadState { _interp: *mut std::ffi::c_void, } -#[unsafe(no_mangle)] -pub extern "C" fn PyGILState_Ensure() -> PyGILState_STATE { +pub(crate) fn attach_vm_to_thread() { VM.with(|vm| { vm.borrow_mut() .get_or_insert_with(|| request_vm_from_interpreter()); }); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyGILState_Ensure() -> PyGILState_STATE { + attach_vm_to_thread(); 0 } #[unsafe(no_mangle)] -pub extern "C" fn PyGILState_Release(_state: PyGILState_STATE) { -} +pub extern "C" fn PyGILState_Release(_state: PyGILState_STATE) {} #[unsafe(no_mangle)] pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState { @@ -47,5 +50,4 @@ pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState { } #[unsafe(no_mangle)] -pub extern "C" fn PyEval_RestoreThread(_tstate: *mut PyThreadState) { -} +pub extern "C" fn PyEval_RestoreThread(_tstate: *mut PyThreadState) {} From c603d37cd849691f8432f7a123170989a1a4c3fa Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 7 Apr 2026 10:55:29 +0200 Subject: [PATCH 013/135] Map RustPython type flags to CPython type flags --- crates/capi/src/object.rs | 98 +++++++++++++++++++--------------- crates/vm/src/builtins/type.rs | 2 +- 2 files changed, 55 insertions(+), 45 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index f041d2774e3..5a765fa409e 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,73 +1,53 @@ -use core::ffi::c_ulong; - use crate::PyObject; +use core::ffi::c_ulong; use rustpython_vm::builtins::PyType; use rustpython_vm::{AsObject, Context, Py}; use std::sync::LazyLock; pub struct PyTypeObject { ty: LazyLock<&'static Py>, - flags: c_ulong, } impl PyTypeObject { - const fn new(f: fn() -> &'static Py, flags: c_ulong) -> PyTypeObject { + const fn new(f: fn() -> &'static Py) -> PyTypeObject { PyTypeObject { ty: LazyLock::new(f), - flags, } } } -const PY_TPFLAGS_HAVE_STACKLESS_EXTENSION: c_ulong = 0; -const PY_TPFLAGS_HAVE_VERSION_TAG: c_ulong = 1 << 18; -const PY_TPFLAGS_DEFAULT: c_ulong = - PY_TPFLAGS_HAVE_STACKLESS_EXTENSION | PY_TPFLAGS_HAVE_VERSION_TAG; -const PY_TPFLAGS_IMMUTABLETYPE: c_ulong = 1 << 8; -const PY_TPFLAGS_BASETYPE: c_ulong = 1 << 10; const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; +const PY_TPFLAGS_LIST_SUBCLASS: c_ulong = 1 << 25; const PY_TPFLAGS_TUPLE_SUBCLASS: c_ulong = 1 << 26; +const PY_TPFLAGS_BYTES_SUBCLASS: c_ulong = 1 << 27; const PY_TPFLAGS_UNICODE_SUBCLASS: c_ulong = 1 << 28; +const PY_TPFLAGS_DICT_SUBCLASS: c_ulong = 1 << 29; +const PY_TPFLAGS_BASE_EXC_SUBCLASS: c_ulong = 1 << 30; const PY_TPFLAGS_TYPE_SUBCLASS: c_ulong = 1 << 31; #[unsafe(no_mangle)] -pub static mut PyType_Type: PyTypeObject = PyTypeObject::new( - || { - let zoo = &Context::genesis().types; - zoo.type_type - }, - PY_TPFLAGS_DEFAULT | PY_TPFLAGS_IMMUTABLETYPE | PY_TPFLAGS_BASETYPE | PY_TPFLAGS_TYPE_SUBCLASS, -); +pub static mut PyType_Type: PyTypeObject = PyTypeObject::new(|| { + let zoo = &Context::genesis().types; + zoo.type_type +}); #[unsafe(no_mangle)] -pub static mut PyLong_Type: PyTypeObject = PyTypeObject::new( - || { - let zoo = &Context::genesis().types; - zoo.int_type - }, - PY_TPFLAGS_DEFAULT | PY_TPFLAGS_IMMUTABLETYPE | PY_TPFLAGS_BASETYPE | PY_TPFLAGS_LONG_SUBCLASS, -); +pub static mut PyLong_Type: PyTypeObject = PyTypeObject::new(|| { + let zoo = &Context::genesis().types; + zoo.int_type +}); #[unsafe(no_mangle)] -pub static mut PyTuple_Type: PyTypeObject = PyTypeObject::new( - || { - let zoo = &Context::genesis().types; - zoo.tuple_type - }, - PY_TPFLAGS_DEFAULT | PY_TPFLAGS_IMMUTABLETYPE | PY_TPFLAGS_BASETYPE | PY_TPFLAGS_TUPLE_SUBCLASS, -); +pub static mut PyTuple_Type: PyTypeObject = PyTypeObject::new(|| { + let zoo = &Context::genesis().types; + zoo.tuple_type +}); #[unsafe(no_mangle)] -pub static mut PyUnicode_Type: PyTypeObject = PyTypeObject::new( - || { - let zoo = &Context::genesis().types; - zoo.str_type - }, - PY_TPFLAGS_DEFAULT - | PY_TPFLAGS_IMMUTABLETYPE - | PY_TPFLAGS_BASETYPE - | PY_TPFLAGS_UNICODE_SUBCLASS, -); +pub static mut PyUnicode_Type: PyTypeObject = PyTypeObject::new(|| { + let zoo = &Context::genesis().types; + zoo.str_type +}); #[unsafe(no_mangle)] pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { @@ -98,8 +78,38 @@ pub extern "C" fn PyType_GetFlags(ty: *mut PyTypeObject) -> c_ulong { panic!("PyType_GetFlags called with null type pointer"); } - // SAFETY: caller guarantees this is a valid exported type object pointer. - unsafe { (*ty).flags } + let ctx =Context::genesis(); + let zoo = &ctx.types; + let exp_zoo = &ctx.exceptions; + let ty_inner = unsafe { *(*ty).ty }; + let mut flags = ty_inner.slots.flags.bits(); + + if ty_inner.is_subtype(zoo.int_type) { + flags |= PY_TPFLAGS_LONG_SUBCLASS; + } + if ty_inner.is_subtype(zoo.list_type) { + flags |= PY_TPFLAGS_LIST_SUBCLASS + } + if ty_inner.is_subtype(zoo.tuple_type) { + flags |= PY_TPFLAGS_TUPLE_SUBCLASS; + } + if ty_inner.is_subtype(zoo.bytes_type) { + flags |= PY_TPFLAGS_BYTES_SUBCLASS; + } + if ty_inner.is_subtype(zoo.str_type) { + flags |= PY_TPFLAGS_UNICODE_SUBCLASS; + } + if ty_inner.is_subtype(zoo.dict_type) { + flags |= PY_TPFLAGS_DICT_SUBCLASS; + } + if ty_inner.is_subtype(exp_zoo.base_exception_type) { + flags |= PY_TPFLAGS_BASE_EXC_SUBCLASS; + } + if ty_inner.is_subtype(zoo.type_type) { + flags |= PY_TPFLAGS_TYPE_SUBCLASS; + } + + flags } #[unsafe(no_mangle)] diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index 64801a1de5c..652c4db1d8b 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -1240,7 +1240,7 @@ impl PyType { } impl Py { - pub(crate) fn is_subtype(&self, other: &Self) -> bool { + pub fn is_subtype(&self, other: &Self) -> bool { is_subtype_with_mro(&self.mro.read(), self, other) } From cee9403330ac2cf1706f910dbdc2b1b4988c6e8a Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 7 Apr 2026 15:47:04 +0200 Subject: [PATCH 014/135] Use `*const Py` instead of `*mut PyTypeObject` --- crates/capi/src/object.rs | 107 +++++++++++++-------------------- crates/capi/src/pylifecycle.rs | 5 +- 2 files changed, 46 insertions(+), 66 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 5a765fa409e..fdf5d4caefc 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,20 +1,9 @@ use crate::PyObject; use core::ffi::c_ulong; +use std::mem::MaybeUninit; use rustpython_vm::builtins::PyType; -use rustpython_vm::{AsObject, Context, Py}; -use std::sync::LazyLock; - -pub struct PyTypeObject { - ty: LazyLock<&'static Py>, -} - -impl PyTypeObject { - const fn new(f: fn() -> &'static Py) -> PyTypeObject { - PyTypeObject { - ty: LazyLock::new(f), - } - } -} +use rustpython_vm::{Context, Py}; +use crate::pylifecycle::INITIALIZED; const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; const PY_TPFLAGS_LIST_SUBCLASS: c_ulong = 1 << 25; @@ -26,86 +15,74 @@ const PY_TPFLAGS_BASE_EXC_SUBCLASS: c_ulong = 1 << 30; const PY_TPFLAGS_TYPE_SUBCLASS: c_ulong = 1 << 31; #[unsafe(no_mangle)] -pub static mut PyType_Type: PyTypeObject = PyTypeObject::new(|| { - let zoo = &Context::genesis().types; - zoo.type_type -}); +pub static mut PyType_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); #[unsafe(no_mangle)] -pub static mut PyLong_Type: PyTypeObject = PyTypeObject::new(|| { - let zoo = &Context::genesis().types; - zoo.int_type -}); +pub static mut PyLong_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); #[unsafe(no_mangle)] -pub static mut PyTuple_Type: PyTypeObject = PyTypeObject::new(|| { - let zoo = &Context::genesis().types; - zoo.tuple_type -}); +pub static mut PyTuple_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); #[unsafe(no_mangle)] -pub static mut PyUnicode_Type: PyTypeObject = PyTypeObject::new(|| { - let zoo = &Context::genesis().types; - zoo.str_type -}); +pub static mut PyUnicode_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); -#[unsafe(no_mangle)] -pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *mut PyTypeObject { - if op.is_null() { - return std::ptr::null_mut(); - } +/// Initialize the static type pointers. This should be called once during interpreter initialization, +/// and before any of the static type pointers are used. +/// +/// Panics: +/// Panics when the interpreter is already initialized. +#[allow(static_mut_refs)] +pub(crate) fn init_static_type_pointers() { + assert!(!INITIALIZED.is_completed(), "Python already initialized, we should not touch the static type pointers"); + let zoo = &Context::genesis().types; unsafe { - let ty = (*op).class(); - if ty.is(*PyType_Type.ty) { - &raw mut PyType_Type - } else if ty.is(*PyLong_Type.ty) { - &raw mut PyLong_Type - } else if ty.is(*PyTuple_Type.ty) { - &raw mut PyTuple_Type - } else if ty.is(*PyUnicode_Type.ty) { - &raw mut PyUnicode_Type - } else { - todo!("Unsupported type: {:?}", ty.name()); - } - } + PyType_Type.write(zoo.type_type); + PyLong_Type.write(zoo.int_type); + PyTuple_Type.write(zoo.tuple_type); + PyUnicode_Type.write(zoo.str_type); + }; } #[unsafe(no_mangle)] -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "C" fn PyType_GetFlags(ty: *mut PyTypeObject) -> c_ulong { - if ty.is_null() { - panic!("PyType_GetFlags called with null type pointer"); - } +pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const Py { + // SAFETY: The caller must guarantee that `op` is a valid pointer to a `PyObject`. + unsafe { (*op).class() } +} +#[unsafe(no_mangle)] +#[allow(clippy::not_unsafe_ptr_arg_deref)] +pub extern "C" fn PyType_GetFlags(ptr: *const Py) -> c_ulong { let ctx =Context::genesis(); let zoo = &ctx.types; let exp_zoo = &ctx.exceptions; - let ty_inner = unsafe { *(*ty).ty }; - let mut flags = ty_inner.slots.flags.bits(); - if ty_inner.is_subtype(zoo.int_type) { + // SAFETY: The caller must guarantee that `ptr` is a valid pointer to a `PyType` object. + let ty = unsafe { &*ptr}; + let mut flags = ty.slots.flags.bits(); + + if ty.is_subtype(zoo.int_type) { flags |= PY_TPFLAGS_LONG_SUBCLASS; } - if ty_inner.is_subtype(zoo.list_type) { + if ty.is_subtype(zoo.list_type) { flags |= PY_TPFLAGS_LIST_SUBCLASS } - if ty_inner.is_subtype(zoo.tuple_type) { + if ty.is_subtype(zoo.tuple_type) { flags |= PY_TPFLAGS_TUPLE_SUBCLASS; } - if ty_inner.is_subtype(zoo.bytes_type) { + if ty.is_subtype(zoo.bytes_type) { flags |= PY_TPFLAGS_BYTES_SUBCLASS; } - if ty_inner.is_subtype(zoo.str_type) { + if ty.is_subtype(zoo.str_type) { flags |= PY_TPFLAGS_UNICODE_SUBCLASS; } - if ty_inner.is_subtype(zoo.dict_type) { + if ty.is_subtype(zoo.dict_type) { flags |= PY_TPFLAGS_DICT_SUBCLASS; } - if ty_inner.is_subtype(exp_zoo.base_exception_type) { + if ty.is_subtype(exp_zoo.base_exception_type) { flags |= PY_TPFLAGS_BASE_EXC_SUBCLASS; } - if ty_inner.is_subtype(zoo.type_type) { + if ty.is_subtype(zoo.type_type) { flags |= PY_TPFLAGS_TYPE_SUBCLASS; } @@ -113,13 +90,13 @@ pub extern "C" fn PyType_GetFlags(ty: *mut PyTypeObject) -> c_ulong { } #[unsafe(no_mangle)] -pub extern "C" fn PyType_GetName(_ty: *mut PyTypeObject) -> *mut PyObject { +pub extern "C" fn PyType_GetName(_ptr: *const Py) -> *mut PyObject { crate::log_stub("PyType_GetName"); std::ptr::null_mut() } #[unsafe(no_mangle)] -pub extern "C" fn PyType_GetQualName(_ty: *mut PyTypeObject) -> *mut PyObject { +pub extern "C" fn PyType_GetQualName(_ptr: *const Py) -> *mut PyObject { crate::log_stub("PyType_GetQualName"); std::ptr::null_mut() } diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index d8b3fc2ae41..5b3c25a0e8c 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -4,10 +4,11 @@ use core::ffi::c_int; use rustpython_vm::Interpreter; use rustpython_vm::vm::thread::ThreadedVirtualMachine; use std::sync::{Once, OnceLock, mpsc}; +use crate::object::init_static_type_pointers; static VM_REQUEST_TX: OnceLock>> = OnceLock::new(); -static INITIALIZED: Once = Once::new(); +pub(crate) static INITIALIZED: Once = Once::new(); /// Request a vm from the main interpreter pub(crate) fn request_vm_from_interpreter() -> ThreadedVirtualMachine { @@ -36,6 +37,8 @@ pub extern "C" fn Py_InitializeEx(_initsigs: c_int) { } INITIALIZED.call_once(|| { + init_static_type_pointers(); + let (tx, rx) = mpsc::channel(); VM_REQUEST_TX .set(tx) From 0a50fccdafea8b3004e690d1e4960fd0da17ce7e Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 7 Apr 2026 16:18:43 +0200 Subject: [PATCH 015/135] Implement `PyUnicode_AsUTF8AndSize` --- crates/capi/src/unicodeobject.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index 27240521110..ee27e9d505c 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -1,11 +1,11 @@ +use crate::PyObject; +use crate::pystate::with_vm; use core::ffi::c_char; use core::ptr; use core::slice; use core::str; - -use crate::PyObject; -use crate::pystate::with_vm; use rustpython_vm::PyObjectRef; +use rustpython_vm::builtins::PyStr; #[unsafe(no_mangle)] pub extern "C" fn PyUnicode_FromStringAndSize(s: *const c_char, len: isize) -> *mut PyObject { @@ -28,12 +28,26 @@ pub extern "C" fn PyUnicode_FromStringAndSize(s: *const c_char, len: isize) -> * } #[unsafe(no_mangle)] -pub extern "C" fn PyUnicode_AsUTF8AndSize( - _unicode: *mut PyObject, - _size: *mut isize, -) -> *const c_char { - crate::log_stub("PyUnicode_AsUTF8AndSize"); - ptr::null() +pub extern "C" fn PyUnicode_AsUTF8AndSize(obj: *mut PyObject, size: *mut isize) -> *const c_char { + with_vm(|vm| { + let obj = unsafe { + obj.as_ref() + .expect("PyUnicode_AsUTF8AndSize called with null pointer") + }; + + let unicode = obj + .downcast_ref::() + .expect("PyUnicode_AsUTF8AndSize called with non-unicode object"); + + let str = unicode + .to_str() + .expect("only utf8 or ascii is currently supported in PyUnicode_AsUTF8AndSize"); + + if !size.is_null() { + unsafe { *size = str.len() as isize }; + } + str.as_ptr() as _ + }) } #[unsafe(no_mangle)] From f588979932a93d104825bb1eb1dcab249fb62f05 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 7 Apr 2026 16:19:11 +0200 Subject: [PATCH 016/135] Implement `PyType_GetName` --- crates/capi/src/object.rs | 28 ++++++++++++++----------- crates/capi/src/pylifecycle.rs | 2 +- example_projects/pyo3_embed/src/main.rs | 14 ++++++++++--- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index fdf5d4caefc..73d0005365e 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,9 +1,11 @@ use crate::PyObject; +use crate::pylifecycle::INITIALIZED; +use crate::pystate::with_vm; use core::ffi::c_ulong; -use std::mem::MaybeUninit; use rustpython_vm::builtins::PyType; +use rustpython_vm::convert::IntoObject; use rustpython_vm::{Context, Py}; -use crate::pylifecycle::INITIALIZED; +use std::mem::MaybeUninit; const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; const PY_TPFLAGS_LIST_SUBCLASS: c_ulong = 1 << 25; @@ -26,7 +28,6 @@ pub static mut PyTuple_Type: MaybeUninit<&'static Py> = MaybeUninit::uni #[unsafe(no_mangle)] pub static mut PyUnicode_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); - /// Initialize the static type pointers. This should be called once during interpreter initialization, /// and before any of the static type pointers are used. /// @@ -34,7 +35,10 @@ pub static mut PyUnicode_Type: MaybeUninit<&'static Py> = MaybeUninit::u /// Panics when the interpreter is already initialized. #[allow(static_mut_refs)] pub(crate) fn init_static_type_pointers() { - assert!(!INITIALIZED.is_completed(), "Python already initialized, we should not touch the static type pointers"); + assert!( + !INITIALIZED.is_completed(), + "Python already initialized, we should not touch the static type pointers" + ); let zoo = &Context::genesis().types; unsafe { PyType_Type.write(zoo.type_type); @@ -53,12 +57,12 @@ pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const Py { #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] pub extern "C" fn PyType_GetFlags(ptr: *const Py) -> c_ulong { - let ctx =Context::genesis(); + let ctx = Context::genesis(); let zoo = &ctx.types; let exp_zoo = &ctx.exceptions; // SAFETY: The caller must guarantee that `ptr` is a valid pointer to a `PyType` object. - let ty = unsafe { &*ptr}; + let ty = unsafe { &*ptr }; let mut flags = ty.slots.flags.bits(); if ty.is_subtype(zoo.int_type) { @@ -90,15 +94,15 @@ pub extern "C" fn PyType_GetFlags(ptr: *const Py) -> c_ulong { } #[unsafe(no_mangle)] -pub extern "C" fn PyType_GetName(_ptr: *const Py) -> *mut PyObject { - crate::log_stub("PyType_GetName"); - std::ptr::null_mut() +pub extern "C" fn PyType_GetName(ptr: *const Py) -> *mut PyObject { + let ty = unsafe { &*ptr }; + with_vm(move |vm| ty.__name__(vm).into_object().into_raw().as_ptr()) } #[unsafe(no_mangle)] -pub extern "C" fn PyType_GetQualName(_ptr: *const Py) -> *mut PyObject { - crate::log_stub("PyType_GetQualName"); - std::ptr::null_mut() +pub extern "C" fn PyType_GetQualName(ptr: *const Py) -> *mut PyObject { + let ty = unsafe { &*ptr }; + with_vm(move |vm| ty.__qualname__(vm).into_object().into_raw().as_ptr()) } #[unsafe(no_mangle)] diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 5b3c25a0e8c..a25f8dc0e27 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,10 +1,10 @@ use crate::log_stub; +use crate::object::init_static_type_pointers; use crate::pystate::attach_vm_to_thread; use core::ffi::c_int; use rustpython_vm::Interpreter; use rustpython_vm::vm::thread::ThreadedVirtualMachine; use std::sync::{Once, OnceLock, mpsc}; -use crate::object::init_static_type_pointers; static VM_REQUEST_TX: OnceLock>> = OnceLock::new(); diff --git a/example_projects/pyo3_embed/src/main.rs b/example_projects/pyo3_embed/src/main.rs index 3ba2c549411..cc9eef902fc 100644 --- a/example_projects/pyo3_embed/src/main.rs +++ b/example_projects/pyo3_embed/src/main.rs @@ -7,10 +7,13 @@ fn main() { Python::attach(|py| { let number = PyInt::new(py, 123); assert!(number.is_instance_of::()); - assert_eq!(number.extract::().unwrap(), 123); + assert_eq!(number.extract::()?, 123); let string = PyString::new(py, "Hello, World!"); assert!(string.is_instance_of::()); + assert_eq!(string.to_str()?, "Hello, World!"); + + assert_eq!(string.get_type().name()?.to_str()?, "str"); let number = number.unbind(); std::thread::spawn(move || { @@ -18,6 +21,11 @@ fn main() { let number = number.bind(py); assert!(number.is_instance_of::()); }); - }).join().unwrap(); - }); + }) + .join() + .unwrap(); + + PyResult::Ok(()) + }) + .unwrap(); } From edaca0ca799f1549a9411f4a5ab60671e9a5816f Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 7 Apr 2026 16:44:54 +0200 Subject: [PATCH 017/135] Implement alternative `PyLong` constructors --- crates/capi/src/longobject.rs | 44 ++++++++++++++++------------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index efaa0fc2cfc..a7a799d1b9e 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -2,46 +2,39 @@ use core::ffi::{c_long, c_longlong, c_ulong, c_ulonglong}; use core::ptr; use crate::PyObject; +use crate::pyerrors::{PyErr_SetString, PyExc_OverflowError}; use crate::pystate::with_vm; -use rustpython_vm::PyObjectRef; use rustpython_vm::builtins::PyInt; +use rustpython_vm::convert::IntoObject; #[unsafe(no_mangle)] pub extern "C" fn PyLong_FromLong(value: c_long) -> *mut PyObject { - with_vm(|vm| { - let obj: PyObjectRef = vm.ctx.new_int(value).into(); - obj.into_raw().as_ptr() - }) + with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr()) } #[unsafe(no_mangle)] -pub extern "C" fn PyLong_FromLongLong(_value: c_longlong) -> *mut PyObject { - crate::log_stub("PyLong_FromLongLong"); - ptr::null_mut() +pub extern "C" fn PyLong_FromLongLong(value: c_longlong) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr()) } #[unsafe(no_mangle)] -pub extern "C" fn PyLong_FromSsize_t(_value: isize) -> *mut PyObject { - crate::log_stub("PyLong_FromSsize_t"); - ptr::null_mut() +pub extern "C" fn PyLong_FromSsize_t(value: isize) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr()) } #[unsafe(no_mangle)] -pub extern "C" fn PyLong_FromSize_t(_value: usize) -> *mut PyObject { - crate::log_stub("PyLong_FromSize_t"); - ptr::null_mut() +pub extern "C" fn PyLong_FromSize_t(value: usize) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr()) } #[unsafe(no_mangle)] -pub extern "C" fn PyLong_FromUnsignedLong(_value: c_ulong) -> *mut PyObject { - crate::log_stub("PyLong_FromUnsignedLong"); - ptr::null_mut() +pub extern "C" fn PyLong_FromUnsignedLong(value: c_ulong) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr()) } #[unsafe(no_mangle)] -pub extern "C" fn PyLong_FromUnsignedLongLong(_value: c_ulonglong) -> *mut PyObject { - crate::log_stub("PyLong_FromUnsignedLongLong"); - ptr::null_mut() +pub extern "C" fn PyLong_FromUnsignedLongLong(value: c_ulonglong) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr()) } #[unsafe(no_mangle)] @@ -57,9 +50,12 @@ pub extern "C" fn PyLong_AsLong(obj: *mut PyObject) -> c_long { .downcast_ref::() .expect("PyLong_AsLong currently only accepts int instances"); - int_obj - .as_bigint() - .try_into() - .expect("PyLong_AsLong: value out of range for c_long") + int_obj.as_bigint().try_into().unwrap_or_else(|_| unsafe { + PyErr_SetString( + PyExc_OverflowError, + c"Python int too large to convert to C long".as_ptr(), + ); + -1 + }) }) } From e4c8bd74f2fbdc19e32c0061cfe219a4056b2c87 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 7 Apr 2026 19:37:04 +0200 Subject: [PATCH 018/135] Move `init_static_type_pointers` to `pylifecycle.rs` --- crates/capi/src/longobject.rs | 1 - crates/capi/src/object.rs | 21 --------------------- crates/capi/src/pylifecycle.rs | 26 ++++++++++++++++++++++++-- crates/capi/src/unicodeobject.rs | 2 +- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index a7a799d1b9e..1a876712452 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -1,5 +1,4 @@ use core::ffi::{c_long, c_longlong, c_ulong, c_ulonglong}; -use core::ptr; use crate::PyObject; use crate::pyerrors::{PyErr_SetString, PyExc_OverflowError}; diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 73d0005365e..c3dfa1cc268 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,5 +1,4 @@ use crate::PyObject; -use crate::pylifecycle::INITIALIZED; use crate::pystate::with_vm; use core::ffi::c_ulong; use rustpython_vm::builtins::PyType; @@ -28,26 +27,6 @@ pub static mut PyTuple_Type: MaybeUninit<&'static Py> = MaybeUninit::uni #[unsafe(no_mangle)] pub static mut PyUnicode_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); -/// Initialize the static type pointers. This should be called once during interpreter initialization, -/// and before any of the static type pointers are used. -/// -/// Panics: -/// Panics when the interpreter is already initialized. -#[allow(static_mut_refs)] -pub(crate) fn init_static_type_pointers() { - assert!( - !INITIALIZED.is_completed(), - "Python already initialized, we should not touch the static type pointers" - ); - let zoo = &Context::genesis().types; - unsafe { - PyType_Type.write(zoo.type_type); - PyLong_Type.write(zoo.int_type); - PyTuple_Type.write(zoo.tuple_type); - PyUnicode_Type.write(zoo.str_type); - }; -} - #[unsafe(no_mangle)] pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const Py { // SAFETY: The caller must guarantee that `op` is a valid pointer to a `PyObject`. diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index a25f8dc0e27..b7918751635 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,9 +1,9 @@ use crate::log_stub; -use crate::object::init_static_type_pointers; +use crate::object::{PyLong_Type, PyTuple_Type, PyType_Type, PyUnicode_Type}; use crate::pystate::attach_vm_to_thread; use core::ffi::c_int; -use rustpython_vm::Interpreter; use rustpython_vm::vm::thread::ThreadedVirtualMachine; +use rustpython_vm::{Context, Interpreter}; use std::sync::{Once, OnceLock, mpsc}; static VM_REQUEST_TX: OnceLock>> = @@ -20,6 +20,28 @@ pub(crate) fn request_vm_from_interpreter() -> ThreadedVirtualMachine { response_rx.recv().expect("Failed to receive VM response") } +/// Initialize the static type pointers. This should be called once during interpreter initialization, +/// and before any of the static type pointers are used. +/// +/// Panics: +/// Panics when the interpreter is already initialized. +#[allow(static_mut_refs)] +pub(crate) fn init_static_type_pointers() { + assert!( + !INITIALIZED.is_completed(), + "Python already initialized, we should not touch the static type pointers" + ); + let context = Context::genesis(); + let types = &context.types; + + unsafe { + PyType_Type.write(types.type_type); + PyLong_Type.write(types.int_type); + PyTuple_Type.write(types.tuple_type); + PyUnicode_Type.write(types.str_type); + }; +} + #[unsafe(no_mangle)] pub extern "C" fn Py_IsInitialized() -> c_int { INITIALIZED.is_completed() as _ diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index ee27e9d505c..837e6e2370a 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -29,7 +29,7 @@ pub extern "C" fn PyUnicode_FromStringAndSize(s: *const c_char, len: isize) -> * #[unsafe(no_mangle)] pub extern "C" fn PyUnicode_AsUTF8AndSize(obj: *mut PyObject, size: *mut isize) -> *const c_char { - with_vm(|vm| { + with_vm(|_vm| { let obj = unsafe { obj.as_ref() .expect("PyUnicode_AsUTF8AndSize called with null pointer") From 26c277744004c8034a854cddf48527b612686074 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 7 Apr 2026 19:50:44 +0200 Subject: [PATCH 019/135] Add support for `None`, `True`, `False`, `Ellipsis` & `NotImplemented` --- crates/capi/src/object.rs | 20 ++++++++++++++++---- example_projects/pyo3_embed/src/main.rs | 10 +++++++++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index c3dfa1cc268..c5d8b58f14f 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -3,7 +3,8 @@ use crate::pystate::with_vm; use core::ffi::c_ulong; use rustpython_vm::builtins::PyType; use rustpython_vm::convert::IntoObject; -use rustpython_vm::{Context, Py}; +use rustpython_vm::{AsObject, Context, Py}; +use std::ffi::c_uint; use std::mem::MaybeUninit; const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; @@ -109,7 +110,18 @@ pub extern "C" fn PyObject_Str(_obj: *mut PyObject) -> *mut PyObject { } #[unsafe(no_mangle)] -pub extern "C" fn Py_GetConstantBorrowed(_constant_id: core::ffi::c_uint) -> *mut PyObject { - crate::log_stub("Py_GetConstantBorrowed"); - std::ptr::null_mut() +pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject { + with_vm(|vm| { + let ctx = &vm.ctx; + match constant_id { + 0 => ctx.none.as_object(), + 1 => ctx.true_value.as_object(), + 2 => ctx.true_value.as_object(), + 3 => ctx.ellipsis.as_object(), + 4 => ctx.not_implemented.as_object(), + _ => panic!("Invalid constant_id passed to Py_GetConstantBorrowed"), + } + .as_raw() + .cast_mut() + }) } diff --git a/example_projects/pyo3_embed/src/main.rs b/example_projects/pyo3_embed/src/main.rs index cc9eef902fc..eef9bd7425f 100644 --- a/example_projects/pyo3_embed/src/main.rs +++ b/example_projects/pyo3_embed/src/main.rs @@ -1,5 +1,11 @@ use pyo3::prelude::*; -use pyo3::types::{PyInt, PyString}; +use pyo3::types::{PyInt, PyNone, PyString}; + + +#[pyfunction] +fn python_function(py: Python<'_>) -> Borrowed<'_, '_, PyNone> { + PyNone::get(py) +} fn main() { Python::initialize(); @@ -25,6 +31,8 @@ fn main() { .join() .unwrap(); + assert!(python_function(py).is_none()); + PyResult::Ok(()) }) .unwrap(); From 09466e2faf23e848008aa9f6ccf31146045e3efa Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 8 Apr 2026 11:51:25 +0200 Subject: [PATCH 020/135] Add support for PyBytes --- crates/capi/src/bytesobject.rs | 42 ++++++++++++++++++++----- example_projects/pyo3_embed/src/main.rs | 5 ++- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/crates/capi/src/bytesobject.rs b/crates/capi/src/bytesobject.rs index a5164bc8487..ebff4fd158f 100644 --- a/crates/capi/src/bytesobject.rs +++ b/crates/capi/src/bytesobject.rs @@ -1,16 +1,42 @@ +use crate::PyObject; +use crate::pystate::with_vm; use core::ffi::c_char; -use core::ptr; +use rustpython_vm::builtins::PyBytes; +use rustpython_vm::convert::IntoObject; -use crate::PyObject; +#[unsafe(no_mangle)] +pub extern "C" fn PyBytes_FromStringAndSize(bytes: *mut c_char, len: isize) -> *mut PyObject { + with_vm(|vm| { + if bytes.is_null() { + std::ptr::null_mut() + } else { + let bytes_slice = + unsafe { core::slice::from_raw_parts(bytes as *const u8, len as usize) }; + vm.ctx + .new_bytes(bytes_slice.to_vec()) + .into_object() + .into_raw() + .as_ptr() + } + }) +} #[unsafe(no_mangle)] -pub extern "C" fn PyBytes_Size(_bytes: *mut PyObject) -> isize { - crate::log_stub("PyBytes_Size"); - 0 +pub extern "C" fn PyBytes_Size(bytes: *mut PyObject) -> isize { + with_vm(|_vm| { + let bytes = unsafe { &*bytes } + .downcast_ref::() + .expect("PyBytes_Size argument must be a bytes object"); + bytes.as_bytes().len() as _ + }) } #[unsafe(no_mangle)] -pub extern "C" fn PyBytes_AsString(_bytes: *mut PyObject) -> *mut c_char { - crate::log_stub("PyBytes_AsString"); - ptr::null_mut() +pub extern "C" fn PyBytes_AsString(bytes: *mut PyObject) -> *mut c_char { + with_vm(|_vm| { + let bytes = unsafe { &*bytes } + .downcast_ref::() + .expect("PyBytes_AsString argument must be a bytes object"); + bytes.as_bytes().as_ptr() as _ + }) } diff --git a/example_projects/pyo3_embed/src/main.rs b/example_projects/pyo3_embed/src/main.rs index eef9bd7425f..1d4990655e4 100644 --- a/example_projects/pyo3_embed/src/main.rs +++ b/example_projects/pyo3_embed/src/main.rs @@ -1,5 +1,5 @@ use pyo3::prelude::*; -use pyo3::types::{PyInt, PyNone, PyString}; +use pyo3::types::{PyBytes, PyInt, PyNone, PyString}; #[pyfunction] @@ -33,6 +33,9 @@ fn main() { assert!(python_function(py).is_none()); + let bytes = PyBytes::new(py, b"Hello, World!"); + assert_eq!(bytes.as_bytes(), b"Hello, World!"); + PyResult::Ok(()) }) .unwrap(); From 94cc39bf0d5072cb93f3900744928293d5e47ac7 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 8 Apr 2026 15:22:59 +0200 Subject: [PATCH 021/135] Add basic exception support --- crates/capi/src/longobject.rs | 2 +- crates/capi/src/pyerrors.rs | 118 +++++++++++++++++++----- crates/capi/src/pylifecycle.rs | 8 +- crates/vm/src/vm/mod.rs | 15 ++- example_projects/pyo3_embed/src/main.rs | 5 +- 5 files changed, 123 insertions(+), 25 deletions(-) diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index 1a876712452..a99e790e557 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -51,7 +51,7 @@ pub extern "C" fn PyLong_AsLong(obj: *mut PyObject) -> c_long { int_obj.as_bigint().try_into().unwrap_or_else(|_| unsafe { PyErr_SetString( - PyExc_OverflowError, + PyExc_OverflowError.assume_init(), c"Python int too large to convert to C long".as_ptr(), ); -1 diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index 3a567bcfece..112e1caf591 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -1,31 +1,52 @@ -use core::ffi::{c_char, c_int}; -use core::ptr; - use crate::PyObject; +use crate::pystate::with_vm; +use core::ffi::{c_char, c_int}; +use rustpython_vm::builtins::{PyTuple, PyType}; +use rustpython_vm::convert::ToPyObject; +use std::ffi::CStr; +use std::mem::MaybeUninit; #[unsafe(no_mangle)] -pub static mut PyExc_BaseException: *mut PyObject = ptr::null_mut(); +pub static mut PyExc_BaseException: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); #[unsafe(no_mangle)] -pub static mut PyExc_TypeError: *mut PyObject = ptr::null_mut(); +pub static mut PyExc_TypeError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); #[unsafe(no_mangle)] -pub static mut PyExc_OverflowError: *mut PyObject = ptr::null_mut(); +pub static mut PyExc_OverflowError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); #[unsafe(no_mangle)] pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject { - crate::log_stub("PyErr_GetRaisedException"); - ptr::null_mut() + with_vm(|vm| { + vm.take_raised_exception().map_or_else( + || std::ptr::null_mut(), + |exc| exc.to_pyobject(vm).into_raw().as_ptr(), + ) + }) } #[unsafe(no_mangle)] -pub extern "C" fn PyErr_SetRaisedException(_exc: *mut PyObject) { - crate::log_stub("PyErr_SetRaisedException"); +pub extern "C" fn PyErr_SetRaisedException(exc: *mut PyObject) { + with_vm(|vm| { + let exception = unsafe { (&*exc).to_owned().downcast_unchecked() }; + vm.push_exception(Some(exception)); + }); } #[unsafe(no_mangle)] -pub extern "C" fn PyErr_SetObject(_exception: *mut PyObject, _value: *mut PyObject) { - crate::log_stub("PyErr_SetObject"); +pub extern "C" fn PyErr_SetObject(exception: *mut PyObject, value: *mut PyObject) { + with_vm(|vm| { + let exc_type = unsafe { (&*exception).to_owned() }; + let exc_val = unsafe { (&*value).to_owned() }; + + let normalized = vm + .normalize_exception(exc_type, exc_val, vm.ctx.none()) + .unwrap_or_else(|_| { + vm.new_type_error("exceptions must derive from BaseException".to_owned()) + }); + + vm.push_exception(Some(normalized)); + }); } #[unsafe(no_mangle)] @@ -35,27 +56,82 @@ pub extern "C" fn PyErr_SetString(_exception: *mut PyObject, _message: *const c_ #[unsafe(no_mangle)] pub extern "C" fn PyErr_PrintEx(_set_sys_last_vars: c_int) { - crate::log_stub("PyErr_PrintEx"); + with_vm(|vm| { + let exception = vm + .take_raised_exception() + .expect("No exception set in PyErr_PrintEx"); + + vm.print_exception(exception); + }); } #[unsafe(no_mangle)] -pub extern "C" fn PyErr_WriteUnraisable(_obj: *mut PyObject) { - crate::log_stub("PyErr_WriteUnraisable"); +pub extern "C" fn PyErr_WriteUnraisable(obj: *mut PyObject) { + with_vm(|vm| { + let exception = vm + .take_raised_exception() + .expect("No exception set in PyErr_WriteUnraisable"); + + let object = unsafe { vm.unwrap_or_none(obj.as_ref().map(|obj| obj.to_owned())) }; + + vm.run_unraisable(exception, None, object) + }); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyErr_NewException( + name: *const c_char, + base: *mut PyObject, + dict: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let (module, name) = unsafe { + CStr::from_ptr(name) + .to_str() + .expect("Exception name is not valid UTF-8") + .rsplit_once('.') + .expect("Exception name must be of the form 'module.ExceptionName'") + }; + + let bases = unsafe { base.as_ref() }.map(|bases| { + if let Some(ty) = bases.downcast_ref::() { + vec![ty.to_owned()] + } else if let Some(tuple) = bases.downcast_ref::() { + tuple + .iter() + .map(|item| item.to_owned().downcast()) + .collect::, _>>() + .expect("PyErr_NewException base tuple must contain only types") + } else { + panic!("PyErr_NewException base must be a type or a tuple of types"); + } + }); + + assert!( + dict.is_null(), + "PyErr_NewException with non-null dict is not supported yet" + ); + + vm.ctx + .new_exception_type(module, name, bases) + .to_pyobject(vm) + .into_raw() + .as_ptr() + }) } #[unsafe(no_mangle)] pub extern "C" fn PyErr_NewExceptionWithDoc( - _name: *const c_char, + name: *const c_char, _doc: *const c_char, - _base: *mut PyObject, - _dict: *mut PyObject, + base: *mut PyObject, + dict: *mut PyObject, ) -> *mut PyObject { - crate::log_stub("PyErr_NewExceptionWithDoc"); - ptr::null_mut() + PyErr_NewException(name, base, dict) } #[unsafe(no_mangle)] pub extern "C" fn PyException_GetTraceback(_exc: *mut PyObject) -> *mut PyObject { crate::log_stub("PyException_GetTraceback"); - ptr::null_mut() + std::ptr::null_mut() } diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index b7918751635..644639814cb 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,9 +1,10 @@ use crate::log_stub; use crate::object::{PyLong_Type, PyTuple_Type, PyType_Type, PyUnicode_Type}; +use crate::pyerrors::{PyExc_BaseException, PyExc_OverflowError, PyExc_TypeError}; use crate::pystate::attach_vm_to_thread; use core::ffi::c_int; use rustpython_vm::vm::thread::ThreadedVirtualMachine; -use rustpython_vm::{Context, Interpreter}; +use rustpython_vm::{AsObject, Context, Interpreter}; use std::sync::{Once, OnceLock, mpsc}; static VM_REQUEST_TX: OnceLock>> = @@ -39,6 +40,11 @@ pub(crate) fn init_static_type_pointers() { PyLong_Type.write(types.int_type); PyTuple_Type.write(types.tuple_type); PyUnicode_Type.write(types.str_type); + + let exc = &context.exceptions; + PyExc_BaseException.write(exc.base_exception_type.as_object().as_raw().cast_mut()); + PyExc_TypeError.write(exc.type_error.as_object().as_raw().cast_mut()); + PyExc_OverflowError.write(exc.overflow_error.as_object().as_raw().cast_mut()); }; } diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 231ba5cde38..e8f3b771a42 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -2037,7 +2037,7 @@ impl VirtualMachine { } } - pub(crate) fn push_exception(&self, exc: Option) { + pub fn push_exception(&self, exc: Option) { self.exceptions.borrow_mut().stack.push(exc); #[cfg(feature = "threading")] thread::update_thread_exception(self.topmost_exception()); @@ -2078,6 +2078,19 @@ impl VirtualMachine { thread::update_thread_exception(self.topmost_exception()); } + pub fn take_raised_exception(&self) -> Option { + let mut excs = self.exceptions.borrow_mut(); + if let Some(top) = excs.stack.last_mut() { + let exc = top.take(); + drop(excs); + #[cfg(feature = "threading")] + thread::update_thread_exception(self.topmost_exception()); + exc + } else { + None + } + } + pub(crate) fn contextualize_exception(&self, exception: &Py) { if let Some(context_exc) = self.topmost_exception() && !context_exc.is(exception) diff --git a/example_projects/pyo3_embed/src/main.rs b/example_projects/pyo3_embed/src/main.rs index 1d4990655e4..b4acdc97a92 100644 --- a/example_projects/pyo3_embed/src/main.rs +++ b/example_projects/pyo3_embed/src/main.rs @@ -1,7 +1,7 @@ +use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::types::{PyBytes, PyInt, PyNone, PyString}; - #[pyfunction] fn python_function(py: Python<'_>) -> Borrowed<'_, '_, PyNone> { PyNone::get(py) @@ -36,6 +36,9 @@ fn main() { let bytes = PyBytes::new(py, b"Hello, World!"); assert_eq!(bytes.as_bytes(), b"Hello, World!"); + PyTypeError::new_err("This is a type error").restore(py); + assert!(PyErr::take(py).is_some()); + PyResult::Ok(()) }) .unwrap(); From 2ef64d2578e2a6f6df337cfd51de6909fb9350bc Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 8 Apr 2026 15:48:31 +0200 Subject: [PATCH 022/135] Add import support --- crates/capi/src/import.rs | 18 +++++++++++++----- example_projects/pyo3_embed/src/main.rs | 2 ++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/capi/src/import.rs b/crates/capi/src/import.rs index 29ea7996687..fd5b5d668b7 100644 --- a/crates/capi/src/import.rs +++ b/crates/capi/src/import.rs @@ -1,9 +1,17 @@ -use core::ptr; - use crate::PyObject; +use crate::pystate::with_vm; +use rustpython_vm::builtins::PyStr; #[unsafe(no_mangle)] -pub extern "C" fn PyImport_Import(_name: *mut PyObject) -> *mut PyObject { - crate::log_stub("PyImport_Import"); - ptr::null_mut() +pub extern "C" fn PyImport_Import(name: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let name = unsafe { (&*name).downcast_unchecked_ref::() }; + vm.import(name, 0).map_or_else( + |err| { + vm.push_exception(Some(err)); + std::ptr::null_mut() + }, + |module| module.into_raw().as_ptr(), + ) + }) } diff --git a/example_projects/pyo3_embed/src/main.rs b/example_projects/pyo3_embed/src/main.rs index b4acdc97a92..b0728fc698e 100644 --- a/example_projects/pyo3_embed/src/main.rs +++ b/example_projects/pyo3_embed/src/main.rs @@ -39,6 +39,8 @@ fn main() { PyTypeError::new_err("This is a type error").restore(py); assert!(PyErr::take(py).is_some()); + py.import("sys")?; + PyResult::Ok(()) }) .unwrap(); From 55b1e14e5146508e5c795181a632849c2aca29fb Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 8 Apr 2026 17:18:50 +0200 Subject: [PATCH 023/135] Add basic call support --- crates/capi/src/abstract_.rs | 77 +++++++++++++++++++++++++ crates/capi/src/lib.rs | 1 + crates/capi/src/object.rs | 22 ++++--- example_projects/pyo3_embed/src/main.rs | 2 + 4 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 crates/capi/src/abstract_.rs diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs new file mode 100644 index 00000000000..82fad911e9a --- /dev/null +++ b/crates/capi/src/abstract_.rs @@ -0,0 +1,77 @@ +use crate::pystate::with_vm; +use rustpython_vm::PyObject; +use rustpython_vm::builtins::{PyStr, PyTuple}; +use std::slice; + +const PY_VECTORCALL_ARGUMENTS_OFFSET: usize = 1usize << (usize::BITS as usize - 1); + +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_CallNoArgs(callable: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + if callable.is_null() { + vm.push_exception(Some(vm.new_system_error( + "PyObject_CallNoArgs called with null callable".to_owned(), + ))); + return std::ptr::null_mut(); + } + + let callable = unsafe { &*callable }; + callable.call((), vm).map_or_else( + |err| { + vm.push_exception(Some(err)); + std::ptr::null_mut() + }, + |result| result.into_raw().as_ptr(), + ) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_VectorcallMethod( + name: *mut PyObject, + args: *const *mut PyObject, + nargsf: usize, + kwnames: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let args_len = nargsf & !PY_VECTORCALL_ARGUMENTS_OFFSET; + let num_positional_args = args_len - 1; + + let (receiver, args) = unsafe { slice::from_raw_parts(args, args_len) } + .split_first() + .expect("PyObject_VectorcallMethod should always have at least one argument"); + + let method_name = unsafe { (&*name).downcast_unchecked_ref::() }; + let callable = match unsafe { + (&**receiver) + .get_attr(method_name, vm) + } { + Ok(obj) => obj, + Err(err) => { + vm.push_exception(Some(err)); + return std::ptr::null_mut() + }, + }; + + let args = args + .iter() + .map(|arg| unsafe { &**arg }.to_owned()) + .collect::>(); + + let kwnames = unsafe { + kwnames + .as_ref() + .map(|tuple| &***tuple.downcast_unchecked_ref::()) + }; + + callable + .vectorcall(args, num_positional_args, kwnames, vm) + .map_or_else( + |err| { + vm.push_exception(Some(err)); + std::ptr::null_mut() + }, + |obj| obj.into_raw().as_ptr(), + ) + }) +} diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 7080bdbae49..35b3f276bda 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -2,6 +2,7 @@ pub use rustpython_vm::PyObject; extern crate alloc; +pub mod abstract_; pub mod bytesobject; pub mod import; pub mod longobject; diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index c5d8b58f14f..c2ee6d8ff99 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -4,7 +4,7 @@ use core::ffi::c_ulong; use rustpython_vm::builtins::PyType; use rustpython_vm::convert::IntoObject; use rustpython_vm::{AsObject, Context, Py}; -use std::ffi::c_uint; +use std::ffi::{c_int, c_uint}; use std::mem::MaybeUninit; const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; @@ -85,12 +85,6 @@ pub extern "C" fn PyType_GetQualName(ptr: *const Py) -> *mut PyObject { with_vm(move |vm| ty.__qualname__(vm).into_object().into_raw().as_ptr()) } -#[unsafe(no_mangle)] -pub extern "C" fn PyObject_CallNoArgs(_callable: *mut PyObject) -> *mut PyObject { - crate::log_stub("PyObject_CallNoArgs"); - std::ptr::null_mut() -} - #[unsafe(no_mangle)] pub extern "C" fn PyObject_GetAttr(_obj: *mut PyObject, _name: *mut PyObject) -> *mut PyObject { crate::log_stub("PyObject_GetAttr"); @@ -125,3 +119,17 @@ pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject { .cast_mut() }) } + +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_IsTrue(obj: *mut PyObject) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + obj.to_owned().is_true(vm).map_or_else( + |err| { + vm.push_exception(Some(err)); + -1 + }, + |is_true| is_true.into(), + ) + }) +} diff --git a/example_projects/pyo3_embed/src/main.rs b/example_projects/pyo3_embed/src/main.rs index b0728fc698e..f93604230e5 100644 --- a/example_projects/pyo3_embed/src/main.rs +++ b/example_projects/pyo3_embed/src/main.rs @@ -19,6 +19,8 @@ fn main() { assert!(string.is_instance_of::()); assert_eq!(string.to_str()?, "Hello, World!"); + assert!(string.call_method1("endswith", ("!",))?.is_truthy()?); + assert_eq!(string.get_type().name()?.to_str()?, "str"); let number = number.unbind(); From 2a7a82ec49e45537d160305724b32c993c79778e Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 8 Apr 2026 20:38:53 +0200 Subject: [PATCH 024/135] Use pyo3 as test harness --- Cargo.lock | 1 + .../capi}/.cargo/config.toml | 4 +- crates/capi/Cargo.toml | 3 ++ crates/capi/pyo3-rustpython.config | 6 +++ crates/capi/src/abstract_.rs | 20 ++++++++ crates/capi/src/bytesobject.rs | 14 ++++++ crates/capi/src/import.rs | 12 +++++ crates/capi/src/longobject.rs | 15 ++++++ crates/capi/src/object.rs | 37 ++++++++++++++ crates/capi/src/pyerrors.rs | 14 ++++++ crates/capi/src/pystate.rs | 22 +++++++++ crates/capi/src/refcount.rs | 26 ++++++++++ crates/capi/src/unicodeobject.rs | 15 ++++++ example_projects/pyo3_embed/Cargo.toml | 12 ----- example_projects/pyo3_embed/README.md | 11 ----- .../pyo3_embed/pyo3-rustpython.config | 9 ---- example_projects/pyo3_embed/src/main.rs | 49 ------------------- 17 files changed, 187 insertions(+), 83 deletions(-) rename {example_projects/pyo3_embed => crates/capi}/.cargo/config.toml (70%) create mode 100644 crates/capi/pyo3-rustpython.config delete mode 100644 example_projects/pyo3_embed/Cargo.toml delete mode 100644 example_projects/pyo3_embed/README.md delete mode 100644 example_projects/pyo3_embed/pyo3-rustpython.config delete mode 100644 example_projects/pyo3_embed/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 1eca00b9fbb..5d70087432a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3108,6 +3108,7 @@ dependencies = [ name = "rustpython-capi" version = "0.5.0" dependencies = [ + "pyo3", "rustpython-vm", ] diff --git a/example_projects/pyo3_embed/.cargo/config.toml b/crates/capi/.cargo/config.toml similarity index 70% rename from example_projects/pyo3_embed/.cargo/config.toml rename to crates/capi/.cargo/config.toml index 54a884e2458..49ff5a492e6 100644 --- a/example_projects/pyo3_embed/.cargo/config.toml +++ b/crates/capi/.cargo/config.toml @@ -1,2 +1,2 @@ -[env] -PYO3_CONFIG_FILE = { value = "pyo3-rustpython.config", relative = true } +[profile.test.env] +PYO3_CONFIG_FILE = { value = "pyo3-rustpython.config", relative = true } \ No newline at end of file diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index 964a996143a..b0106ae223f 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -14,5 +14,8 @@ crate-type = ["staticlib"] [dependencies] rustpython-vm = { workspace = true, features = ["threading"]} +[dev-dependencies] +pyo3 = { version = "0.28.3", features = ["auto-initialize", "abi3-py314"] } + [lints] workspace = true diff --git a/crates/capi/pyo3-rustpython.config b/crates/capi/pyo3-rustpython.config new file mode 100644 index 00000000000..e039bd6d6bc --- /dev/null +++ b/crates/capi/pyo3-rustpython.config @@ -0,0 +1,6 @@ +implementation=CPython +version=3.14 +shared=true +abi3=true +build_flags= +suppress_build_script_link_lines=true diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index 82fad911e9a..b1beb7861c0 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -75,3 +75,23 @@ pub extern "C" fn PyObject_VectorcallMethod( ) }) } + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::PyString; + + #[test] + fn test_call_method1() { + Python::attach(|py| { + let string = PyString::new(py, "Hello, World!"); + assert!( + string + .call_method1("endswith", ("!",)) + .unwrap() + .is_truthy() + .unwrap() + ); + }) + } +} diff --git a/crates/capi/src/bytesobject.rs b/crates/capi/src/bytesobject.rs index ebff4fd158f..82d216f8c48 100644 --- a/crates/capi/src/bytesobject.rs +++ b/crates/capi/src/bytesobject.rs @@ -40,3 +40,17 @@ pub extern "C" fn PyBytes_AsString(bytes: *mut PyObject) -> *mut c_char { bytes.as_bytes().as_ptr() as _ }) } + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::PyBytes; + + #[test] + fn test_bytes() { + Python::attach(|py| { + let bytes = PyBytes::new(py, b"Hello, World!"); + assert_eq!(bytes.as_bytes(), b"Hello, World!"); + }) + } +} diff --git a/crates/capi/src/import.rs b/crates/capi/src/import.rs index fd5b5d668b7..5f74c265761 100644 --- a/crates/capi/src/import.rs +++ b/crates/capi/src/import.rs @@ -15,3 +15,15 @@ pub extern "C" fn PyImport_Import(name: *mut PyObject) -> *mut PyObject { ) }) } + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + + #[test] + fn test_import() { + Python::attach(|py| { + let _module = py.import("sys").unwrap(); + }) + } +} diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index a99e790e557..b8d0ef740b5 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -58,3 +58,18 @@ pub extern "C" fn PyLong_AsLong(obj: *mut PyObject) -> c_long { }) }) } + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::PyInt; + + #[test] + fn test_py_int() { + Python::attach(|py| { + let number = PyInt::new(py, 123); + assert!(number.is_instance_of::()); + assert_eq!(number.extract::().unwrap(), 123); + }) + } +} diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index c2ee6d8ff99..4610e4b17a5 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -133,3 +133,40 @@ pub extern "C" fn PyObject_IsTrue(obj: *mut PyObject) -> c_int { ) }) } + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::{PyBool, PyString}; + + #[test] + fn test_is_truthy() { + Python::attach(|py| { + assert!(!py.None().is_truthy(py).unwrap()); + }) + } + + #[test] + fn test_is_none() { + Python::attach(|py| { + assert!(py.None().is_none(py)); + }) + } + + #[test] + #[cfg(false)] + fn test_bool() { + Python::attach(|py| { + assert!(PyBool::new(py, true).extract::().unwrap()); + assert!(!PyBool::new(py, false).extract::().unwrap()); + }) + } + + #[test] + fn test_type_name() { + Python::attach(|py| { + let string = PyString::new(py, "Hello, World!"); + assert_eq!(string.get_type().name().unwrap().to_str().unwrap(), "str"); + }) + } +} diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index 112e1caf591..0a3ff0fb2a1 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -135,3 +135,17 @@ pub extern "C" fn PyException_GetTraceback(_exc: *mut PyObject) -> *mut PyObject crate::log_stub("PyException_GetTraceback"); std::ptr::null_mut() } + +#[cfg(test)] +mod tests { + use pyo3::exceptions::PyTypeError; + use pyo3::prelude::*; + + #[test] + fn test_raised_exception() { + Python::attach(|py| { + PyTypeError::new_err("This is a type error").restore(py); + assert!(PyErr::take(py).is_some()); + }) + } +} diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index de25dba790a..0e12482fac8 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -51,3 +51,25 @@ pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState { #[unsafe(no_mangle)] pub extern "C" fn PyEval_RestoreThread(_tstate: *mut PyThreadState) {} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::PyInt; + + #[test] + fn test_new_thread() { + Python::attach(|py| { + let number = PyInt::new(py, 123).unbind(); + + std::thread::spawn(move || { + Python::attach(|py| { + let number = number.bind(py); + assert!(number.is_instance_of::()); + }); + }) + .join() + .unwrap(); + }) + } +} diff --git a/crates/capi/src/refcount.rs b/crates/capi/src/refcount.rs index 5e6f55f0460..3a20bdefd1b 100644 --- a/crates/capi/src/refcount.rs +++ b/crates/capi/src/refcount.rs @@ -1,4 +1,5 @@ use crate::PyObject; +use crate::pystate::with_vm; use rustpython_vm::PyObjectRef; use std::ptr::NonNull; @@ -27,3 +28,28 @@ pub extern "C" fn _Py_IncRef(op: *mut PyObject) { std::mem::forget(owned); } + +#[unsafe(no_mangle)] +pub extern "C" fn Py_REFCNT(op: *mut PyObject) -> isize { + with_vm(|vm| unsafe { &*op }.strong_count() as _) +} + +#[cfg(test)] +mod tests { + use pyo3::PyTypeInfo; + use pyo3::prelude::*; + use pyo3::types::PyInt; + + #[test] + fn test_refcount() { + Python::attach(|py| { + let obj = PyInt::type_object(py); + unsafe { pyo3::ffi::Py_REFCNT(obj.as_ptr()) }; + let ref_count = obj.get_refcnt(); + let obj_clone = obj.clone(); + assert_eq!(obj.get_refcnt(), ref_count + 1); + drop(obj_clone); + assert_eq!(obj.get_refcnt(), ref_count); + }); + } +} diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index 837e6e2370a..42a71ece01b 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -64,3 +64,18 @@ pub extern "C" fn PyUnicode_AsEncodedString( pub extern "C" fn PyUnicode_InternInPlace(_string: *mut *mut PyObject) { crate::log_stub("PyUnicode_InternInPlace"); } + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::PyString; + + #[test] + fn test_unicode() { + Python::attach(|py| { + let string = PyString::new(py, "Hello, World!"); + assert!(string.is_instance_of::()); + assert_eq!(string.to_str().unwrap(), "Hello, World!"); + }) + } +} diff --git a/example_projects/pyo3_embed/Cargo.toml b/example_projects/pyo3_embed/Cargo.toml deleted file mode 100644 index d424a486184..00000000000 --- a/example_projects/pyo3_embed/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "example_pyo3_embed" -version = "0.1.0" -edition = "2021" - -[dependencies] -pyo3 = { version = "0.28.3", features = ["abi3-py314"] } -rustpython-capi = { path = "../../crates/capi" } - -[workspace] - -[patch.crates-io] diff --git a/example_projects/pyo3_embed/README.md b/example_projects/pyo3_embed/README.md deleted file mode 100644 index 7d9cc2cf98b..00000000000 --- a/example_projects/pyo3_embed/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# PyO3 embed against RustPython C-API - -This example demonstrates linking `pyo3` against RustPython's minimal C-API shim (`rustpython-capi`) instead of a system CPython library. - -From this directory, run: - -```shell -cargo run -``` - -The local `.cargo/config.toml` sets `PYO3_CONFIG_FILE` automatically. diff --git a/example_projects/pyo3_embed/pyo3-rustpython.config b/example_projects/pyo3_embed/pyo3-rustpython.config deleted file mode 100644 index 2b097508e4f..00000000000 --- a/example_projects/pyo3_embed/pyo3-rustpython.config +++ /dev/null @@ -1,9 +0,0 @@ -implementation=CPython -version=3.14 -shared=false -abi3=true -build_flags= -suppress_build_script_link_lines=true -extra_build_script_line=cargo:rustc-link-search=native=/Users/basschoenmaeckers/repo/RustPython/target/debug -extra_build_script_line=cargo:rustc-link-lib=static=rustpython_capi -extra_build_script_line=cargo:rustc-link-lib=framework=CoreFoundation diff --git a/example_projects/pyo3_embed/src/main.rs b/example_projects/pyo3_embed/src/main.rs deleted file mode 100644 index f93604230e5..00000000000 --- a/example_projects/pyo3_embed/src/main.rs +++ /dev/null @@ -1,49 +0,0 @@ -use pyo3::exceptions::PyTypeError; -use pyo3::prelude::*; -use pyo3::types::{PyBytes, PyInt, PyNone, PyString}; - -#[pyfunction] -fn python_function(py: Python<'_>) -> Borrowed<'_, '_, PyNone> { - PyNone::get(py) -} - -fn main() { - Python::initialize(); - - Python::attach(|py| { - let number = PyInt::new(py, 123); - assert!(number.is_instance_of::()); - assert_eq!(number.extract::()?, 123); - - let string = PyString::new(py, "Hello, World!"); - assert!(string.is_instance_of::()); - assert_eq!(string.to_str()?, "Hello, World!"); - - assert!(string.call_method1("endswith", ("!",))?.is_truthy()?); - - assert_eq!(string.get_type().name()?.to_str()?, "str"); - - let number = number.unbind(); - std::thread::spawn(move || { - Python::attach(|py| { - let number = number.bind(py); - assert!(number.is_instance_of::()); - }); - }) - .join() - .unwrap(); - - assert!(python_function(py).is_none()); - - let bytes = PyBytes::new(py, b"Hello, World!"); - assert_eq!(bytes.as_bytes(), b"Hello, World!"); - - PyTypeError::new_err("This is a type error").restore(py); - assert!(PyErr::take(py).is_some()); - - py.import("sys")?; - - PyResult::Ok(()) - }) - .unwrap(); -} From 96e16b1ac730a54aa498fcbcf1f6a73dbf06c0a8 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 08:52:56 +0200 Subject: [PATCH 025/135] Fix pyo3 by correctly setting `PYO3_CONFIG_FILE` --- crates/capi/.cargo/config.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/capi/.cargo/config.toml b/crates/capi/.cargo/config.toml index 49ff5a492e6..3a880edb36e 100644 --- a/crates/capi/.cargo/config.toml +++ b/crates/capi/.cargo/config.toml @@ -1,2 +1,3 @@ -[profile.test.env] -PYO3_CONFIG_FILE = { value = "pyo3-rustpython.config", relative = true } \ No newline at end of file +[env] +PYO3_CONFIG_FILE = { value = "pyo3-rustpython.config", relative = true } +PYO3_NO_PYTHON = { value = "1" } From e351fb269ba1331afab61a1fbdb47f9456707df0 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 09:43:27 +0200 Subject: [PATCH 026/135] Fix python bool creation test --- Cargo.lock | 16 +++++----------- Cargo.toml | 3 ++- crates/capi/Cargo.toml | 2 +- crates/capi/src/object.rs | 10 ++++++---- crates/capi/src/pylifecycle.rs | 3 ++- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d70087432a..da41f383f0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2644,8 +2644,7 @@ dependencies = [ [[package]] name = "pyo3" version = "0.28.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" +source = "git+https://github.com/PyO3/pyo3#e662975d241c56c44dfeab8651d3f79ec1b23bff" dependencies = [ "libc", "once_cell", @@ -2658,8 +2657,7 @@ dependencies = [ [[package]] name = "pyo3-build-config" version = "0.28.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" +source = "git+https://github.com/PyO3/pyo3#e662975d241c56c44dfeab8651d3f79ec1b23bff" dependencies = [ "target-lexicon", ] @@ -2667,8 +2665,7 @@ dependencies = [ [[package]] name = "pyo3-ffi" version = "0.28.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" +source = "git+https://github.com/PyO3/pyo3#e662975d241c56c44dfeab8651d3f79ec1b23bff" dependencies = [ "libc", "pyo3-build-config", @@ -2677,8 +2674,7 @@ dependencies = [ [[package]] name = "pyo3-macros" version = "0.28.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" +source = "git+https://github.com/PyO3/pyo3#e662975d241c56c44dfeab8651d3f79ec1b23bff" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2689,12 +2685,10 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" version = "0.28.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" +source = "git+https://github.com/PyO3/pyo3#e662975d241c56c44dfeab8651d3f79ec1b23bff" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 7bd8b8f3374..61dae08d882 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ rustyline = { workspace = true } [dev-dependencies] criterion = { workspace = true } -pyo3 = { version = "0.28.2", features = ["auto-initialize"] } +pyo3 = { git = "https://github.com/PyO3/pyo3", features = ["auto-initialize"] } rustpython-stdlib = { workspace = true } ruff_python_parser = { workspace = true } @@ -102,6 +102,7 @@ lto = "thin" [patch.crates-io] parking_lot_core = { git = "https://github.com/youknowone/parking_lot", branch = "rustpython" } +pyo3-ffi = { git = "https://github.com/PyO3/pyo3" } # REDOX START, Uncomment when you want to compile/check with redoxer # REDOX END diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index b0106ae223f..e4e46ecc260 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -15,7 +15,7 @@ crate-type = ["staticlib"] rustpython-vm = { workspace = true, features = ["threading"]} [dev-dependencies] -pyo3 = { version = "0.28.3", features = ["auto-initialize", "abi3-py314"] } +pyo3 = { git = "https://github.com/PyO3/pyo3", features = ["auto-initialize", "abi3-py314"] } [lints] workspace = true diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 4610e4b17a5..c71be05e07f 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -28,6 +28,9 @@ pub static mut PyTuple_Type: MaybeUninit<&'static Py> = MaybeUninit::uni #[unsafe(no_mangle)] pub static mut PyUnicode_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); +#[unsafe(no_mangle)] +pub static mut PyBool_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); + #[unsafe(no_mangle)] pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const Py { // SAFETY: The caller must guarantee that `op` is a valid pointer to a `PyObject`. @@ -109,7 +112,7 @@ pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject { let ctx = &vm.ctx; match constant_id { 0 => ctx.none.as_object(), - 1 => ctx.true_value.as_object(), + 1 => ctx.false_value.as_object(), 2 => ctx.true_value.as_object(), 3 => ctx.ellipsis.as_object(), 4 => ctx.not_implemented.as_object(), @@ -154,11 +157,10 @@ mod tests { } #[test] - #[cfg(false)] fn test_bool() { Python::attach(|py| { - assert!(PyBool::new(py, true).extract::().unwrap()); - assert!(!PyBool::new(py, false).extract::().unwrap()); + assert!(PyBool::new(py, true).is_truthy().unwrap()); + assert!(!PyBool::new(py, false).is_truthy().unwrap()); }) } diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 644639814cb..47ff9559938 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,5 +1,5 @@ use crate::log_stub; -use crate::object::{PyLong_Type, PyTuple_Type, PyType_Type, PyUnicode_Type}; +use crate::object::{PyBool_Type, PyLong_Type, PyTuple_Type, PyType_Type, PyUnicode_Type}; use crate::pyerrors::{PyExc_BaseException, PyExc_OverflowError, PyExc_TypeError}; use crate::pystate::attach_vm_to_thread; use core::ffi::c_int; @@ -40,6 +40,7 @@ pub(crate) fn init_static_type_pointers() { PyLong_Type.write(types.int_type); PyTuple_Type.write(types.tuple_type); PyUnicode_Type.write(types.str_type); + PyBool_Type.write(types.bool_type); let exc = &context.exceptions; PyExc_BaseException.write(exc.base_exception_type.as_object().as_raw().cast_mut()); From 2fece20c88cfe573878af57a49868cd5b3e75121 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 09:47:04 +0200 Subject: [PATCH 027/135] Implement `PyUnicode_EqualToUTF8AndSize` --- crates/capi/src/unicodeobject.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index 42a71ece01b..ea81611c822 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -1,6 +1,6 @@ use crate::PyObject; use crate::pystate::with_vm; -use core::ffi::c_char; +use core::ffi::{c_char, c_int}; use core::ptr; use core::slice; use core::str; @@ -65,6 +65,25 @@ pub extern "C" fn PyUnicode_InternInPlace(_string: *mut *mut PyObject) { crate::log_stub("PyUnicode_InternInPlace"); } +#[unsafe(no_mangle)] +pub extern "C" fn PyUnicode_EqualToUTF8AndSize( + unicode: *mut PyObject, + string: *const c_char, + size: isize, +) -> c_int { + with_vm(|_vm| { + let unicode = unsafe { (&*unicode).downcast_unchecked_ref::() }; + unsafe { + let slice = slice::from_raw_parts(string as _, size as _); + str::from_utf8(slice) + } + .ok() + .and_then(|other| Some(unicode.to_str()? == other)) + .unwrap_or(false) + .into() + }) +} + #[cfg(test)] mod tests { use pyo3::prelude::*; @@ -76,6 +95,7 @@ mod tests { let string = PyString::new(py, "Hello, World!"); assert!(string.is_instance_of::()); assert_eq!(string.to_str().unwrap(), "Hello, World!"); + assert_eq!(string, "Hello, World!"); }) } } From e56029d1904ff7692ef4a30b0aad380965619113 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 10:10:54 +0200 Subject: [PATCH 028/135] Add `PyExc_SystemError` --- crates/capi/src/pyerrors.rs | 3 +++ crates/capi/src/pylifecycle.rs | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index 0a3ff0fb2a1..28de4002f92 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -9,6 +9,9 @@ use std::mem::MaybeUninit; #[unsafe(no_mangle)] pub static mut PyExc_BaseException: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); +#[unsafe(no_mangle)] +pub static mut PyExc_SystemError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); + #[unsafe(no_mangle)] pub static mut PyExc_TypeError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 47ff9559938..a514fdb0c5d 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,6 +1,8 @@ use crate::log_stub; use crate::object::{PyBool_Type, PyLong_Type, PyTuple_Type, PyType_Type, PyUnicode_Type}; -use crate::pyerrors::{PyExc_BaseException, PyExc_OverflowError, PyExc_TypeError}; +use crate::pyerrors::{ + PyExc_BaseException, PyExc_OverflowError, PyExc_SystemError, PyExc_TypeError, +}; use crate::pystate::attach_vm_to_thread; use core::ffi::c_int; use rustpython_vm::vm::thread::ThreadedVirtualMachine; @@ -44,6 +46,7 @@ pub(crate) fn init_static_type_pointers() { let exc = &context.exceptions; PyExc_BaseException.write(exc.base_exception_type.as_object().as_raw().cast_mut()); + PyExc_SystemError.write(exc.system_error.as_object().as_raw().cast_mut()); PyExc_TypeError.write(exc.type_error.as_object().as_raw().cast_mut()); PyExc_OverflowError.write(exc.overflow_error.as_object().as_raw().cast_mut()); }; From 9702a47fce7ff1f57c4891e7df4f3295ac307c05 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 10:14:00 +0200 Subject: [PATCH 029/135] Compile to dynamic lib --- crates/capi/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index e4e46ecc260..9cc65b015f1 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true license.workspace = true [lib] -crate-type = ["staticlib"] +crate-type = ["cdylib"] [dependencies] rustpython-vm = { workspace = true, features = ["threading"]} From 738d4d42d63157c92fbb72231bf94907adc78e4d Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 10:31:55 +0200 Subject: [PATCH 030/135] Add test for static type pointers --- crates/capi/src/object.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index c71be05e07f..829cffef46f 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -140,7 +140,7 @@ pub extern "C" fn PyObject_IsTrue(obj: *mut PyObject) -> c_int { #[cfg(test)] mod tests { use pyo3::prelude::*; - use pyo3::types::{PyBool, PyString}; + use pyo3::types::{PyBool, PyNone, PyString}; #[test] fn test_is_truthy() { @@ -171,4 +171,13 @@ mod tests { assert_eq!(string.get_type().name().unwrap().to_str().unwrap(), "str"); }) } + + #[test] + #[ignore = "Instance checking on static type pointers is yet supported"] + fn test_static_type_pointers() { + Python::attach(|py| { + assert!(py.None().bind(py).is_instance_of::()); + assert!(PyBool::new(py, true).is_instance_of::()); + }) + } } From 8ad7e0cef1028761a9438c9c2e9df3e0583b68af Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 11:15:23 +0200 Subject: [PATCH 031/135] Streamline returning PyResult --- crates/capi/src/abstract_.rs | 49 +++--------- crates/capi/src/bytesobject.rs | 28 +++---- crates/capi/src/import.rs | 13 +-- crates/capi/src/lib.rs | 2 + crates/capi/src/longobject.rs | 32 +++----- crates/capi/src/object.rs | 17 +--- crates/capi/src/pyerrors.rs | 17 +--- crates/capi/src/pystate.rs | 5 +- crates/capi/src/refcount.rs | 5 +- crates/capi/src/unicodeobject.rs | 28 +++---- crates/capi/src/util.rs | 133 +++++++++++++++++++++++++++++++ 11 files changed, 196 insertions(+), 133 deletions(-) create mode 100644 crates/capi/src/util.rs diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index b1beb7861c0..f9947b3fff3 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -1,6 +1,6 @@ -use crate::pystate::with_vm; -use rustpython_vm::PyObject; +use crate::with_vm; use rustpython_vm::builtins::{PyStr, PyTuple}; +use rustpython_vm::{PyObject, PyObjectRef, PyResult}; use std::slice; const PY_VECTORCALL_ARGUMENTS_OFFSET: usize = 1usize << (usize::BITS as usize - 1); @@ -9,20 +9,13 @@ const PY_VECTORCALL_ARGUMENTS_OFFSET: usize = 1usize << (usize::BITS as usize - pub extern "C" fn PyObject_CallNoArgs(callable: *mut PyObject) -> *mut PyObject { with_vm(|vm| { if callable.is_null() { - vm.push_exception(Some(vm.new_system_error( - "PyObject_CallNoArgs called with null callable".to_owned(), - ))); - return std::ptr::null_mut(); + return Err( + vm.new_system_error("PyObject_CallNoArgs called with null callable".to_owned()) + ); } let callable = unsafe { &*callable }; - callable.call((), vm).map_or_else( - |err| { - vm.push_exception(Some(err)); - std::ptr::null_mut() - }, - |result| result.into_raw().as_ptr(), - ) + callable.call((), vm) }) } @@ -33,7 +26,7 @@ pub extern "C" fn PyObject_VectorcallMethod( nargsf: usize, kwnames: *mut PyObject, ) -> *mut PyObject { - with_vm(|vm| { + with_vm::(|vm| { let args_len = nargsf & !PY_VECTORCALL_ARGUMENTS_OFFSET; let num_positional_args = args_len - 1; @@ -41,38 +34,22 @@ pub extern "C" fn PyObject_VectorcallMethod( .split_first() .expect("PyObject_VectorcallMethod should always have at least one argument"); - let method_name = unsafe { (&*name).downcast_unchecked_ref::() }; - let callable = match unsafe { - (&**receiver) - .get_attr(method_name, vm) - } { - Ok(obj) => obj, - Err(err) => { - vm.push_exception(Some(err)); - return std::ptr::null_mut() - }, - }; + let method_name = unsafe { (&*name).try_downcast_ref::(vm)? }; + let callable = unsafe { (&**receiver).get_attr(method_name, vm)? }; let args = args .iter() .map(|arg| unsafe { &**arg }.to_owned()) .collect::>(); - let kwnames = unsafe { + let kwnames: Option<&[PyObjectRef]> = unsafe { kwnames .as_ref() - .map(|tuple| &***tuple.downcast_unchecked_ref::()) + .map(|tuple| Ok(&***tuple.try_downcast_ref::(vm)?)) + .transpose()? }; - callable - .vectorcall(args, num_positional_args, kwnames, vm) - .map_or_else( - |err| { - vm.push_exception(Some(err)); - std::ptr::null_mut() - }, - |obj| obj.into_raw().as_ptr(), - ) + callable.vectorcall(args, num_positional_args, kwnames, vm) }) } diff --git a/crates/capi/src/bytesobject.rs b/crates/capi/src/bytesobject.rs index 82d216f8c48..b955ca56e6d 100644 --- a/crates/capi/src/bytesobject.rs +++ b/crates/capi/src/bytesobject.rs @@ -1,43 +1,33 @@ -use crate::PyObject; -use crate::pystate::with_vm; +use crate::{PyObject, with_vm}; use core::ffi::c_char; use rustpython_vm::builtins::PyBytes; -use rustpython_vm::convert::IntoObject; #[unsafe(no_mangle)] pub extern "C" fn PyBytes_FromStringAndSize(bytes: *mut c_char, len: isize) -> *mut PyObject { with_vm(|vm| { if bytes.is_null() { - std::ptr::null_mut() + todo!("PyBytes_FromStringAndSize with null bytes is not yet implemented"); } else { let bytes_slice = unsafe { core::slice::from_raw_parts(bytes as *const u8, len as usize) }; - vm.ctx - .new_bytes(bytes_slice.to_vec()) - .into_object() - .into_raw() - .as_ptr() + vm.ctx.new_bytes(bytes_slice.to_vec()) } }) } #[unsafe(no_mangle)] pub extern "C" fn PyBytes_Size(bytes: *mut PyObject) -> isize { - with_vm(|_vm| { - let bytes = unsafe { &*bytes } - .downcast_ref::() - .expect("PyBytes_Size argument must be a bytes object"); - bytes.as_bytes().len() as _ + with_vm(|vm| { + let bytes = unsafe { &*bytes }.try_downcast_ref::(vm)?; + Ok(bytes.as_bytes().len() as isize) }) } #[unsafe(no_mangle)] pub extern "C" fn PyBytes_AsString(bytes: *mut PyObject) -> *mut c_char { - with_vm(|_vm| { - let bytes = unsafe { &*bytes } - .downcast_ref::() - .expect("PyBytes_AsString argument must be a bytes object"); - bytes.as_bytes().as_ptr() as _ + with_vm(|vm| { + let bytes = unsafe { &*bytes }.try_downcast_ref::(vm)?; + Ok(bytes.as_bytes().as_ptr() as *mut c_char) }) } diff --git a/crates/capi/src/import.rs b/crates/capi/src/import.rs index 5f74c265761..f3af06d2217 100644 --- a/crates/capi/src/import.rs +++ b/crates/capi/src/import.rs @@ -1,18 +1,11 @@ -use crate::PyObject; -use crate::pystate::with_vm; +use crate::{PyObject, with_vm}; use rustpython_vm::builtins::PyStr; #[unsafe(no_mangle)] pub extern "C" fn PyImport_Import(name: *mut PyObject) -> *mut PyObject { with_vm(|vm| { - let name = unsafe { (&*name).downcast_unchecked_ref::() }; - vm.import(name, 0).map_or_else( - |err| { - vm.push_exception(Some(err)); - std::ptr::null_mut() - }, - |module| module.into_raw().as_ptr(), - ) + let name = unsafe { (&*name).try_downcast_ref::(vm)? }; + vm.import(name, 0) }) } diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 35b3f276bda..ab8ba27a7b9 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -1,3 +1,4 @@ +use crate::pystate::with_vm; pub use rustpython_vm::PyObject; extern crate alloc; @@ -14,6 +15,7 @@ pub mod refcount; pub mod traceback; pub mod tupleobject; pub mod unicodeobject; +pub(crate) mod util; #[inline] pub(crate) fn log_stub(name: &str) { diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index b8d0ef740b5..814c4a649cc 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -1,39 +1,36 @@ +use crate::{PyObject, with_vm}; use core::ffi::{c_long, c_longlong, c_ulong, c_ulonglong}; - -use crate::PyObject; -use crate::pyerrors::{PyErr_SetString, PyExc_OverflowError}; -use crate::pystate::with_vm; +use rustpython_vm::PyResult; use rustpython_vm::builtins::PyInt; -use rustpython_vm::convert::IntoObject; #[unsafe(no_mangle)] pub extern "C" fn PyLong_FromLong(value: c_long) -> *mut PyObject { - with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr()) + with_vm(|vm| vm.ctx.new_int(value)) } #[unsafe(no_mangle)] pub extern "C" fn PyLong_FromLongLong(value: c_longlong) -> *mut PyObject { - with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr()) + with_vm(|vm| vm.ctx.new_int(value)) } #[unsafe(no_mangle)] pub extern "C" fn PyLong_FromSsize_t(value: isize) -> *mut PyObject { - with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr()) + with_vm(|vm| vm.ctx.new_int(value)) } #[unsafe(no_mangle)] pub extern "C" fn PyLong_FromSize_t(value: usize) -> *mut PyObject { - with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr()) + with_vm(|vm| vm.ctx.new_int(value)) } #[unsafe(no_mangle)] pub extern "C" fn PyLong_FromUnsignedLong(value: c_ulong) -> *mut PyObject { - with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr()) + with_vm(|vm| vm.ctx.new_int(value)) } #[unsafe(no_mangle)] pub extern "C" fn PyLong_FromUnsignedLongLong(value: c_ulonglong) -> *mut PyObject { - with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr()) + with_vm(|vm| vm.ctx.new_int(value)) } #[unsafe(no_mangle)] @@ -42,20 +39,17 @@ pub extern "C" fn PyLong_AsLong(obj: *mut PyObject) -> c_long { panic!("PyLong_AsLong called with null object"); } - with_vm(|_vm| { + with_vm::>(|vm| { // SAFETY: non-null checked above; caller promises a valid PyObject pointer. let obj_ref = unsafe { &*obj }; let int_obj = obj_ref .downcast_ref::() .expect("PyLong_AsLong currently only accepts int instances"); - int_obj.as_bigint().try_into().unwrap_or_else(|_| unsafe { - PyErr_SetString( - PyExc_OverflowError.assume_init(), - c"Python int too large to convert to C long".as_ptr(), - ); - -1 - }) + int_obj + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C long")) }) } diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 829cffef46f..154b2ce28f6 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,8 +1,6 @@ -use crate::PyObject; -use crate::pystate::with_vm; +use crate::{PyObject, with_vm}; use core::ffi::c_ulong; use rustpython_vm::builtins::PyType; -use rustpython_vm::convert::IntoObject; use rustpython_vm::{AsObject, Context, Py}; use std::ffi::{c_int, c_uint}; use std::mem::MaybeUninit; @@ -79,13 +77,13 @@ pub extern "C" fn PyType_GetFlags(ptr: *const Py) -> c_ulong { #[unsafe(no_mangle)] pub extern "C" fn PyType_GetName(ptr: *const Py) -> *mut PyObject { let ty = unsafe { &*ptr }; - with_vm(move |vm| ty.__name__(vm).into_object().into_raw().as_ptr()) + with_vm(move |vm| ty.__name__(vm)) } #[unsafe(no_mangle)] pub extern "C" fn PyType_GetQualName(ptr: *const Py) -> *mut PyObject { let ty = unsafe { &*ptr }; - with_vm(move |vm| ty.__qualname__(vm).into_object().into_raw().as_ptr()) + with_vm(move |vm| ty.__qualname__(vm)) } #[unsafe(no_mangle)] @@ -119,7 +117,6 @@ pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject { _ => panic!("Invalid constant_id passed to Py_GetConstantBorrowed"), } .as_raw() - .cast_mut() }) } @@ -127,13 +124,7 @@ pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject { pub extern "C" fn PyObject_IsTrue(obj: *mut PyObject) -> c_int { with_vm(|vm| { let obj = unsafe { &*obj }; - obj.to_owned().is_true(vm).map_or_else( - |err| { - vm.push_exception(Some(err)); - -1 - }, - |is_true| is_true.into(), - ) + obj.to_owned().is_true(vm) }) } diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index 28de4002f92..6dd3c310571 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -1,8 +1,6 @@ -use crate::PyObject; -use crate::pystate::with_vm; +use crate::{PyObject, with_vm}; use core::ffi::{c_char, c_int}; use rustpython_vm::builtins::{PyTuple, PyType}; -use rustpython_vm::convert::ToPyObject; use std::ffi::CStr; use std::mem::MaybeUninit; @@ -20,12 +18,7 @@ pub static mut PyExc_OverflowError: MaybeUninit<*mut PyObject> = MaybeUninit::un #[unsafe(no_mangle)] pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject { - with_vm(|vm| { - vm.take_raised_exception().map_or_else( - || std::ptr::null_mut(), - |exc| exc.to_pyobject(vm).into_raw().as_ptr(), - ) - }) + with_vm(|vm| vm.take_raised_exception()) } #[unsafe(no_mangle)] @@ -115,11 +108,7 @@ pub extern "C" fn PyErr_NewException( "PyErr_NewException with non-null dict is not supported yet" ); - vm.ctx - .new_exception_type(module, name, bases) - .to_pyobject(vm) - .into_raw() - .as_ptr() + vm.ctx.new_exception_type(module, name, bases) }) } diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index 0e12482fac8..2e69d0422db 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -1,4 +1,5 @@ use crate::pylifecycle::request_vm_from_interpreter; +use crate::util::FfiResult; use core::ffi::c_int; use core::ptr; use rustpython_vm::VirtualMachine; @@ -9,13 +10,13 @@ thread_local! { static VM: RefCell> = const { RefCell::new(None) }; } -pub fn with_vm(f: impl FnOnce(&VirtualMachine) -> R) -> R { +pub(crate) fn with_vm(f: impl FnOnce(&VirtualMachine) -> R) -> R::Output { VM.with(|vm_ref| { let vm = vm_ref.borrow(); let vm = vm .as_ref() .expect("Thread was not attached to an interpreter"); - vm.run(f) + vm.run(|vm| f(vm).into_output(vm)) }) } diff --git a/crates/capi/src/refcount.rs b/crates/capi/src/refcount.rs index 3a20bdefd1b..f58691bd46d 100644 --- a/crates/capi/src/refcount.rs +++ b/crates/capi/src/refcount.rs @@ -1,5 +1,4 @@ -use crate::PyObject; -use crate::pystate::with_vm; +use crate::{PyObject, with_vm}; use rustpython_vm::PyObjectRef; use std::ptr::NonNull; @@ -31,7 +30,7 @@ pub extern "C" fn _Py_IncRef(op: *mut PyObject) { #[unsafe(no_mangle)] pub extern "C" fn Py_REFCNT(op: *mut PyObject) -> isize { - with_vm(|vm| unsafe { &*op }.strong_count() as _) + with_vm(|vm| unsafe { &*op }.strong_count() as isize) } #[cfg(test)] diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index ea81611c822..982c791557f 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -1,10 +1,8 @@ -use crate::PyObject; -use crate::pystate::with_vm; +use crate::{PyObject, with_vm}; use core::ffi::{c_char, c_int}; use core::ptr; use core::slice; use core::str; -use rustpython_vm::PyObjectRef; use rustpython_vm::builtins::PyStr; #[unsafe(no_mangle)] @@ -21,23 +19,18 @@ pub extern "C" fn PyUnicode_FromStringAndSize(s: *const c_char, len: isize) -> * str::from_utf8(bytes).expect("PyUnicode_FromStringAndSize got non-UTF8 data") }; - with_vm(|vm| { - let obj: PyObjectRef = vm.ctx.new_str(text).into(); - obj.into_raw().as_ptr() - }) + with_vm(|vm| vm.ctx.new_str(text)) } #[unsafe(no_mangle)] pub extern "C" fn PyUnicode_AsUTF8AndSize(obj: *mut PyObject, size: *mut isize) -> *const c_char { - with_vm(|_vm| { + with_vm(|vm| { let obj = unsafe { obj.as_ref() .expect("PyUnicode_AsUTF8AndSize called with null pointer") }; - let unicode = obj - .downcast_ref::() - .expect("PyUnicode_AsUTF8AndSize called with non-unicode object"); + let unicode = obj.try_downcast_ref::(vm)?; let str = unicode .to_str() @@ -46,7 +39,7 @@ pub extern "C" fn PyUnicode_AsUTF8AndSize(obj: *mut PyObject, size: *mut isize) if !size.is_null() { unsafe { *size = str.len() as isize }; } - str.as_ptr() as _ + Ok(str.as_ptr() as *const c_char) }) } @@ -71,16 +64,17 @@ pub extern "C" fn PyUnicode_EqualToUTF8AndSize( string: *const c_char, size: isize, ) -> c_int { - with_vm(|_vm| { - let unicode = unsafe { (&*unicode).downcast_unchecked_ref::() }; - unsafe { + with_vm(|vm| { + let unicode = unsafe { &*unicode }.try_downcast_ref::(vm)?; + let result = unsafe { let slice = slice::from_raw_parts(string as _, size as _); str::from_utf8(slice) } .ok() .and_then(|other| Some(unicode.to_str()? == other)) - .unwrap_or(false) - .into() + .unwrap_or(false); + + Ok(result) }) } diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs new file mode 100644 index 00000000000..096c393d7ec --- /dev/null +++ b/crates/capi/src/util.rs @@ -0,0 +1,133 @@ +use core::ffi::c_long; +use rustpython_vm::{PyObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; +use std::ffi::{c_char, c_int}; + +pub(crate) trait FfiResult { + type Output; + fn into_output(self, vm: &VirtualMachine) -> Self::Output; +} + +impl FfiResult for () { + type Output = (); + + fn into_output(self, _vm: &VirtualMachine) -> Self::Output { + self + } +} + +impl FfiResult for PyRef +where + Self: Into, +{ + type Output = *mut PyObject; + + fn into_output(self, _vm: &VirtualMachine) -> Self::Output { + self.into().into_raw().as_ptr() + } +} + +impl FfiResult for Option> +where + PyRef: Into, +{ + type Output = *mut PyObject; + + fn into_output(self, _vm: &VirtualMachine) -> Self::Output { + self.map_or_else( + || std::ptr::null_mut(), + |obj| obj.into().into_raw().as_ptr(), + ) + } +} + +impl FfiResult for PyObjectRef { + type Output = *mut PyObject; + + fn into_output(self, _vm: &VirtualMachine) -> Self::Output { + self.into_raw().as_ptr() + } +} +impl FfiResult for *const PyObject { + type Output = *mut PyObject; + + fn into_output(self, _vm: &VirtualMachine) -> Self::Output { + self.cast_mut() + } +} + +impl FfiResult for PyResult { + type Output = *mut PyObject; + + fn into_output(self, vm: &VirtualMachine) -> Self::Output { + self.map_or_else( + |err| { + vm.push_exception(Some(err)); + std::ptr::null_mut() + }, + |obj| obj.into_raw().as_ptr(), + ) + } +} + +impl FfiResult for PyResult { + type Output = c_long; + + fn into_output(self, vm: &VirtualMachine) -> Self::Output { + self.unwrap_or_else(|err| { + vm.push_exception(Some(err)); + -1 + }) + } +} + +impl FfiResult for isize { + type Output = isize; + + fn into_output(self, _vm: &VirtualMachine) -> Self::Output { + self + } +} + +impl FfiResult for PyResult { + type Output = isize; + + fn into_output(self, vm: &VirtualMachine) -> Self::Output { + self.unwrap_or_else(|err| { + vm.push_exception(Some(err)); + -1 + }) + } +} + +impl FfiResult for PyResult { + type Output = c_int; + + fn into_output(self, vm: &VirtualMachine) -> Self::Output { + self.map_or_else( + |err| { + vm.push_exception(Some(err)); + -1 + }, + Into::into, + ) + } +} + +impl FfiResult for PyResult<*mut c_char> { + type Output = *mut c_char; + + fn into_output(self, vm: &VirtualMachine) -> Self::Output { + self.map(|ptr| ptr.cast_const()).into_output(vm).cast_mut() + } +} + +impl FfiResult for PyResult<*const c_char> { + type Output = *const c_char; + + fn into_output(self, vm: &VirtualMachine) -> Self::Output { + self.unwrap_or_else(|err| { + vm.push_exception(Some(err)); + core::ptr::null_mut() + }) + } +} From 32faf149c5bbfcf7a47f13aa66b737b433962bbf Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 11:16:16 +0200 Subject: [PATCH 032/135] Fix some clippy warning --- crates/capi/src/abstract_.rs | 2 +- crates/capi/src/object.rs | 10 +++++----- crates/capi/src/pyerrors.rs | 6 +++--- crates/capi/src/pystate.rs | 6 +++--- crates/capi/src/refcount.rs | 17 ++++++++--------- crates/capi/src/util.rs | 9 +++------ 6 files changed, 23 insertions(+), 27 deletions(-) diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index f9947b3fff3..a11c8c6ccf1 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -1,7 +1,7 @@ use crate::with_vm; +use alloc::slice; use rustpython_vm::builtins::{PyStr, PyTuple}; use rustpython_vm::{PyObject, PyObjectRef, PyResult}; -use std::slice; const PY_VECTORCALL_ARGUMENTS_OFFSET: usize = 1usize << (usize::BITS as usize - 1); diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 154b2ce28f6..bdeb56109f2 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,9 +1,9 @@ use crate::{PyObject, with_vm}; use core::ffi::c_ulong; +use core::ffi::{c_int, c_uint}; +use core::mem::MaybeUninit; use rustpython_vm::builtins::PyType; use rustpython_vm::{AsObject, Context, Py}; -use std::ffi::{c_int, c_uint}; -use std::mem::MaybeUninit; const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; const PY_TPFLAGS_LIST_SUBCLASS: c_ulong = 1 << 25; @@ -89,19 +89,19 @@ pub extern "C" fn PyType_GetQualName(ptr: *const Py) -> *mut PyObject { #[unsafe(no_mangle)] pub extern "C" fn PyObject_GetAttr(_obj: *mut PyObject, _name: *mut PyObject) -> *mut PyObject { crate::log_stub("PyObject_GetAttr"); - std::ptr::null_mut() + core::ptr::null_mut() } #[unsafe(no_mangle)] pub extern "C" fn PyObject_Repr(_obj: *mut PyObject) -> *mut PyObject { crate::log_stub("PyObject_Repr"); - std::ptr::null_mut() + core::ptr::null_mut() } #[unsafe(no_mangle)] pub extern "C" fn PyObject_Str(_obj: *mut PyObject) -> *mut PyObject { crate::log_stub("PyObject_Str"); - std::ptr::null_mut() + core::ptr::null_mut() } #[unsafe(no_mangle)] diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index 6dd3c310571..dd5516ed307 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -1,8 +1,8 @@ use crate::{PyObject, with_vm}; +use core::ffi::CStr; use core::ffi::{c_char, c_int}; +use core::mem::MaybeUninit; use rustpython_vm::builtins::{PyTuple, PyType}; -use std::ffi::CStr; -use std::mem::MaybeUninit; #[unsafe(no_mangle)] pub static mut PyExc_BaseException: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); @@ -125,7 +125,7 @@ pub extern "C" fn PyErr_NewExceptionWithDoc( #[unsafe(no_mangle)] pub extern "C" fn PyException_GetTraceback(_exc: *mut PyObject) -> *mut PyObject { crate::log_stub("PyException_GetTraceback"); - std::ptr::null_mut() + core::ptr::null_mut() } #[cfg(test)] diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index 2e69d0422db..36995cdd1dc 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -1,10 +1,10 @@ use crate::pylifecycle::request_vm_from_interpreter; use crate::util::FfiResult; +use core::cell::RefCell; use core::ffi::c_int; use core::ptr; use rustpython_vm::VirtualMachine; use rustpython_vm::vm::thread::ThreadedVirtualMachine; -use std::cell::RefCell; thread_local! { static VM: RefCell> = const { RefCell::new(None) }; @@ -25,13 +25,13 @@ type PyGILState_STATE = c_int; #[repr(C)] pub struct PyThreadState { - _interp: *mut std::ffi::c_void, + _interp: *mut core::ffi::c_void, } pub(crate) fn attach_vm_to_thread() { VM.with(|vm| { vm.borrow_mut() - .get_or_insert_with(|| request_vm_from_interpreter()); + .get_or_insert_with(request_vm_from_interpreter); }); } diff --git a/crates/capi/src/refcount.rs b/crates/capi/src/refcount.rs index f58691bd46d..ebb284d65df 100644 --- a/crates/capi/src/refcount.rs +++ b/crates/capi/src/refcount.rs @@ -1,6 +1,6 @@ use crate::{PyObject, with_vm}; +use core::ptr::NonNull; use rustpython_vm::PyObjectRef; -use std::ptr::NonNull; #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] @@ -25,30 +25,29 @@ pub extern "C" fn _Py_IncRef(op: *mut PyObject) { // SAFETY: op is non-null and expected to be a valid pointer for this shim. let owned = unsafe { (*op).to_owned() }; - std::mem::forget(owned); + core::mem::forget(owned); } #[unsafe(no_mangle)] pub extern "C" fn Py_REFCNT(op: *mut PyObject) -> isize { - with_vm(|vm| unsafe { &*op }.strong_count() as isize) + with_vm(|_vm| unsafe { &*op }.strong_count() as isize) } #[cfg(test)] mod tests { - use pyo3::PyTypeInfo; use pyo3::prelude::*; use pyo3::types::PyInt; + use pyo3::{PyTypeInfo, ffi}; #[test] fn test_refcount() { - Python::attach(|py| { + Python::attach(|py| unsafe { let obj = PyInt::type_object(py); - unsafe { pyo3::ffi::Py_REFCNT(obj.as_ptr()) }; - let ref_count = obj.get_refcnt(); + let ref_count = ffi::Py_REFCNT(obj.as_ptr()); let obj_clone = obj.clone(); - assert_eq!(obj.get_refcnt(), ref_count + 1); + assert_eq!(ffi::Py_REFCNT(obj.as_ptr()), ref_count + 1); drop(obj_clone); - assert_eq!(obj.get_refcnt(), ref_count); + assert_eq!(ffi::Py_REFCNT(obj.as_ptr()), ref_count); }); } } diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 096c393d7ec..5013696c9d6 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -1,6 +1,6 @@ use core::ffi::c_long; +use core::ffi::{c_char, c_int}; use rustpython_vm::{PyObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; -use std::ffi::{c_char, c_int}; pub(crate) trait FfiResult { type Output; @@ -33,10 +33,7 @@ where type Output = *mut PyObject; fn into_output(self, _vm: &VirtualMachine) -> Self::Output { - self.map_or_else( - || std::ptr::null_mut(), - |obj| obj.into().into_raw().as_ptr(), - ) + self.map_or_else(core::ptr::null_mut, |obj| obj.into().into_raw().as_ptr()) } } @@ -62,7 +59,7 @@ impl FfiResult for PyResult { self.map_or_else( |err| { vm.push_exception(Some(err)); - std::ptr::null_mut() + core::ptr::null_mut() }, |obj| obj.into_raw().as_ptr(), ) From a34ced91624b643fd67fc158c3e1120f947804b7 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 11:42:19 +0200 Subject: [PATCH 033/135] Use bounded channel for vm request response --- crates/capi/src/pylifecycle.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index a514fdb0c5d..75b3b1caf15 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -9,7 +9,7 @@ use rustpython_vm::vm::thread::ThreadedVirtualMachine; use rustpython_vm::{AsObject, Context, Interpreter}; use std::sync::{Once, OnceLock, mpsc}; -static VM_REQUEST_TX: OnceLock>> = +static VM_REQUEST_TX: OnceLock>> = OnceLock::new(); pub(crate) static INITIALIZED: Once = Once::new(); @@ -18,7 +18,7 @@ pub(crate) fn request_vm_from_interpreter() -> ThreadedVirtualMachine { let tx = VM_REQUEST_TX .get() .expect("VM request channel not initialized"); - let (response_tx, response_rx) = mpsc::channel(); + let (response_tx, response_rx) = mpsc::sync_channel(1); tx.send(response_tx).expect("Failed to send VM request"); response_rx.recv().expect("Failed to receive VM response") } From 5a8c57b7bdbaa824b2a904d0a38f9e13450702b4 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 13:29:34 +0200 Subject: [PATCH 034/135] Test creating a new (heap) exception type --- crates/capi/src/object.rs | 16 +++++++++++++- crates/capi/src/pyerrors.rs | 40 +++++++++++++++++++++++++++++++++- crates/capi/src/pylifecycle.rs | 3 ++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index bdeb56109f2..77f74b9893a 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -2,7 +2,7 @@ use crate::{PyObject, with_vm}; use core::ffi::c_ulong; use core::ffi::{c_int, c_uint}; use core::mem::MaybeUninit; -use rustpython_vm::builtins::PyType; +use rustpython_vm::builtins::{PyStr, PyType}; use rustpython_vm::{AsObject, Context, Py}; const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; @@ -85,6 +85,20 @@ pub extern "C" fn PyType_GetQualName(ptr: *const Py) -> *mut PyObject { let ty = unsafe { &*ptr }; with_vm(move |vm| ty.__qualname__(vm)) } +#[unsafe(no_mangle)] +pub extern "C" fn PyType_GetFullyQualifiedName(ptr: *const Py) -> *mut PyObject { + let ty = unsafe { &*ptr }; + with_vm(move |vm| { + let module = ty.__module__(vm).downcast::().unwrap(); + let qualname = ty.__qualname__(vm).downcast::().unwrap(); + let fully_qualified_name = format!( + "{}.{}", + module.to_string_lossy(), + qualname.to_string_lossy() + ); + vm.ctx.new_str(fully_qualified_name) + }) +} #[unsafe(no_mangle)] pub extern "C" fn PyObject_GetAttr(_obj: *mut PyObject, _name: *mut PyObject) -> *mut PyObject { diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index dd5516ed307..969d9e94c1e 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -7,6 +7,9 @@ use rustpython_vm::builtins::{PyTuple, PyType}; #[unsafe(no_mangle)] pub static mut PyExc_BaseException: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); +#[unsafe(no_mangle)] +pub static mut PyExc_Exception: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); + #[unsafe(no_mangle)] pub static mut PyExc_SystemError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); @@ -128,9 +131,29 @@ pub extern "C" fn PyException_GetTraceback(_exc: *mut PyObject) -> *mut PyObject core::ptr::null_mut() } +#[unsafe(no_mangle)] +pub extern "C" fn PyErr_GivenExceptionMatches(given: *mut PyObject, exc: *mut PyObject) -> c_int { + with_vm(|vm| { + let given = unsafe { &*given }; + let exc = unsafe { &*exc }; + + if let Some(exc_type) = exc.downcast_ref::() { + given.is_subclass(exc_type.as_ref(), vm) + } else if let Some(exc_tuple) = exc.downcast_ref::() { + Ok(exc_tuple + .iter() + .any(|ty| given.is_subclass(ty, vm).unwrap_or_default())) + } else { + Ok(false) + } + }) +} + #[cfg(test)] mod tests { - use pyo3::exceptions::PyTypeError; + use pyo3::PyTypeInfo; + use pyo3::create_exception; + use pyo3::exceptions::{PyException, PyTypeError}; use pyo3::prelude::*; #[test] @@ -140,4 +163,19 @@ mod tests { assert!(PyErr::take(py).is_some()); }) } + + #[test] + fn test_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 75b3b1caf15..c420d2116b8 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,7 +1,7 @@ use crate::log_stub; use crate::object::{PyBool_Type, PyLong_Type, PyTuple_Type, PyType_Type, PyUnicode_Type}; use crate::pyerrors::{ - PyExc_BaseException, PyExc_OverflowError, PyExc_SystemError, PyExc_TypeError, + PyExc_BaseException, PyExc_Exception, PyExc_OverflowError, PyExc_SystemError, PyExc_TypeError, }; use crate::pystate::attach_vm_to_thread; use core::ffi::c_int; @@ -46,6 +46,7 @@ pub(crate) fn init_static_type_pointers() { let exc = &context.exceptions; PyExc_BaseException.write(exc.base_exception_type.as_object().as_raw().cast_mut()); + PyExc_Exception.write(exc.exception_type.as_object().as_raw().cast_mut()); PyExc_SystemError.write(exc.system_error.as_object().as_raw().cast_mut()); PyExc_TypeError.write(exc.type_error.as_object().as_raw().cast_mut()); PyExc_OverflowError.write(exc.overflow_error.as_object().as_raw().cast_mut()); From 3dda576be79d4ceb41009f295568d05242dcc688 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 14:05:28 +0200 Subject: [PATCH 035/135] Don't check for NULL in `Py_DecRef`/`Py_IncRef` --- crates/capi/src/refcount.rs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/crates/capi/src/refcount.rs b/crates/capi/src/refcount.rs index ebb284d65df..9d5ebb64cdd 100644 --- a/crates/capi/src/refcount.rs +++ b/crates/capi/src/refcount.rs @@ -5,27 +5,15 @@ use rustpython_vm::PyObjectRef; #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] pub extern "C" fn _Py_DecRef(op: *mut PyObject) { - let Some(ptr) = NonNull::new(op) else { - return; - }; - - let owned = unsafe { PyObjectRef::from_raw(ptr) }; - - // Dropping so we decrement the refcount - drop(owned); + // By dropping PyObjectRef, we will decrement the reference count. + unsafe { PyObjectRef::from_raw(NonNull::new_unchecked(op)) }; } #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] pub extern "C" fn _Py_IncRef(op: *mut PyObject) { - if op.is_null() { - return; - } - - // SAFETY: op is non-null and expected to be a valid pointer for this shim. - let owned = unsafe { (*op).to_owned() }; - - core::mem::forget(owned); + // Don't drop the owned value, as we just want to increment the refcount. + core::mem::forget(unsafe { (*op).to_owned() }); } #[unsafe(no_mangle)] From a1b3af674f8181412e843dadaefdd5d4df86fa11 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 16:34:27 +0200 Subject: [PATCH 036/135] Add basic `PyDict` support --- crates/capi/src/dictobject.rs | 83 +++++++++++++++++++++++++++++++++++ crates/capi/src/lib.rs | 1 + crates/capi/src/util.rs | 13 ++++++ 3 files changed, 97 insertions(+) create mode 100644 crates/capi/src/dictobject.rs diff --git a/crates/capi/src/dictobject.rs b/crates/capi/src/dictobject.rs new file mode 100644 index 00000000000..8de00eaa5e8 --- /dev/null +++ b/crates/capi/src/dictobject.rs @@ -0,0 +1,83 @@ +use crate::PyObject; +use crate::pystate::with_vm; +use rustpython_vm::builtins::PyDict; +use core::ffi::c_int; + +#[unsafe(no_mangle)] +pub extern "C" fn PyDict_New() -> *mut PyObject { + with_vm(|vm| vm.ctx.new_dict()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyDict_SetItem( + dict: *mut PyObject, + key: *mut PyObject, + val: *mut PyObject, +) -> c_int { + with_vm(|vm| { + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + let key = unsafe { &*key }; + let value = unsafe { &*val }.to_owned(); + dict.set_item(key, value, vm)?; + Ok(0) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyDict_GetItemRef( + dict: *mut PyObject, + key: *mut PyObject, + result: *mut *mut PyObject, +) -> c_int { + with_vm(|vm| { + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + let key = unsafe { &*key }; + + if let Some(value) = dict.get_item_opt(key, vm)? { + unsafe { + *result = value.into_raw().as_ptr(); + } + Ok(1) + } else { + unsafe { + *result = core::ptr::null_mut(); + } + Ok(0) + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyDict_Size(dict: *mut PyObject) -> isize { + with_vm(|vm| { + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + Ok(dict.__len__() as isize) + }) +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::{IntoPyDict, PyDict, PyInt}; + + #[test] + fn test_create_empty_dict() { + Python::attach(|py| { + let dict = PyDict::new(py); + assert!(dict.is_instance_of::()); + }) + } + + #[test] + fn test_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::()?; + assert_eq!(value, 2); + assert_eq!(dict.len(), 2); + + Ok::<_, PyErr>(()) + }) + .unwrap() + } +} diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index ab8ba27a7b9..4a6af6818b1 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -5,6 +5,7 @@ extern crate alloc; pub mod abstract_; pub mod bytesobject; +pub mod dictobject; pub mod import; pub mod longobject; pub mod object; diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 5013696c9d6..53bec4d8f29 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -110,6 +110,19 @@ impl FfiResult for PyResult { } } +impl FfiResult for PyResult { + type Output = c_int; + + fn into_output(self, vm: &VirtualMachine) -> Self::Output { + self.unwrap_or_else( + |err| { + vm.push_exception(Some(err)); + -1 + }, + ) + } +} + impl FfiResult for PyResult<*mut c_char> { type Output = *mut c_char; From 3c7ef8f6ebeb3570ac8b3d8b4df70ff3a4063e33 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 17:21:20 +0200 Subject: [PATCH 037/135] Implement minimal tuple functions --- crates/capi/src/tupleobject.rs | 25 ++++++++++++++++++------- crates/capi/src/util.rs | 24 ++++++++++++++++++------ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/crates/capi/src/tupleobject.rs b/crates/capi/src/tupleobject.rs index 996a835bb78..e19374598dd 100644 --- a/crates/capi/src/tupleobject.rs +++ b/crates/capi/src/tupleobject.rs @@ -1,15 +1,26 @@ -use core::ptr; - use crate::PyObject; +use crate::pystate::with_vm; +use rustpython_vm::builtins::PyTuple; #[unsafe(no_mangle)] pub extern "C" fn PyTuple_Size(_tuple: *mut PyObject) -> isize { - crate::log_stub("PyTuple_Size"); - 0 + with_vm(|vm| { + let tuple = unsafe { &*_tuple }.try_downcast_ref::(vm)?; + Ok(tuple.__len__() as isize) + }) } #[unsafe(no_mangle)] -pub extern "C" fn PyTuple_GetItem(_tuple: *mut PyObject, _pos: isize) -> *mut PyObject { - crate::log_stub("PyTuple_GetItem"); - ptr::null_mut() +pub extern "C" fn PyTuple_GetItem(_tuple: *mut PyObject, pos: isize) -> *mut PyObject { + with_vm(|vm| { + let tuple = unsafe { &*_tuple }.try_downcast_ref::(vm)?; + let result: &PyObject = pos + .try_into() + .ok() + .and_then(|index: usize| tuple.get(index)) + .ok_or_else(|| vm.new_index_error("tuple index out of range"))?; + + //Return borrowed reference + Ok(result.as_raw()) + }) } diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 53bec4d8f29..0393003e656 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -52,6 +52,20 @@ impl FfiResult for *const PyObject { } } +impl FfiResult for PyResult<*const PyObject> { + type Output = *mut PyObject; + + fn into_output(self, vm: &VirtualMachine) -> Self::Output { + self.map_or_else( + |err| { + vm.push_exception(Some(err)); + core::ptr::null_mut() + }, + |ptr| ptr.cast_mut(), + ) + } +} + impl FfiResult for PyResult { type Output = *mut PyObject; @@ -114,12 +128,10 @@ impl FfiResult for PyResult { type Output = c_int; fn into_output(self, vm: &VirtualMachine) -> Self::Output { - self.unwrap_or_else( - |err| { - vm.push_exception(Some(err)); - -1 - }, - ) + self.unwrap_or_else(|err| { + vm.push_exception(Some(err)); + -1 + }) } } From 5625ba30b80743cbb8c46aaa722a6854d5c7a263 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 17:45:56 +0200 Subject: [PATCH 038/135] Implement `PyObject_Repr` & `PyObject_Str` --- crates/capi/src/object.rs | 41 ++++++++++++++++++++++++++++++++------- crates/capi/src/util.rs | 11 +++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 77f74b9893a..4166169c9b3 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -4,6 +4,7 @@ use core::ffi::{c_int, c_uint}; use core::mem::MaybeUninit; use rustpython_vm::builtins::{PyStr, PyType}; use rustpython_vm::{AsObject, Context, Py}; +use core::ptr::NonNull; const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; const PY_TPFLAGS_LIST_SUBCLASS: c_ulong = 1 << 25; @@ -107,15 +108,25 @@ pub extern "C" fn PyObject_GetAttr(_obj: *mut PyObject, _name: *mut PyObject) -> } #[unsafe(no_mangle)] -pub extern "C" fn PyObject_Repr(_obj: *mut PyObject) -> *mut PyObject { - crate::log_stub("PyObject_Repr"); - core::ptr::null_mut() +pub extern "C" fn PyObject_Repr(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let Some(obj) = NonNull::new(obj) else { + return Ok(vm.ctx.new_str("")); + }; + + unsafe { obj.as_ref() }.repr(vm) + }) } #[unsafe(no_mangle)] -pub extern "C" fn PyObject_Str(_obj: *mut PyObject) -> *mut PyObject { - crate::log_stub("PyObject_Str"); - core::ptr::null_mut() +pub extern "C" fn PyObject_Str(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let Some(obj) = NonNull::new(obj) else { + return Ok(vm.ctx.new_str("")); + }; + + unsafe { obj.as_ref() }.str(vm) + }) } #[unsafe(no_mangle)] @@ -145,7 +156,7 @@ pub extern "C" fn PyObject_IsTrue(obj: *mut PyObject) -> c_int { #[cfg(test)] mod tests { use pyo3::prelude::*; - use pyo3::types::{PyBool, PyNone, PyString}; + use pyo3::types::{PyBool, PyInt, PyNone, PyString}; #[test] fn test_is_truthy() { @@ -185,4 +196,20 @@ mod tests { assert!(PyBool::new(py, true).is_instance_of::()); }) } + + #[test] + fn test_repr() { + Python::attach(|py| { + let module = py.import("sys").unwrap(); + assert_eq!(module.repr().unwrap(), ""); + }) + } + + #[test] + fn test_obj_to_str() { + Python::attach(|py| { + let number = PyInt::new(py, 42); + assert_eq!(number.str().unwrap(), "42"); + }) + } } diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 0393003e656..4ed585aa4e5 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -80,6 +80,17 @@ impl FfiResult for PyResult { } } +impl FfiResult for PyResult> +where + PyRef: Into, +{ + type Output = *mut PyObject; + + fn into_output(self, vm: &VirtualMachine) -> Self::Output { + self.map(Into::into).into_output(vm) + } +} + impl FfiResult for PyResult { type Output = c_long; From ea45d7830a1c6351bd6cf0ee0b6863271d6f7855 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 17:51:15 +0200 Subject: [PATCH 039/135] Implement `PyObject_GetAttr` --- crates/capi/src/object.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 4166169c9b3..58d8a435da4 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -102,9 +102,12 @@ pub extern "C" fn PyType_GetFullyQualifiedName(ptr: *const Py) -> *mut P } #[unsafe(no_mangle)] -pub extern "C" fn PyObject_GetAttr(_obj: *mut PyObject, _name: *mut PyObject) -> *mut PyObject { - crate::log_stub("PyObject_GetAttr"); - core::ptr::null_mut() +pub extern "C" fn PyObject_GetAttr(obj: *mut PyObject, name: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let name = unsafe { &*name }.try_downcast_ref::(vm)?; + obj.get_attr(name, vm) + }) } #[unsafe(no_mangle)] @@ -212,4 +215,20 @@ mod tests { assert_eq!(number.str().unwrap(), "42"); }) } + + #[test] + fn test_get_attr() { + Python::attach(|py| { + let sys = py.import("sys").unwrap(); + let implementation = sys + .getattr("implementation") + .unwrap() + .getattr("name") + .unwrap() + .str() + .unwrap(); + + assert_eq!(implementation, "rustpython"); + }) + } } From fcbc6b9c282665a28ca5f701d8c378404600cb81 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 9 Apr 2026 18:26:55 +0200 Subject: [PATCH 040/135] Implement `PyUnicode_InternInPlace` --- crates/capi/src/dictobject.rs | 2 +- crates/capi/src/object.rs | 2 +- crates/capi/src/unicodeobject.rs | 22 ++++++++++++++++++++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/capi/src/dictobject.rs b/crates/capi/src/dictobject.rs index 8de00eaa5e8..e5eea6cc27f 100644 --- a/crates/capi/src/dictobject.rs +++ b/crates/capi/src/dictobject.rs @@ -1,7 +1,7 @@ use crate::PyObject; use crate::pystate::with_vm; -use rustpython_vm::builtins::PyDict; use core::ffi::c_int; +use rustpython_vm::builtins::PyDict; #[unsafe(no_mangle)] pub extern "C" fn PyDict_New() -> *mut PyObject { diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 58d8a435da4..b83d9a045df 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -2,9 +2,9 @@ use crate::{PyObject, with_vm}; use core::ffi::c_ulong; use core::ffi::{c_int, c_uint}; use core::mem::MaybeUninit; +use core::ptr::NonNull; use rustpython_vm::builtins::{PyStr, PyType}; use rustpython_vm::{AsObject, Context, Py}; -use core::ptr::NonNull; const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; const PY_TPFLAGS_LIST_SUBCLASS: c_ulong = 1 << 25; diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index 982c791557f..8fee64f1cec 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -1,8 +1,10 @@ use crate::{PyObject, with_vm}; use core::ffi::{c_char, c_int}; use core::ptr; +use core::ptr::NonNull; use core::slice; use core::str; +use rustpython_vm::PyObjectRef; use rustpython_vm::builtins::PyStr; #[unsafe(no_mangle)] @@ -54,8 +56,16 @@ pub extern "C" fn PyUnicode_AsEncodedString( } #[unsafe(no_mangle)] -pub extern "C" fn PyUnicode_InternInPlace(_string: *mut *mut PyObject) { - crate::log_stub("PyUnicode_InternInPlace"); +pub extern "C" fn PyUnicode_InternInPlace(string: *mut *mut PyObject) { + with_vm(|vm| { + let old_str = unsafe { PyObjectRef::from_raw(NonNull::new_unchecked(*string)) } + .downcast_exact::(vm) + .expect("PyUnicode_InternInPlace called with non-string object"); + + let interned: PyObjectRef = vm.ctx.intern_str(old_str).to_owned().into(); + + unsafe { *string = interned.into_raw().as_ptr() } + }) } #[unsafe(no_mangle)] @@ -80,6 +90,7 @@ pub extern "C" fn PyUnicode_EqualToUTF8AndSize( #[cfg(test)] mod tests { + use pyo3::intern; use pyo3::prelude::*; use pyo3::types::PyString; @@ -92,4 +103,11 @@ mod tests { assert_eq!(string, "Hello, World!"); }) } + + #[test] + fn test_intern_str() { + Python::attach(|py| { + let _string = intern!(py, "Hello, World!"); + }) + } } From ff9d38561183390144b6f09cf7c4040491e87c82 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 10 Apr 2026 10:50:20 +0200 Subject: [PATCH 041/135] Add support for running python source code --- crates/capi/Cargo.toml | 2 +- crates/capi/src/abstract_.rs | 22 ++++++++- crates/capi/src/ceval.rs | 81 ++++++++++++++++++++++++++++++++++ crates/capi/src/import.rs | 14 ++++++ crates/capi/src/lib.rs | 1 + crates/capi/src/object.rs | 3 ++ crates/capi/src/pylifecycle.rs | 5 ++- 7 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 crates/capi/src/ceval.rs diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index 9cc65b015f1..53c4cc62965 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -12,7 +12,7 @@ license.workspace = true crate-type = ["cdylib"] [dependencies] -rustpython-vm = { workspace = true, features = ["threading"]} +rustpython-vm = { workspace = true, features = ["threading", "compiler"]} [dev-dependencies] pyo3 = { git = "https://github.com/PyO3/pyo3", features = ["auto-initialize", "abi3-py314"] } diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index a11c8c6ccf1..280fb04c09b 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -1,6 +1,7 @@ use crate::with_vm; use alloc::slice; -use rustpython_vm::builtins::{PyStr, PyTuple}; +use core::ffi::c_int; +use rustpython_vm::builtins::{PyDict, PyStr, PyTuple}; use rustpython_vm::{PyObject, PyObjectRef, PyResult}; const PY_VECTORCALL_ARGUMENTS_OFFSET: usize = 1usize << (usize::BITS as usize - 1); @@ -53,6 +54,25 @@ pub extern "C" fn PyObject_VectorcallMethod( }) } +#[unsafe(no_mangle)] +pub extern "C" fn PySequence_Contains(obj: *mut PyObject, value: *mut PyObject) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let value = unsafe { &mut *value }; + match obj.try_sequence(vm) { + Ok(sequence) => sequence.contains(value, vm), + Err(type_err) => { + // TODO Dict should implement sequence protocol, but for now we can special case it + if let Some(dict) = obj.downcast_ref::() { + Ok(dict.contains_key(value, vm)) + } else { + Err(type_err) + } + } + } + }) +} + #[cfg(test)] mod tests { use pyo3::prelude::*; diff --git a/crates/capi/src/ceval.rs b/crates/capi/src/ceval.rs new file mode 100644 index 00000000000..b70d974d2c6 --- /dev/null +++ b/crates/capi/src/ceval.rs @@ -0,0 +1,81 @@ +use crate::pystate::with_vm; +use core::ffi::{CStr, c_char, c_int}; +use rustpython_vm::builtins::{PyCode, PyDict}; +use rustpython_vm::compiler::Mode; +use rustpython_vm::function::ArgMapping; +use rustpython_vm::scope::Scope; +use rustpython_vm::{AsObject, PyObject}; + +#[unsafe(no_mangle)] +pub extern "C" fn Py_CompileString( + code: *const c_char, + filename: *const c_char, + start: c_int, +) -> *mut PyObject { + with_vm(|vm| { + let code = unsafe { CStr::from_ptr(code) } + .to_str() + .expect("Invalid UTF-8 in code string"); + let filename = unsafe { CStr::from_ptr(filename) } + .to_str() + .expect("Invalid UTF-8 in filename string"); + + let mode = match start { + 256 => Mode::Single, + 257 => Mode::Exec, + 258 => Mode::Eval, + _ => panic!("Invalid start argument to Py_CompileString: {start}"), + }; + + vm.compile(code, mode, filename.to_owned()) + .map_err(|err| vm.new_syntax_error(&err, Some(code))) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyEval_EvalCode( + co: *mut PyObject, + globals: *mut PyObject, + locals: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let code = unsafe { &*co }.try_downcast_ref::(vm)?; + let globals = unsafe { &*globals }.try_downcast_ref::(vm)?; + let locals = unsafe { &*locals }.try_downcast_ref::(vm)?; + + let scope = Scope::with_builtins( + Some(ArgMapping::from_dict_exact(locals.to_owned())), + globals.to_owned(), + vm, + ); + + vm.run_code_obj(code.to_owned(), scope) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyEval_GetBuiltins() -> *mut PyObject { + with_vm(|vm| vm.builtins.as_object().as_raw()) +} + +#[cfg(test)] +mod tests { + use pyo3::exceptions::PyException; + use pyo3::prelude::*; + + #[test] + fn test_code_eval() { + Python::attach(|py| { + let result = py.eval(c"1 + 1", None, None).unwrap(); + assert_eq!(result.extract::().unwrap(), 2); + }) + } + + #[test] + fn test_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/import.rs b/crates/capi/src/import.rs index f3af06d2217..617992bd481 100644 --- a/crates/capi/src/import.rs +++ b/crates/capi/src/import.rs @@ -1,4 +1,5 @@ use crate::{PyObject, with_vm}; +use core::ffi::{CStr, c_char}; use rustpython_vm::builtins::PyStr; #[unsafe(no_mangle)] @@ -9,6 +10,19 @@ pub extern "C" fn PyImport_Import(name: *mut PyObject) -> *mut PyObject { }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyImport_AddModuleRef(name: *const c_char) -> *mut PyObject { + with_vm(|vm| { + let name = unsafe { CStr::from_ptr(name) } + .to_str() + .expect("Name is not valid UTF-8"); + + // TODO check if module already exists and return it if so, instead of creating a new one + + vm.new_module(name, vm.ctx.new_dict(), None) + }) +} + #[cfg(test)] mod tests { use pyo3::prelude::*; diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 4a6af6818b1..c3899c687d3 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -5,6 +5,7 @@ extern crate alloc; pub mod abstract_; pub mod bytesobject; +pub mod ceval; pub mod dictobject; pub mod import; pub mod longobject; diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index b83d9a045df..a5a9b270315 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -30,6 +30,9 @@ pub static mut PyUnicode_Type: MaybeUninit<&'static Py> = MaybeUninit::u #[unsafe(no_mangle)] pub static mut PyBool_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); +#[unsafe(no_mangle)] +pub static mut PyDict_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); + #[unsafe(no_mangle)] pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const Py { // SAFETY: The caller must guarantee that `op` is a valid pointer to a `PyObject`. diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index c420d2116b8..f732a9f2f41 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,5 +1,7 @@ use crate::log_stub; -use crate::object::{PyBool_Type, PyLong_Type, PyTuple_Type, PyType_Type, PyUnicode_Type}; +use crate::object::{ + PyBool_Type, PyDict_Type, PyLong_Type, PyTuple_Type, PyType_Type, PyUnicode_Type, +}; use crate::pyerrors::{ PyExc_BaseException, PyExc_Exception, PyExc_OverflowError, PyExc_SystemError, PyExc_TypeError, }; @@ -43,6 +45,7 @@ pub(crate) fn init_static_type_pointers() { PyTuple_Type.write(types.tuple_type); PyUnicode_Type.write(types.str_type); PyBool_Type.write(types.bool_type); + PyDict_Type.write(types.dict_type); let exc = &context.exceptions; PyExc_BaseException.write(exc.base_exception_type.as_object().as_raw().cast_mut()); From 7b362402040207c43b597d92c7cb25da4acde9fb Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 10 Apr 2026 11:28:20 +0200 Subject: [PATCH 042/135] Make all modules private --- crates/capi/src/lib.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index c3899c687d3..0852f5f3864 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -3,20 +3,20 @@ pub use rustpython_vm::PyObject; extern crate alloc; -pub mod abstract_; -pub mod bytesobject; -pub mod ceval; -pub mod dictobject; -pub mod import; -pub mod longobject; -pub mod object; -pub mod pyerrors; -pub mod pylifecycle; -pub mod pystate; -pub mod refcount; -pub mod traceback; -pub mod tupleobject; -pub mod unicodeobject; +pub(crate) mod abstract_; +pub(crate) mod bytesobject; +pub(crate) mod ceval; +pub(crate) mod dictobject; +pub(crate) mod import; +pub(crate) mod longobject; +pub(crate) mod object; +pub(crate) mod pyerrors; +pub(crate) mod pylifecycle; +pub(crate) mod pystate; +pub(crate) mod refcount; +pub(crate) mod traceback; +pub(crate) mod tupleobject; +pub(crate) mod unicodeobject; pub(crate) mod util; #[inline] From c4b8ea3a4af74080bd2b29b658d8d859b8f509d0 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 10 Apr 2026 11:56:20 +0200 Subject: [PATCH 043/135] Implement `PyErr_SetString` --- crates/capi/src/pyerrors.rs | 34 +++++++++++++++++++++++----------- crates/capi/src/util.rs | 11 +++++++++++ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index 969d9e94c1e..4da73d80dec 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -1,8 +1,11 @@ use crate::{PyObject, with_vm}; +use core::convert::Infallible; use core::ffi::CStr; use core::ffi::{c_char, c_int}; use core::mem::MaybeUninit; +use rustpython_vm::PyResult; use rustpython_vm::builtins::{PyTuple, PyType}; +use rustpython_vm::convert::IntoObject; #[unsafe(no_mangle)] pub static mut PyExc_BaseException: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); @@ -26,31 +29,40 @@ pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject { #[unsafe(no_mangle)] pub extern "C" fn PyErr_SetRaisedException(exc: *mut PyObject) { - with_vm(|vm| { + with_vm::>(|_vm| { let exception = unsafe { (&*exc).to_owned().downcast_unchecked() }; - vm.push_exception(Some(exception)); + Err(exception) }); } #[unsafe(no_mangle)] pub extern "C" fn PyErr_SetObject(exception: *mut PyObject, value: *mut PyObject) { - with_vm(|vm| { + with_vm::>(|vm| { let exc_type = unsafe { (&*exception).to_owned() }; let exc_val = unsafe { (&*value).to_owned() }; - let normalized = vm - .normalize_exception(exc_type, exc_val, vm.ctx.none()) - .unwrap_or_else(|_| { - vm.new_type_error("exceptions must derive from BaseException".to_owned()) - }); + let normalized = vm.normalize_exception(exc_type, exc_val, vm.ctx.none())?; - vm.push_exception(Some(normalized)); + Err(normalized) }); } #[unsafe(no_mangle)] -pub extern "C" fn PyErr_SetString(_exception: *mut PyObject, _message: *const c_char) { - crate::log_stub("PyErr_SetString"); +pub extern "C" fn PyErr_SetString(exception: *mut PyObject, message: *const c_char) { + with_vm::>(|vm| { + let exc_type = unsafe { &*exception }.try_downcast_ref::(vm)?; + + let message = unsafe { CStr::from_ptr(message) } + .to_str() + .expect("Exception message is not valid UTF-8"); + + let exc = vm.invoke_exception( + exc_type.to_owned(), + vec![vm.ctx.new_str(message).into_object()], + )?; + + Err(exc) + }); } #[unsafe(no_mangle)] diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 4ed585aa4e5..aafe78f6891 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -1,3 +1,4 @@ +use core::convert::Infallible; use core::ffi::c_long; use core::ffi::{c_char, c_int}; use rustpython_vm::{PyObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; @@ -164,3 +165,13 @@ impl FfiResult for PyResult<*const c_char> { }) } } + +impl FfiResult for PyResult { + type Output = (); + + fn into_output(self, vm: &VirtualMachine) -> Self::Output { + match self { + Err(err) => vm.push_exception(Some(err)), + } + } +} From dc56590b17bfc02a2f2c9df712a5fadb4c09ea0e Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 10 Apr 2026 13:31:25 +0200 Subject: [PATCH 044/135] Add basic support for `PyList` --- crates/capi/src/lib.rs | 1 + crates/capi/src/listobject.rs | 95 ++++++++++++++++++++++++++++++++++ crates/capi/src/pyerrors.rs | 3 ++ crates/capi/src/pylifecycle.rs | 4 +- 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 crates/capi/src/listobject.rs diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 0852f5f3864..315ef5d624a 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -8,6 +8,7 @@ pub(crate) mod bytesobject; pub(crate) mod ceval; pub(crate) mod dictobject; pub(crate) mod import; +pub(crate) mod listobject; pub(crate) mod longobject; pub(crate) mod object; pub(crate) mod pyerrors; diff --git a/crates/capi/src/listobject.rs b/crates/capi/src/listobject.rs new file mode 100644 index 00000000000..8fb2053d99d --- /dev/null +++ b/crates/capi/src/listobject.rs @@ -0,0 +1,95 @@ +use crate::PyObject; +use crate::pystate::with_vm; +use core::ffi::c_int; +use core::ptr::NonNull; +use rustpython_vm::PyObjectRef; +use rustpython_vm::builtins::PyList; + +#[unsafe(no_mangle)] +pub extern "C" fn PyList_New(size: isize) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_list(Vec::with_capacity(size as usize))) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyList_Size(obj: *mut PyObject) -> isize { + with_vm(|vm| { + let list = unsafe { &*obj }.try_downcast_ref::(vm)?; + Ok(list.__len__() as isize) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyList_GetItemRef(obj: *mut PyObject, index: isize) -> *mut PyObject { + with_vm(|vm| { + let list = unsafe { &*obj }.try_downcast_ref::(vm)?; + + list.borrow_vec() + .get(index as usize) + .ok_or_else(|| vm.new_index_error(format!("list index out of range: {index}"))) + .map(ToOwned::to_owned) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyList_SetItem(list: *mut PyObject, index: isize, item: *mut PyObject) -> c_int { + with_vm(|vm| { + let list = unsafe { &*list }.try_downcast_ref::(vm)?; + let item = unsafe { PyObjectRef::from_raw(NonNull::new_unchecked(item)) }; + + let mut list_mut = list.borrow_vec_mut(); + match index - list_mut.len() as isize { + ..0 => { + list_mut[index as usize] = item; + Ok(0) + } + // This is somewhat a hack, we assume that we are populating a list right after PyList_New + 0 if list_mut.capacity() > index as usize => { + list_mut.push(item); + Ok(0) + } + 0.. => Err(vm.new_index_error(format!("list assignment index out of range: {index}"))), + } + }) +} + +#[cfg(test)] +mod tests { + use pyo3::exceptions::PyIndexError; + use pyo3::prelude::*; + use pyo3::types::PyList; + + #[test] + fn test_create_list() { + Python::attach(|py| { + let list = PyList::new(py, &[1, 2, 3]).unwrap(); + assert_eq!(list.len(), 3); + assert_eq!(list.get_item(0).unwrap().extract::().unwrap(), 1); + assert_eq!(list.get_item(1).unwrap().extract::().unwrap(), 2); + assert_eq!(list.get_item(2).unwrap().extract::().unwrap(), 3); + assert!(list.get_item(3).is_err()); + }) + } + + #[test] + fn test_replace_item_in_list() { + Python::attach(|py| { + let list = PyList::new(py, &[1]).unwrap(); + assert_eq!(list.len(), 1); + list.set_item(0, 2).unwrap(); + assert_eq!(list.len(), 1); + assert_eq!(list.get_item(0).unwrap().extract::().unwrap(), 2); + }) + } + + #[test] + fn test_set_item_out_of_range() { + Python::attach(|py| { + let list = PyList::empty(py); + assert!( + list.set_item(0, 1) + .unwrap_err() + .is_instance_of::(py) + ); + }) + } +} diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index 4da73d80dec..685aa53fe32 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -22,6 +22,9 @@ pub static mut PyExc_TypeError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit #[unsafe(no_mangle)] pub static mut PyExc_OverflowError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); +#[unsafe(no_mangle)] +pub static mut PyExc_IndexError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); + #[unsafe(no_mangle)] pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject { with_vm(|vm| vm.take_raised_exception()) diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index f732a9f2f41..f08ac0ba1a8 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -3,7 +3,8 @@ use crate::object::{ PyBool_Type, PyDict_Type, PyLong_Type, PyTuple_Type, PyType_Type, PyUnicode_Type, }; use crate::pyerrors::{ - PyExc_BaseException, PyExc_Exception, PyExc_OverflowError, PyExc_SystemError, PyExc_TypeError, + PyExc_BaseException, PyExc_Exception, PyExc_IndexError, PyExc_OverflowError, PyExc_SystemError, + PyExc_TypeError, }; use crate::pystate::attach_vm_to_thread; use core::ffi::c_int; @@ -53,6 +54,7 @@ pub(crate) fn init_static_type_pointers() { PyExc_SystemError.write(exc.system_error.as_object().as_raw().cast_mut()); PyExc_TypeError.write(exc.type_error.as_object().as_raw().cast_mut()); PyExc_OverflowError.write(exc.overflow_error.as_object().as_raw().cast_mut()); + PyExc_IndexError.write(exc.index_error.as_object().as_raw().cast_mut()); }; } From 47ed41dc3b38376cb960102da47434653ec1cc37 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 10 Apr 2026 15:10:56 +0200 Subject: [PATCH 045/135] Support `PyLong_AsLong` for non `PyInt` types --- crates/capi/src/longobject.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index 814c4a649cc..805ab121533 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -35,16 +35,13 @@ pub extern "C" fn PyLong_FromUnsignedLongLong(value: c_ulonglong) -> *mut PyObje #[unsafe(no_mangle)] pub extern "C" fn PyLong_AsLong(obj: *mut PyObject) -> c_long { - if obj.is_null() { - panic!("PyLong_AsLong called with null object"); - } - with_vm::>(|vm| { // SAFETY: non-null checked above; caller promises a valid PyObject pointer. let obj_ref = unsafe { &*obj }; let int_obj = obj_ref - .downcast_ref::() - .expect("PyLong_AsLong currently only accepts int instances"); + .to_owned() + .try_downcast::(vm) + .or_else(|_| obj_ref.try_index(vm))?; int_obj .as_bigint() From 375a170fcee66d369553d97ee77f205f24e1c016 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 10 Apr 2026 16:06:26 +0200 Subject: [PATCH 046/135] Add support for `PyComplex` --- Cargo.lock | 1 + crates/capi/Cargo.toml | 1 + crates/capi/src/complexobject.rs | 48 ++++++++++++++++++++++++++++++++ crates/capi/src/lib.rs | 1 + crates/capi/src/object.rs | 13 +++++++++ crates/capi/src/pylifecycle.rs | 4 ++- crates/capi/src/util.rs | 12 ++++++++ 7 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 crates/capi/src/complexobject.rs diff --git a/Cargo.lock b/Cargo.lock index da41f383f0a..abf7a7ce68e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3102,6 +3102,7 @@ dependencies = [ name = "rustpython-capi" version = "0.5.0" dependencies = [ + "num-complex", "pyo3", "rustpython-vm", ] diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index 53c4cc62965..e2610095d9f 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true crate-type = ["cdylib"] [dependencies] +num-complex.workspace = true rustpython-vm = { workspace = true, features = ["threading", "compiler"]} [dev-dependencies] diff --git a/crates/capi/src/complexobject.rs b/crates/capi/src/complexobject.rs new file mode 100644 index 00000000000..663c7264e76 --- /dev/null +++ b/crates/capi/src/complexobject.rs @@ -0,0 +1,48 @@ +use crate::{PyObject, with_vm}; +use core::ffi::c_double; +use num_complex::{Complex, Complex64}; +use rustpython_vm::builtins::PyComplex; +use rustpython_vm::{PyResult, VirtualMachine}; + +#[unsafe(no_mangle)] +pub extern "C" fn PyComplex_FromDoubles(real: c_double, imag: c_double) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_complex(Complex::new(real, imag))) +} + +fn try_to_complex(vm: &VirtualMachine, obj: &PyObject) -> PyResult { + obj.try_downcast_ref::(vm).map_or_else( + |type_err| { + if let Some((complex, _)) = obj.to_owned().try_complex(vm)? { + Ok(complex) + } else { + Err(type_err) + } + }, + |complex| Ok(complex.to_complex()), + ) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyComplex_RealAsDouble(obj: *mut PyObject) -> c_double { + with_vm(|vm| try_to_complex(vm, unsafe { &*obj }).map(|complex| complex.re)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyComplex_ImagAsDouble(obj: *mut PyObject) -> c_double { + with_vm(|vm| try_to_complex(vm, unsafe { &*obj }).map(|complex| complex.im)) +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::PyComplex; + + #[test] + fn test_py_int() { + Python::attach(|py| { + let number = PyComplex::from_doubles(py, 1.0, 2.0); + assert_eq!(number.real(), 1.0); + assert_eq!(number.imag(), 2.0); + }) + } +} diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 315ef5d624a..d6d53c8fcc3 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -6,6 +6,7 @@ extern crate alloc; pub(crate) mod abstract_; pub(crate) mod bytesobject; pub(crate) mod ceval; +pub(crate) mod complexobject; pub(crate) mod dictobject; pub(crate) mod import; pub(crate) mod listobject; diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index a5a9b270315..116cae9e532 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -33,6 +33,9 @@ pub static mut PyBool_Type: MaybeUninit<&'static Py> = MaybeUninit::unin #[unsafe(no_mangle)] pub static mut PyDict_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); +#[unsafe(no_mangle)] +pub static mut PyComplex_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); + #[unsafe(no_mangle)] pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const Py { // SAFETY: The caller must guarantee that `op` is a valid pointer to a `PyObject`. @@ -89,6 +92,7 @@ pub extern "C" fn PyType_GetQualName(ptr: *const Py) -> *mut PyObject { let ty = unsafe { &*ptr }; with_vm(move |vm| ty.__qualname__(vm)) } + #[unsafe(no_mangle)] pub extern "C" fn PyType_GetFullyQualifiedName(ptr: *const Py) -> *mut PyObject { let ty = unsafe { &*ptr }; @@ -104,6 +108,15 @@ pub extern "C" fn PyType_GetFullyQualifiedName(ptr: *const Py) -> *mut P }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyType_IsSubtype(a: *const Py, b: *const Py) -> c_int { + with_vm(move |_vm| { + let a = unsafe { &*a }; + let b = unsafe { &*b }; + Ok(a.is_subtype(b)) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn PyObject_GetAttr(obj: *mut PyObject, name: *mut PyObject) -> *mut PyObject { with_vm(|vm| { diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index f08ac0ba1a8..817058bc745 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,6 +1,7 @@ use crate::log_stub; use crate::object::{ - PyBool_Type, PyDict_Type, PyLong_Type, PyTuple_Type, PyType_Type, PyUnicode_Type, + PyBool_Type, PyComplex_Type, PyDict_Type, PyLong_Type, PyTuple_Type, PyType_Type, + PyUnicode_Type, }; use crate::pyerrors::{ PyExc_BaseException, PyExc_Exception, PyExc_IndexError, PyExc_OverflowError, PyExc_SystemError, @@ -47,6 +48,7 @@ pub(crate) fn init_static_type_pointers() { PyUnicode_Type.write(types.str_type); PyBool_Type.write(types.bool_type); PyDict_Type.write(types.dict_type); + PyComplex_Type.write(types.complex_type); let exc = &context.exceptions; PyExc_BaseException.write(exc.base_exception_type.as_object().as_raw().cast_mut()); diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index aafe78f6891..9cec5d2c98f 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -2,6 +2,7 @@ use core::convert::Infallible; use core::ffi::c_long; use core::ffi::{c_char, c_int}; use rustpython_vm::{PyObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; +use std::ffi::c_double; pub(crate) trait FfiResult { type Output; @@ -147,6 +148,17 @@ impl FfiResult for PyResult { } } +impl FfiResult for PyResult { + type Output = c_double; + + fn into_output(self, vm: &VirtualMachine) -> Self::Output { + self.unwrap_or_else(|err| { + vm.push_exception(Some(err)); + -1.0 + }) + } +} + impl FfiResult for PyResult<*mut c_char> { type Output = *mut c_char; From fe8107ab91532404a6e24d3849d39c9e71beb51a Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 10 Apr 2026 17:33:06 +0200 Subject: [PATCH 047/135] Implement c_variadic `PyObject_CallMethodObjArgs` --- crates/capi/Cargo.toml | 4 ++++ crates/capi/src/abstract_.rs | 35 +++++++++++++++++++++++++++++++++++ crates/capi/src/lib.rs | 1 + 3 files changed, 40 insertions(+) diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index e2610095d9f..102fd4fb0e8 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -15,6 +15,10 @@ crate-type = ["cdylib"] num-complex.workspace = true rustpython-vm = { workspace = true, features = ["threading", "compiler"]} +[features] +# Enable PyObject_CallMethodObjArgs variadic function, which is only available on nightly Rust. +nightly = [] + [dev-dependencies] pyo3 = { git = "https://github.com/PyO3/pyo3", features = ["auto-initialize", "abi3-py314"] } diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index 280fb04c09b..c19a86ca817 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -20,6 +20,29 @@ pub extern "C" fn PyObject_CallNoArgs(callable: *mut PyObject) -> *mut PyObject }) } +#[unsafe(no_mangle)] +#[cfg(feature = "nightly")] +pub unsafe extern "C" fn PyObject_CallMethodObjArgs( + receiver: *mut PyObject, + name: *mut PyObject, + mut args: ... +) -> *mut PyObject { + with_vm(|vm| { + let mut arguments: Vec = vec![]; + loop { + if let Some(arg) = core::ptr::NonNull::new(unsafe { args.arg::<*mut PyObject>() }) { + arguments.push(unsafe { arg.as_ref() }.to_owned()); + } else { + break; + } + } + + let method_name = unsafe { (&*name).try_downcast_ref::(vm)? }; + let callable = unsafe { (&*receiver).get_attr(method_name, vm)? }; + callable.call(arguments, vm) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn PyObject_VectorcallMethod( name: *mut PyObject, @@ -78,6 +101,18 @@ mod tests { use pyo3::prelude::*; use pyo3::types::PyString; + #[test] + #[cfg(feature = "nightly")] + fn test_call_method0() { + Python::attach(|py| { + let string = PyString::new(py, "Hello, World!"); + assert_eq!( + string.call_method0("upper").unwrap().str().unwrap(), + "HELLO, WORLD!" + ); + }) + } + #[test] fn test_call_method1() { Python::attach(|py| { diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index d6d53c8fcc3..a3fc203a154 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -1,3 +1,4 @@ +#![cfg_attr(feature = "nightly", feature(c_variadic))] use crate::pystate::with_vm; pub use rustpython_vm::PyObject; From 2f6fc50bcf10853564606c891603f9eb3b71fbed Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 11 Apr 2026 12:41:41 +0200 Subject: [PATCH 048/135] Implement `PyCapsule` --- crates/capi/src/capsule.rs | 48 +++++++++++++++++++++++++++++++ crates/capi/src/lib.rs | 1 + crates/capi/src/util.rs | 18 +++++++++--- crates/vm/src/builtins/capsule.rs | 46 ++++++++++++++++++++++++----- crates/vm/src/vm/context.rs | 16 ++++++++--- 5 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 crates/capi/src/capsule.rs diff --git a/crates/capi/src/capsule.rs b/crates/capi/src/capsule.rs new file mode 100644 index 00000000000..b990bd62978 --- /dev/null +++ b/crates/capi/src/capsule.rs @@ -0,0 +1,48 @@ +use crate::PyObject; +use crate::pystate::with_vm; +use core::ffi::{c_char, c_void}; +use rustpython_vm::builtins::PyCapsule; + +#[allow(non_camel_case_types)] +pub type PyCapsule_Destructor = unsafe extern "C" fn(capsule: *mut PyObject); + +#[unsafe(no_mangle)] +pub extern "C" fn PyCapsule_New( + pointer: *mut c_void, + _name: *const c_char, + destructor: Option, +) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_capsule(pointer, destructor)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCapsule_GetPointer( + capsule: *mut PyObject, + _name: *const c_char, +) -> *mut c_void { + with_vm(|vm| { + let capsule = unsafe { &*capsule }.try_downcast_ref::(vm)?; + Ok(capsule.pointer()) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCapsule_GetName(_capsule: *mut PyObject) -> *const c_char { + core::ptr::null_mut() +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::PyCapsule; + + #[test] + fn test_capsule_new() { + Python::attach(|py| { + let value = String::from("Some data"); + let capsule = PyCapsule::new_with_value(py, value, c"my_capsule").unwrap(); + let ptr = capsule.pointer_checked(Some(c"my_capsule")).unwrap(); + assert_eq!(unsafe { ptr.cast::().as_ref() }, "Some data"); + }) + } +} diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index a3fc203a154..3119133d09f 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -6,6 +6,7 @@ extern crate alloc; pub(crate) mod abstract_; pub(crate) mod bytesobject; +pub(crate) mod capsule; pub(crate) mod ceval; pub(crate) mod complexobject; pub(crate) mod dictobject; diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 9cec5d2c98f..53b975bf884 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -1,8 +1,7 @@ +use crate::PyObject; use core::convert::Infallible; -use core::ffi::c_long; -use core::ffi::{c_char, c_int}; -use rustpython_vm::{PyObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; -use std::ffi::c_double; +use core::ffi::{c_char, c_double, c_int, c_long, c_void}; +use rustpython_vm::{PyObjectRef, PyRef, PyResult, VirtualMachine}; pub(crate) trait FfiResult { type Output; @@ -167,6 +166,17 @@ impl FfiResult for PyResult<*mut c_char> { } } +impl FfiResult for PyResult<*mut c_void> { + type Output = *mut c_void; + + fn into_output(self, vm: &VirtualMachine) -> Self::Output { + self.unwrap_or_else(|err| { + vm.push_exception(Some(err)); + core::ptr::null_mut() + }) + } +} + impl FfiResult for PyResult<*const c_char> { type Output = *const c_char; diff --git a/crates/vm/src/builtins/capsule.rs b/crates/vm/src/builtins/capsule.rs index c9c7fd2849d..13c372e993a 100644 --- a/crates/vm/src/builtins/capsule.rs +++ b/crates/vm/src/builtins/capsule.rs @@ -1,14 +1,19 @@ use super::PyType; -use crate::{Context, Py, PyPayload, PyResult, class::PyClassImpl, types::Representable}; +use crate::{ + AsObject, Context, Py, PyObject, PyPayload, PyResult, VirtualMachine, + class::PyClassImpl, + types::{Destructor, Representable}, +}; +use core::ffi::c_void; +use core::sync::atomic::AtomicPtr; /// PyCapsule - a container for C pointers. /// In RustPython, this is a minimal implementation for compatibility. #[pyclass(module = false, name = "PyCapsule")] -#[derive(Debug, Clone, Copy)] +#[derive(Debug)] pub struct PyCapsule { - // Capsules store opaque pointers; we don't expose the actual pointer functionality - // since RustPython doesn't have the same C extension model as CPython. - _private: (), + ptr: AtomicPtr, + destructor: Option, } impl PyPayload for PyCapsule { @@ -18,8 +23,26 @@ impl PyPayload for PyCapsule { } } -#[pyclass(with(Representable), flags(DISALLOW_INSTANTIATION))] -impl PyCapsule {} +#[pyclass(with(Representable, Destructor), flags(DISALLOW_INSTANTIATION))] +impl PyCapsule { + pub fn new( + ptr: *mut c_void, + destructor: Option, + ) -> Self { + Self { + ptr: ptr.into(), + destructor, + } + } + + pub fn pointer(&self) -> *mut c_void { + self.ptr.load(std::sync::atomic::Ordering::Relaxed) + } + + fn destructor(&self) -> Option { + self.destructor + } +} impl Representable for PyCapsule { #[inline] @@ -28,6 +51,15 @@ impl Representable for PyCapsule { } } +impl Destructor for PyCapsule { + fn del(zelf: &Py, _vm: &VirtualMachine) -> PyResult<()> { + if let Some(destructor) = zelf.destructor() { + unsafe { destructor(zelf.as_object().as_raw().cast_mut()) }; + } + Ok(()) + } +} + pub fn init(context: &'static Context) { PyCapsule::extend_class(context, context.types.capsule_type); } diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index fedb641c542..6f630fb0192 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -1,9 +1,9 @@ use crate::{ - PyResult, VirtualMachine, + PyObject, PyResult, VirtualMachine, builtins::{ - PyByteArray, PyBytes, PyComplex, PyDict, PyDictRef, PyEllipsis, PyFloat, PyFrozenSet, - PyInt, PyIntRef, PyList, PyListRef, PyNone, PyNotImplemented, PyStr, PyStrInterned, - PyTuple, PyTupleRef, PyType, PyTypeRef, PyUtf8Str, + PyByteArray, PyBytes, PyCapsule, PyComplex, PyDict, PyDictRef, PyEllipsis, PyFloat, + PyFrozenSet, PyInt, PyIntRef, PyList, PyListRef, PyNone, PyNotImplemented, PyStr, + PyStrInterned, PyTuple, PyTupleRef, PyType, PyTypeRef, PyUtf8Str, bool_::PyBool, code::{self, PyCode}, descriptor::{ @@ -752,6 +752,14 @@ impl Context { let code = code.into_code_object(self); PyRef::new_ref(PyCode::new(code), self.types.code_type.to_owned(), None) } + + pub fn new_capsule( + &self, + ptr: *mut core::ffi::c_void, + destructor: Option, + ) -> PyRef { + PyCapsule::new(ptr, destructor).into_ref(self) + } } impl AsRef for Context { From 52442ae724d01f4451e7a26d319ed91a22badce8 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 11 Apr 2026 13:02:22 +0200 Subject: [PATCH 049/135] Allow creating empty tuples --- crates/capi/src/tupleobject.rs | 45 +++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/crates/capi/src/tupleobject.rs b/crates/capi/src/tupleobject.rs index e19374598dd..221f9770d7e 100644 --- a/crates/capi/src/tupleobject.rs +++ b/crates/capi/src/tupleobject.rs @@ -1,19 +1,42 @@ use crate::PyObject; use crate::pystate::with_vm; +use rustpython_vm::PyResult; use rustpython_vm::builtins::PyTuple; #[unsafe(no_mangle)] -pub extern "C" fn PyTuple_Size(_tuple: *mut PyObject) -> isize { +pub extern "C" fn PyTuple_New(len: isize) -> *mut PyObject { with_vm(|vm| { - let tuple = unsafe { &*_tuple }.try_downcast_ref::(vm)?; + if len == 0 { + return Ok(vm.ctx.empty_tuple.to_owned()); + } + + Err(vm.new_not_implemented_error("PyTuple_New is not yet implemented")) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyTuple_SetItem( + _tuple: *mut PyObject, + _pos: isize, + _value: *mut PyObject, +) -> *mut PyObject { + with_vm::(|vm| { + Err(vm.new_not_implemented_error("PyTuple_SetItem is not yet implemented")) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyTuple_Size(tuple: *mut PyObject) -> isize { + with_vm(|vm| { + let tuple = unsafe { &*tuple }.try_downcast_ref::(vm)?; Ok(tuple.__len__() as isize) }) } #[unsafe(no_mangle)] -pub extern "C" fn PyTuple_GetItem(_tuple: *mut PyObject, pos: isize) -> *mut PyObject { +pub extern "C" fn PyTuple_GetItem(tuple: *mut PyObject, pos: isize) -> *mut PyObject { with_vm(|vm| { - let tuple = unsafe { &*_tuple }.try_downcast_ref::(vm)?; + let tuple = unsafe { &*tuple }.try_downcast_ref::(vm)?; let result: &PyObject = pos .try_into() .ok() @@ -24,3 +47,17 @@ pub extern "C" fn PyTuple_GetItem(_tuple: *mut PyObject, pos: isize) -> *mut PyO Ok(result.as_raw()) }) } + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::PyTuple; + + #[test] + fn test_empty_tuple() { + Python::attach(|py| { + let tuple = PyTuple::empty(py); + assert_eq!(tuple.len(), 0); + }) + } +} From 537fc69d5ba542af0b50cf0f7450193fec24c65f Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 11 Apr 2026 13:23:13 +0200 Subject: [PATCH 050/135] Implement `PyCMethod_New` --- crates/capi/src/lib.rs | 1 + crates/capi/src/methodobject.rs | 102 ++++++++++++++++++++++++++++++++ crates/capi/src/object.rs | 28 +++++---- 3 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 crates/capi/src/methodobject.rs diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 3119133d09f..d22e7699b7e 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -13,6 +13,7 @@ pub(crate) mod dictobject; pub(crate) mod import; pub(crate) mod listobject; pub(crate) mod longobject; +pub(crate) mod methodobject; pub(crate) mod object; pub(crate) mod pyerrors; pub(crate) mod pylifecycle; diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs new file mode 100644 index 00000000000..adac9f16593 --- /dev/null +++ b/crates/capi/src/methodobject.rs @@ -0,0 +1,102 @@ +use crate::PyObject; +use crate::object::PyTypeObject; +use crate::pystate::with_vm; +use core::ffi::{CStr, c_char, c_int, c_void}; +use core::ptr::NonNull; +use rustpython_vm::function::{FuncArgs, PyMethodFlags}; +use rustpython_vm::{AsObject, PyObjectRef, PyResult, VirtualMachine}; + +type PyCFunction = unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject; +type PyCFunctionWithKeywords = unsafe extern "C" fn( + slf: *mut PyObject, + args: *mut PyObject, + kwargs: *mut PyObject, +) -> *mut PyObject; + +#[repr(C)] +pub struct PyMethodDef { + pub ml_name: *const c_char, + pub ml_meth: *mut c_void, + pub ml_flags: c_int, + pub ml_doc: *const c_char, +} + +fn c_function_wrapper( + vm: &VirtualMachine, + slf: &PyObjectRef, + mut args: FuncArgs, + ml_meth: usize, + ml_flags: c_int, +) -> PyResult { + let slf = slf.as_object().as_raw().cast_mut(); + let flags = PyMethodFlags::from_bits_truncate(ml_flags as u32); + + let arg_tuple = vm.ctx.new_tuple(core::mem::take(&mut args.args)); + let arg_tuple_ptr = arg_tuple.as_object().as_raw().cast_mut(); + + let ret_ptr = if flags.contains(PyMethodFlags::KEYWORDS) { + let kwargs = vm.ctx.new_dict(); + for (k, v) in args.kwargs { + kwargs.set_item(&*k, v, vm)?; + } + let kwargs_ptr = kwargs.as_object().as_raw().cast_mut(); + let f = unsafe { core::mem::transmute::(ml_meth) }; + unsafe { f(slf, arg_tuple_ptr, kwargs_ptr) } + } else { + let f = unsafe { core::mem::transmute::(ml_meth) }; + unsafe { f(slf, arg_tuple_ptr) } + }; + + let ret_ptr = NonNull::new(ret_ptr) + .ok_or_else(|| vm.new_system_error("native method returned NULL".to_owned()))?; + Ok(unsafe { PyObjectRef::from_raw(ret_ptr) }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCMethod_New( + ml: *mut PyMethodDef, + slf: *mut PyObject, + _module: *mut PyObject, + _cls: *mut PyTypeObject, +) -> *mut PyObject { + with_vm(|vm| -> PyResult { + let ml = unsafe { &*ml }; + let name = unsafe { CStr::from_ptr(ml.ml_name) } + .to_str() + .expect("Method name was not valid UTF-8"); + + let doc = unsafe { CStr::from_ptr(ml.ml_doc) } + .to_str() + .expect("Method doc was not valid UTF-8"); + + let ml_meth = ml.ml_meth as usize; + let ml_flags = ml.ml_flags; + let slf = unsafe { &*slf }.to_owned(); + let callable = move |args: FuncArgs, vm: &VirtualMachine| { + c_function_wrapper(vm, &slf, args, ml_meth, ml_flags) + }; + + let flags = PyMethodFlags::from_bits_truncate(ml_flags as u32); + let method = vm.ctx.new_method_def(name, callable, flags, Some(doc)); + Ok(method.build_function(vm).into()) + }) +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::{PyCFunction, PyString}; + + #[test] + fn test_new_c_function() { + Python::attach(|py| { + let f = PyCFunction::new_closure(py, None, None, |_args, _kwargs| "Hello from Rust!") + .unwrap(); + + assert_eq!( + f.call0().unwrap().cast::().unwrap(), + "Hello from Rust!" + ); + }) + } +} diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 116cae9e532..8570e8ba08a 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -15,36 +15,38 @@ const PY_TPFLAGS_DICT_SUBCLASS: c_ulong = 1 << 29; const PY_TPFLAGS_BASE_EXC_SUBCLASS: c_ulong = 1 << 30; const PY_TPFLAGS_TYPE_SUBCLASS: c_ulong = 1 << 31; +pub type PyTypeObject = Py; + #[unsafe(no_mangle)] -pub static mut PyType_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); +pub static mut PyType_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); #[unsafe(no_mangle)] -pub static mut PyLong_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); +pub static mut PyLong_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); #[unsafe(no_mangle)] -pub static mut PyTuple_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); +pub static mut PyTuple_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); #[unsafe(no_mangle)] -pub static mut PyUnicode_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); +pub static mut PyUnicode_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); #[unsafe(no_mangle)] -pub static mut PyBool_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); +pub static mut PyBool_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); #[unsafe(no_mangle)] -pub static mut PyDict_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); +pub static mut PyDict_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); #[unsafe(no_mangle)] -pub static mut PyComplex_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); +pub static mut PyComplex_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); #[unsafe(no_mangle)] -pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const Py { +pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const PyTypeObject { // SAFETY: The caller must guarantee that `op` is a valid pointer to a `PyObject`. unsafe { (*op).class() } } #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "C" fn PyType_GetFlags(ptr: *const Py) -> c_ulong { +pub extern "C" fn PyType_GetFlags(ptr: *const PyTypeObject) -> c_ulong { let ctx = Context::genesis(); let zoo = &ctx.types; let exp_zoo = &ctx.exceptions; @@ -82,19 +84,19 @@ pub extern "C" fn PyType_GetFlags(ptr: *const Py) -> c_ulong { } #[unsafe(no_mangle)] -pub extern "C" fn PyType_GetName(ptr: *const Py) -> *mut PyObject { +pub extern "C" fn PyType_GetName(ptr: *const PyTypeObject) -> *mut PyObject { let ty = unsafe { &*ptr }; with_vm(move |vm| ty.__name__(vm)) } #[unsafe(no_mangle)] -pub extern "C" fn PyType_GetQualName(ptr: *const Py) -> *mut PyObject { +pub extern "C" fn PyType_GetQualName(ptr: *const PyTypeObject) -> *mut PyObject { let ty = unsafe { &*ptr }; with_vm(move |vm| ty.__qualname__(vm)) } #[unsafe(no_mangle)] -pub extern "C" fn PyType_GetFullyQualifiedName(ptr: *const Py) -> *mut PyObject { +pub extern "C" fn PyType_GetFullyQualifiedName(ptr: *const PyTypeObject) -> *mut PyObject { let ty = unsafe { &*ptr }; with_vm(move |vm| { let module = ty.__module__(vm).downcast::().unwrap(); @@ -109,7 +111,7 @@ pub extern "C" fn PyType_GetFullyQualifiedName(ptr: *const Py) -> *mut P } #[unsafe(no_mangle)] -pub extern "C" fn PyType_IsSubtype(a: *const Py, b: *const Py) -> c_int { +pub extern "C" fn PyType_IsSubtype(a: *const PyTypeObject, b: *const PyTypeObject) -> c_int { with_vm(move |_vm| { let a = unsafe { &*a }; let b = unsafe { &*b }; From 62485f6b032d105728c47cb6ab9197ec5f870e77 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 11 Apr 2026 13:52:17 +0200 Subject: [PATCH 051/135] Add `PyMethodPointer` union --- crates/capi/src/methodobject.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index adac9f16593..923b3354d16 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -1,7 +1,7 @@ use crate::PyObject; use crate::object::PyTypeObject; use crate::pystate::with_vm; -use core::ffi::{CStr, c_char, c_int, c_void}; +use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; use rustpython_vm::function::{FuncArgs, PyMethodFlags}; use rustpython_vm::{AsObject, PyObjectRef, PyResult, VirtualMachine}; @@ -13,19 +13,26 @@ type PyCFunctionWithKeywords = unsafe extern "C" fn( kwargs: *mut PyObject, ) -> *mut PyObject; +#[repr(C)] +#[derive(Copy, Clone)] +pub union PyMethodPointer { + function: PyCFunction, + function_with_keywords: PyCFunctionWithKeywords, +} + #[repr(C)] pub struct PyMethodDef { - pub ml_name: *const c_char, - pub ml_meth: *mut c_void, - pub ml_flags: c_int, - pub ml_doc: *const c_char, + ml_name: *const c_char, + ml_meth: PyMethodPointer, + ml_flags: c_int, + ml_doc: *const c_char, } fn c_function_wrapper( vm: &VirtualMachine, slf: &PyObjectRef, mut args: FuncArgs, - ml_meth: usize, + ml_meth: PyMethodPointer, ml_flags: c_int, ) -> PyResult { let slf = slf.as_object().as_raw().cast_mut(); @@ -35,15 +42,15 @@ fn c_function_wrapper( let arg_tuple_ptr = arg_tuple.as_object().as_raw().cast_mut(); let ret_ptr = if flags.contains(PyMethodFlags::KEYWORDS) { + let f = unsafe { ml_meth.function_with_keywords }; let kwargs = vm.ctx.new_dict(); for (k, v) in args.kwargs { kwargs.set_item(&*k, v, vm)?; } let kwargs_ptr = kwargs.as_object().as_raw().cast_mut(); - let f = unsafe { core::mem::transmute::(ml_meth) }; unsafe { f(slf, arg_tuple_ptr, kwargs_ptr) } } else { - let f = unsafe { core::mem::transmute::(ml_meth) }; + let f = unsafe { ml_meth.function }; unsafe { f(slf, arg_tuple_ptr) } }; @@ -69,8 +76,8 @@ pub extern "C" fn PyCMethod_New( .to_str() .expect("Method doc was not valid UTF-8"); - let ml_meth = ml.ml_meth as usize; let ml_flags = ml.ml_flags; + let ml_meth = ml.ml_meth; let slf = unsafe { &*slf }.to_owned(); let callable = move |args: FuncArgs, vm: &VirtualMachine| { c_function_wrapper(vm, &slf, args, ml_meth, ml_flags) From 61de6bf92965a4ed3148adcbf9a08af9a3e3f284 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 11 Apr 2026 17:11:18 +0200 Subject: [PATCH 052/135] Also support `NoArgs` CFunction's --- Cargo.lock | 1 + crates/capi/Cargo.toml | 1 + crates/capi/src/lib.rs | 1 + crates/capi/src/methodobject.rs | 92 ++++++++++++++++++++++++--------- crates/capi/src/moduleobject.rs | 11 ++++ 5 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 crates/capi/src/moduleobject.rs diff --git a/Cargo.lock b/Cargo.lock index abf7a7ce68e..385bf6f2943 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3102,6 +3102,7 @@ dependencies = [ name = "rustpython-capi" version = "0.5.0" dependencies = [ + "bitflags 2.11.0", "num-complex", "pyo3", "rustpython-vm", diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index 102fd4fb0e8..b4c2188aa83 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true crate-type = ["cdylib"] [dependencies] +bitflags.workspace = true num-complex.workspace = true rustpython-vm = { workspace = true, features = ["threading", "compiler"]} diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index d22e7699b7e..2792315d146 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -14,6 +14,7 @@ pub(crate) mod import; pub(crate) mod listobject; pub(crate) mod longobject; pub(crate) mod methodobject; +pub(crate) mod moduleobject; pub(crate) mod object; pub(crate) mod pyerrors; pub(crate) mod pylifecycle; diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 923b3354d16..2e84166e6e4 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -1,6 +1,7 @@ use crate::PyObject; use crate::object::PyTypeObject; use crate::pystate::with_vm; +use bitflags::bitflags_match; use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; use rustpython_vm::function::{FuncArgs, PyMethodFlags}; @@ -30,29 +31,38 @@ pub struct PyMethodDef { fn c_function_wrapper( vm: &VirtualMachine, - slf: &PyObjectRef, + slf: Option<&PyObjectRef>, mut args: FuncArgs, - ml_meth: PyMethodPointer, - ml_flags: c_int, + method: PyMethodPointer, + flags: PyMethodFlags, ) -> PyResult { - let slf = slf.as_object().as_raw().cast_mut(); - let flags = PyMethodFlags::from_bits_truncate(ml_flags as u32); + let slf_ptr = slf + .map(|slf| slf.as_object().as_raw().cast_mut()) + .unwrap_or_default(); let arg_tuple = vm.ctx.new_tuple(core::mem::take(&mut args.args)); let arg_tuple_ptr = arg_tuple.as_object().as_raw().cast_mut(); - let ret_ptr = if flags.contains(PyMethodFlags::KEYWORDS) { - let f = unsafe { ml_meth.function_with_keywords }; - let kwargs = vm.ctx.new_dict(); - for (k, v) in args.kwargs { - kwargs.set_item(&*k, v, vm)?; - } - let kwargs_ptr = kwargs.as_object().as_raw().cast_mut(); - unsafe { f(slf, arg_tuple_ptr, kwargs_ptr) } - } else { - let f = unsafe { ml_meth.function }; - unsafe { f(slf, arg_tuple_ptr) } - }; + let ret_ptr = bitflags_match!(flags, { + PyMethodFlags::NOARGS => { + let f = unsafe { method.function }; + unsafe { Ok(f(slf_ptr, arg_tuple_ptr)) } + }, + PyMethodFlags::VARARGS => { + let f = unsafe { method.function }; + unsafe { Ok(f(slf_ptr, arg_tuple_ptr)) } + }, + PyMethodFlags::VARARGS | PyMethodFlags::KEYWORDS => { + let f = unsafe { method.function_with_keywords }; + let kwargs = vm.ctx.new_dict(); + for (k, v) in args.kwargs { + kwargs.set_item(&*k, v, vm)?; + } + let kwargs_ptr = kwargs.as_object().as_raw().cast_mut(); + unsafe { Ok(f(slf_ptr, arg_tuple_ptr, kwargs_ptr)) } + }, + _ => panic!("Unexpected flags value: {flags:?}"), + })?; let ret_ptr = NonNull::new(ret_ptr) .ok_or_else(|| vm.new_system_error("native method returned NULL".to_owned()))?; @@ -76,14 +86,26 @@ pub extern "C" fn PyCMethod_New( .to_str() .expect("Method doc was not valid UTF-8"); - let ml_flags = ml.ml_flags; - let ml_meth = ml.ml_meth; - let slf = unsafe { &*slf }.to_owned(); + let flags = PyMethodFlags::from_bits(ml.ml_flags as u32) + .expect("PyMethodDef contains unknown flags"); + + assert!( + !flags.intersects( + PyMethodFlags::METHOD + | PyMethodFlags::FASTCALL + | PyMethodFlags::CLASS + | PyMethodFlags::STATIC + ), + "These flags are not supported not supported yet: {:?}", + flags + ); + + let method = ml.ml_meth; + let slf = NonNull::new(slf).map(|ptr| unsafe { ptr.as_ref().to_owned() }); let callable = move |args: FuncArgs, vm: &VirtualMachine| { - c_function_wrapper(vm, &slf, args, ml_meth, ml_flags) + c_function_wrapper(vm, slf.as_ref(), args, method, flags) }; - let flags = PyMethodFlags::from_bits_truncate(ml_flags as u32); let method = vm.ctx.new_method_def(name, callable, flags, Some(doc)); Ok(method.build_function(vm).into()) }) @@ -91,11 +113,12 @@ pub extern "C" fn PyCMethod_New( #[cfg(test)] mod tests { + use pyo3::ffi::{PyLong_FromLong, PyObject}; use pyo3::prelude::*; - use pyo3::types::{PyCFunction, PyString}; + use pyo3::types::{PyCFunction, PyInt, PyString}; #[test] - fn test_new_c_function() { + fn test_closure_function() { Python::attach(|py| { let f = PyCFunction::new_closure(py, None, None, |_args, _kwargs| "Hello from Rust!") .unwrap(); @@ -106,4 +129,25 @@ mod tests { ); }) } + + #[test] + fn test_function_no_args() { + Python::attach(|py| { + unsafe extern "C" fn c_fn(_self: *mut PyObject, _args: *mut PyObject) -> *mut PyObject { + assert!(_self.is_null()); + unsafe { PyLong_FromLong(4200) } + } + + let py_fn = PyCFunction::new(py, c_fn, c"py_fn", c"", None).unwrap(); + + let result = py_fn + .call0() + .unwrap() + .cast::() + .unwrap() + .extract::() + .unwrap(); + assert_eq!(result, 4200); + }) + } } diff --git a/crates/capi/src/moduleobject.rs b/crates/capi/src/moduleobject.rs new file mode 100644 index 00000000000..29dc1a00203 --- /dev/null +++ b/crates/capi/src/moduleobject.rs @@ -0,0 +1,11 @@ +use crate::PyObject; +use crate::pystate::with_vm; +use rustpython_vm::builtins::PyModule; + +#[unsafe(no_mangle)] +pub extern "C" fn PyModule_GetNameObject(module: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let module = unsafe { &*module }.try_downcast_ref::(vm)?; + module.get_attr("__name__", vm) + }) +} From 248ea520fa5bf66fcddfd2869719084a1c0a78d0 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 11 Apr 2026 17:29:40 +0200 Subject: [PATCH 053/135] Fix error return of `PyCFunction` --- crates/capi/src/methodobject.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 2e84166e6e4..ccbc2fa922e 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -64,8 +64,10 @@ fn c_function_wrapper( _ => panic!("Unexpected flags value: {flags:?}"), })?; - let ret_ptr = NonNull::new(ret_ptr) - .ok_or_else(|| vm.new_system_error("native method returned NULL".to_owned()))?; + let ret_ptr = NonNull::new(ret_ptr).ok_or_else(|| { + vm.take_raised_exception() + .expect("Native function returned NULL, but there was no exception set") + })?; Ok(unsafe { PyObjectRef::from_raw(ret_ptr) }) } @@ -96,7 +98,7 @@ pub extern "C" fn PyCMethod_New( | PyMethodFlags::CLASS | PyMethodFlags::STATIC ), - "These flags are not supported not supported yet: {:?}", + "These flags are not yet supported: {:?}", flags ); @@ -113,6 +115,7 @@ pub extern "C" fn PyCMethod_New( #[cfg(test)] mod tests { + use pyo3::exceptions::PyException; use pyo3::ffi::{PyLong_FromLong, PyObject}; use pyo3::prelude::*; use pyo3::types::{PyCFunction, PyInt, PyString}; @@ -150,4 +153,20 @@ mod tests { assert_eq!(result, 4200); }) } + + #[test] + fn test_closure_function_error() { + Python::attach(|py| { + let f = PyCFunction::new_closure(py, None, None, |_args, _kwargs| { + Err::<(), _>(PyException::new_err("Something went wrong")) + }) + .unwrap(); + + let err = f.call0().unwrap_err(); + assert_eq!( + err.value(py).repr().unwrap(), + "Exception('Something went wrong')" + ); + }) + } } From ad0d1af4a1474757cc6b5444e052d75107537f98 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 11 Apr 2026 18:05:09 +0200 Subject: [PATCH 054/135] Add `PyDict` iter support --- crates/capi/src/dictobject.rs | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/crates/capi/src/dictobject.rs b/crates/capi/src/dictobject.rs index e5eea6cc27f..83984886fd0 100644 --- a/crates/capi/src/dictobject.rs +++ b/crates/capi/src/dictobject.rs @@ -1,6 +1,7 @@ use crate::PyObject; use crate::pystate::with_vm; use core::ffi::c_int; +use rustpython_vm::AsObject; use rustpython_vm::builtins::PyDict; #[unsafe(no_mangle)] @@ -55,6 +56,31 @@ pub extern "C" fn PyDict_Size(dict: *mut PyObject) -> isize { }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyDict_Next( + dict: *mut PyObject, + pos: *mut isize, + key: *mut *mut PyObject, + value: *mut *mut PyObject, +) -> c_int { + with_vm(|vm| { + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + let index = unsafe { *pos } as usize; + let items = dict.items_vec(); + + if let Some((k, v)) = items.get(index) { + unsafe { + *key = k.as_object().as_raw().cast_mut(); + *value = v.as_object().as_raw().cast_mut(); + *pos += 1; + } + Ok(1) + } else { + Ok(0) + } + }) +} + #[cfg(test)] mod tests { use pyo3::prelude::*; @@ -80,4 +106,16 @@ mod tests { }) .unwrap() } + + #[test] + fn test_dict_iter() { + Python::attach(|py| { + let dict = [(1, 2), (3, 4)].into_py_dict(py).unwrap(); + let values = dict + .into_iter() + .flat_map(|(k, v)| [k.extract().unwrap(), v.extract().unwrap()]) + .collect::>(); + assert_eq!(values, vec![1, 2, 3, 4]); + }) + } } From d81ad21f3556bbc21c1fcf5b37eed635addec926 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 11 Apr 2026 18:06:26 +0200 Subject: [PATCH 055/135] Add more `Exception` type statics --- crates/capi/src/pyerrors.rs | 38 ++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index 685aa53fe32..a5a34a44779 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -25,6 +25,15 @@ pub static mut PyExc_OverflowError: MaybeUninit<*mut PyObject> = MaybeUninit::un #[unsafe(no_mangle)] pub static mut PyExc_IndexError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); +#[unsafe(no_mangle)] +pub static mut PyExc_AttributeError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); + +#[unsafe(no_mangle)] +pub static mut PyExc_RuntimeError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); + +#[unsafe(no_mangle)] +pub static mut PyExc_ValueError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); + #[unsafe(no_mangle)] pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject { with_vm(|vm| vm.take_raised_exception()) @@ -141,9 +150,32 @@ pub extern "C" fn PyErr_NewExceptionWithDoc( } #[unsafe(no_mangle)] -pub extern "C" fn PyException_GetTraceback(_exc: *mut PyObject) -> *mut PyObject { - crate::log_stub("PyException_GetTraceback"); - core::ptr::null_mut() +pub extern "C" fn PyException_GetTraceback(exc: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let exc = unsafe { &*exc }; + exc.get_attr("__traceback__", vm) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyException_SetCause(exc: *mut PyObject, cause: *mut PyObject) { + with_vm(|vm| { + let exc = unsafe { &*exc }; + let cause = unsafe { cause.as_ref() }.map(|obj| obj.to_owned()); + if let Err(err) = exc.set_attr("__cause__", vm.unwrap_or_none(cause), vm) { + vm.push_exception(Some(err)); + } + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyException_SetTraceback(exc: *mut PyObject, tb: *mut PyObject) -> c_int { + with_vm(|vm| { + let exc = unsafe { &*exc }; + let traceback = unsafe { tb.as_ref() }.map(|obj| obj.to_owned()); + exc.set_attr("__traceback__", vm.unwrap_or_none(traceback), vm)?; + Ok(0) + }) } #[unsafe(no_mangle)] From 6cd7fc9dccc08638992bddfeca99f97ff69ae2ba Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 13 Apr 2026 11:25:55 +0200 Subject: [PATCH 056/135] Add support for `set`, `get`, & `del` on any object --- crates/capi/src/abstract_.rs | 53 +++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index c19a86ca817..090e785e0e2 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -77,6 +77,40 @@ pub extern "C" fn PyObject_VectorcallMethod( }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_GetItem(obj: *mut PyObject, key: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let key = unsafe { &*key }; + obj.get_item(key, vm) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_SetItem( + obj: *mut PyObject, + key: *mut PyObject, + value: *mut PyObject, +) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let key = unsafe { &*key }; + let value = unsafe { &*value }.to_owned(); + obj.set_item(key, value, vm)?; + Ok(0) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_DelItem(obj: *mut PyObject, key: *mut PyObject) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let key = unsafe { &*key }; + obj.del_item(key, vm)?; + Ok(0) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn PySequence_Contains(obj: *mut PyObject, value: *mut PyObject) -> c_int { with_vm(|vm| { @@ -99,7 +133,7 @@ pub extern "C" fn PySequence_Contains(obj: *mut PyObject, value: *mut PyObject) #[cfg(test)] mod tests { use pyo3::prelude::*; - use pyo3::types::PyString; + use pyo3::types::{PyDict, PyString}; #[test] #[cfg(feature = "nightly")] @@ -126,4 +160,21 @@ mod tests { ); }) } + + #[test] + fn test_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()); + }) + } } From ae3f3db76265fed2cbf5dfad3f48ef61d853ce20 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 13 Apr 2026 12:24:39 +0200 Subject: [PATCH 057/135] Refactor `FfiResult` --- crates/capi/src/abstract_.rs | 8 +- crates/capi/src/bytesobject.rs | 4 +- crates/capi/src/dictobject.rs | 13 +- crates/capi/src/listobject.rs | 6 +- crates/capi/src/longobject.rs | 2 +- crates/capi/src/pyerrors.rs | 17 ++- crates/capi/src/pystate.rs | 2 +- crates/capi/src/refcount.rs | 2 +- crates/capi/src/tupleobject.rs | 4 +- crates/capi/src/unicodeobject.rs | 2 +- crates/capi/src/util.rs | 202 +++++++++++-------------------- 11 files changed, 98 insertions(+), 164 deletions(-) diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index 090e785e0e2..3ccc043eedc 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -50,7 +50,7 @@ pub extern "C" fn PyObject_VectorcallMethod( nargsf: usize, kwnames: *mut PyObject, ) -> *mut PyObject { - with_vm::(|vm| { + with_vm::(|vm| { let args_len = nargsf & !PY_VECTORCALL_ARGUMENTS_OFFSET; let num_positional_args = args_len - 1; @@ -96,8 +96,7 @@ pub extern "C" fn PyObject_SetItem( let obj = unsafe { &*obj }; let key = unsafe { &*key }; let value = unsafe { &*value }.to_owned(); - obj.set_item(key, value, vm)?; - Ok(0) + obj.set_item(key, value, vm) }) } @@ -106,8 +105,7 @@ pub extern "C" fn PyObject_DelItem(obj: *mut PyObject, key: *mut PyObject) -> c_ with_vm(|vm| { let obj = unsafe { &*obj }; let key = unsafe { &*key }; - obj.del_item(key, vm)?; - Ok(0) + obj.del_item(key, vm) }) } diff --git a/crates/capi/src/bytesobject.rs b/crates/capi/src/bytesobject.rs index b955ca56e6d..8000cd22544 100644 --- a/crates/capi/src/bytesobject.rs +++ b/crates/capi/src/bytesobject.rs @@ -19,7 +19,7 @@ pub extern "C" fn PyBytes_FromStringAndSize(bytes: *mut c_char, len: isize) -> * pub extern "C" fn PyBytes_Size(bytes: *mut PyObject) -> isize { with_vm(|vm| { let bytes = unsafe { &*bytes }.try_downcast_ref::(vm)?; - Ok(bytes.as_bytes().len() as isize) + Ok(bytes.as_bytes().len()) }) } @@ -27,7 +27,7 @@ pub extern "C" fn PyBytes_Size(bytes: *mut PyObject) -> isize { pub extern "C" fn PyBytes_AsString(bytes: *mut PyObject) -> *mut c_char { with_vm(|vm| { let bytes = unsafe { &*bytes }.try_downcast_ref::(vm)?; - Ok(bytes.as_bytes().as_ptr() as *mut c_char) + Ok(bytes.as_bytes().as_ptr()) }) } diff --git a/crates/capi/src/dictobject.rs b/crates/capi/src/dictobject.rs index 83984886fd0..b36d30516c7 100644 --- a/crates/capi/src/dictobject.rs +++ b/crates/capi/src/dictobject.rs @@ -19,8 +19,7 @@ pub extern "C" fn PyDict_SetItem( let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; let key = unsafe { &*key }; let value = unsafe { &*val }.to_owned(); - dict.set_item(key, value, vm)?; - Ok(0) + dict.set_item(key, value, vm) }) } @@ -38,12 +37,12 @@ pub extern "C" fn PyDict_GetItemRef( unsafe { *result = value.into_raw().as_ptr(); } - Ok(1) + Ok(true) } else { unsafe { *result = core::ptr::null_mut(); } - Ok(0) + Ok(false) } }) } @@ -52,7 +51,7 @@ pub extern "C" fn PyDict_GetItemRef( pub extern "C" fn PyDict_Size(dict: *mut PyObject) -> isize { with_vm(|vm| { let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; - Ok(dict.__len__() as isize) + Ok(dict.__len__()) }) } @@ -74,9 +73,9 @@ pub extern "C" fn PyDict_Next( *value = v.as_object().as_raw().cast_mut(); *pos += 1; } - Ok(1) + Ok(true) } else { - Ok(0) + Ok(false) } }) } diff --git a/crates/capi/src/listobject.rs b/crates/capi/src/listobject.rs index 8fb2053d99d..8f84c2e6cba 100644 --- a/crates/capi/src/listobject.rs +++ b/crates/capi/src/listobject.rs @@ -14,7 +14,7 @@ pub extern "C" fn PyList_New(size: isize) -> *mut PyObject { pub extern "C" fn PyList_Size(obj: *mut PyObject) -> isize { with_vm(|vm| { let list = unsafe { &*obj }.try_downcast_ref::(vm)?; - Ok(list.__len__() as isize) + Ok(list.__len__()) }) } @@ -40,12 +40,12 @@ pub extern "C" fn PyList_SetItem(list: *mut PyObject, index: isize, item: *mut P match index - list_mut.len() as isize { ..0 => { list_mut[index as usize] = item; - Ok(0) + Ok(()) } // This is somewhat a hack, we assume that we are populating a list right after PyList_New 0 if list_mut.capacity() > index as usize => { list_mut.push(item); - Ok(0) + Ok(()) } 0.. => Err(vm.new_index_error(format!("list assignment index out of range: {index}"))), } diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index 805ab121533..62a32ef13a2 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -35,7 +35,7 @@ pub extern "C" fn PyLong_FromUnsignedLongLong(value: c_ulonglong) -> *mut PyObje #[unsafe(no_mangle)] pub extern "C" fn PyLong_AsLong(obj: *mut PyObject) -> c_long { - with_vm::>(|vm| { + with_vm::, _>(|vm| { // SAFETY: non-null checked above; caller promises a valid PyObject pointer. let obj_ref = unsafe { &*obj }; let int_obj = obj_ref diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index a5a34a44779..7ee59dc3829 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -41,7 +41,7 @@ pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject { #[unsafe(no_mangle)] pub extern "C" fn PyErr_SetRaisedException(exc: *mut PyObject) { - with_vm::>(|_vm| { + with_vm::, _>(|_vm| { let exception = unsafe { (&*exc).to_owned().downcast_unchecked() }; Err(exception) }); @@ -49,7 +49,7 @@ pub extern "C" fn PyErr_SetRaisedException(exc: *mut PyObject) { #[unsafe(no_mangle)] pub extern "C" fn PyErr_SetObject(exception: *mut PyObject, value: *mut PyObject) { - with_vm::>(|vm| { + with_vm::, _>(|vm| { let exc_type = unsafe { (&*exception).to_owned() }; let exc_val = unsafe { (&*value).to_owned() }; @@ -61,7 +61,7 @@ pub extern "C" fn PyErr_SetObject(exception: *mut PyObject, value: *mut PyObject #[unsafe(no_mangle)] pub extern "C" fn PyErr_SetString(exception: *mut PyObject, message: *const c_char) { - with_vm::>(|vm| { + with_vm::, _>(|vm| { let exc_type = unsafe { &*exception }.try_downcast_ref::(vm)?; let message = unsafe { CStr::from_ptr(message) } @@ -85,7 +85,7 @@ pub extern "C" fn PyErr_PrintEx(_set_sys_last_vars: c_int) { .expect("No exception set in PyErr_PrintEx"); vm.print_exception(exception); - }); + }) } #[unsafe(no_mangle)] @@ -98,7 +98,7 @@ pub extern "C" fn PyErr_WriteUnraisable(obj: *mut PyObject) { let object = unsafe { vm.unwrap_or_none(obj.as_ref().map(|obj| obj.to_owned())) }; vm.run_unraisable(exception, None, object) - }); + }) } #[unsafe(no_mangle)] @@ -162,9 +162,7 @@ pub extern "C" fn PyException_SetCause(exc: *mut PyObject, cause: *mut PyObject) with_vm(|vm| { let exc = unsafe { &*exc }; let cause = unsafe { cause.as_ref() }.map(|obj| obj.to_owned()); - if let Err(err) = exc.set_attr("__cause__", vm.unwrap_or_none(cause), vm) { - vm.push_exception(Some(err)); - } + exc.set_attr("__cause__", vm.unwrap_or_none(cause), vm) }) } @@ -173,8 +171,7 @@ pub extern "C" fn PyException_SetTraceback(exc: *mut PyObject, tb: *mut PyObject with_vm(|vm| { let exc = unsafe { &*exc }; let traceback = unsafe { tb.as_ref() }.map(|obj| obj.to_owned()); - exc.set_attr("__traceback__", vm.unwrap_or_none(traceback), vm)?; - Ok(0) + exc.set_attr("__traceback__", vm.unwrap_or_none(traceback), vm) }) } diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index 36995cdd1dc..92d9dea621e 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -10,7 +10,7 @@ thread_local! { static VM: RefCell> = const { RefCell::new(None) }; } -pub(crate) fn with_vm(f: impl FnOnce(&VirtualMachine) -> R) -> R::Output { +pub(crate) fn with_vm, O>(f: impl FnOnce(&VirtualMachine) -> R) -> O { VM.with(|vm_ref| { let vm = vm_ref.borrow(); let vm = vm diff --git a/crates/capi/src/refcount.rs b/crates/capi/src/refcount.rs index 9d5ebb64cdd..fc73f78b616 100644 --- a/crates/capi/src/refcount.rs +++ b/crates/capi/src/refcount.rs @@ -18,7 +18,7 @@ pub extern "C" fn _Py_IncRef(op: *mut PyObject) { #[unsafe(no_mangle)] pub extern "C" fn Py_REFCNT(op: *mut PyObject) -> isize { - with_vm(|_vm| unsafe { &*op }.strong_count() as isize) + with_vm(|_vm| unsafe { &*op }.strong_count()) } #[cfg(test)] diff --git a/crates/capi/src/tupleobject.rs b/crates/capi/src/tupleobject.rs index 221f9770d7e..e34f457b0fd 100644 --- a/crates/capi/src/tupleobject.rs +++ b/crates/capi/src/tupleobject.rs @@ -20,7 +20,7 @@ pub extern "C" fn PyTuple_SetItem( _pos: isize, _value: *mut PyObject, ) -> *mut PyObject { - with_vm::(|vm| { + with_vm::(|vm| { Err(vm.new_not_implemented_error("PyTuple_SetItem is not yet implemented")) }) } @@ -29,7 +29,7 @@ pub extern "C" fn PyTuple_SetItem( pub extern "C" fn PyTuple_Size(tuple: *mut PyObject) -> isize { with_vm(|vm| { let tuple = unsafe { &*tuple }.try_downcast_ref::(vm)?; - Ok(tuple.__len__() as isize) + Ok(tuple.__len__()) }) } diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index 8fee64f1cec..501460777c2 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -41,7 +41,7 @@ pub extern "C" fn PyUnicode_AsUTF8AndSize(obj: *mut PyObject, size: *mut isize) if !size.is_null() { unsafe { *size = str.len() as isize }; } - Ok(str.as_ptr() as *const c_char) + Ok(str.as_ptr()) }) } diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 53b975bf884..5f98bb5e4d9 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -3,197 +3,137 @@ use core::convert::Infallible; use core::ffi::{c_char, c_double, c_int, c_long, c_void}; use rustpython_vm::{PyObjectRef, PyRef, PyResult, VirtualMachine}; -pub(crate) trait FfiResult { - type Output; - fn into_output(self, vm: &VirtualMachine) -> Self::Output; +pub(crate) trait FfiResult { + const ERR_VALUE: Output; + + fn into_output(self, vm: &VirtualMachine) -> Output; } impl FfiResult for () { - type Output = (); + const ERR_VALUE: () = (); - fn into_output(self, _vm: &VirtualMachine) -> Self::Output { + fn into_output(self, _vm: &VirtualMachine) -> () { self } } -impl FfiResult for PyRef -where - Self: Into, -{ - type Output = *mut PyObject; +impl FfiResult for () { + const ERR_VALUE: c_int = -1; - fn into_output(self, _vm: &VirtualMachine) -> Self::Output { - self.into().into_raw().as_ptr() + fn into_output(self, _vm: &VirtualMachine) -> c_int { + 0 } } -impl FfiResult for Option> +impl FfiResult<*mut PyObject> for PyRef where - PyRef: Into, + Self: Into, { - type Output = *mut PyObject; + const ERR_VALUE: *mut PyObject = core::ptr::null_mut(); - fn into_output(self, _vm: &VirtualMachine) -> Self::Output { - self.map_or_else(core::ptr::null_mut, |obj| obj.into().into_raw().as_ptr()) + fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject { + self.into().into_raw().as_ptr() } } -impl FfiResult for PyObjectRef { - type Output = *mut PyObject; +impl FfiResult<*mut PyObject> for PyObjectRef { + const ERR_VALUE: *mut PyObject = std::ptr::null_mut(); - fn into_output(self, _vm: &VirtualMachine) -> Self::Output { + fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject { self.into_raw().as_ptr() } } -impl FfiResult for *const PyObject { - type Output = *mut PyObject; - - fn into_output(self, _vm: &VirtualMachine) -> Self::Output { - self.cast_mut() - } -} - -impl FfiResult for PyResult<*const PyObject> { - type Output = *mut PyObject; - - fn into_output(self, vm: &VirtualMachine) -> Self::Output { - self.map_or_else( - |err| { - vm.push_exception(Some(err)); - core::ptr::null_mut() - }, - |ptr| ptr.cast_mut(), - ) - } -} - -impl FfiResult for PyResult { - type Output = *mut PyObject; - - fn into_output(self, vm: &VirtualMachine) -> Self::Output { - self.map_or_else( - |err| { - vm.push_exception(Some(err)); - core::ptr::null_mut() - }, - |obj| obj.into_raw().as_ptr(), - ) - } -} -impl FfiResult for PyResult> -where - PyRef: Into, -{ - type Output = *mut PyObject; - - fn into_output(self, vm: &VirtualMachine) -> Self::Output { - self.map(Into::into).into_output(vm) - } -} - -impl FfiResult for PyResult { - type Output = c_long; +impl FfiResult<*mut PyObject> for *const PyObject { + const ERR_VALUE: *mut PyObject = std::ptr::null_mut(); - fn into_output(self, vm: &VirtualMachine) -> Self::Output { - self.unwrap_or_else(|err| { - vm.push_exception(Some(err)); - -1 - }) + fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject { + self.cast_mut() } } -impl FfiResult for isize { - type Output = isize; +impl FfiResult for *mut c_void { + const ERR_VALUE: *mut c_void = std::ptr::null_mut(); - fn into_output(self, _vm: &VirtualMachine) -> Self::Output { + fn into_output(self, _vm: &VirtualMachine) -> *mut c_void { self } } -impl FfiResult for PyResult { - type Output = isize; +impl FfiResult<*mut c_char> for *const u8 { + const ERR_VALUE: *mut c_char = std::ptr::null_mut(); - fn into_output(self, vm: &VirtualMachine) -> Self::Output { - self.unwrap_or_else(|err| { - vm.push_exception(Some(err)); - -1 - }) + fn into_output(self, _vm: &VirtualMachine) -> *mut c_char { + self.cast_mut().cast() } } -impl FfiResult for PyResult { - type Output = c_int; +impl FfiResult for usize { + const ERR_VALUE: isize = -1; - fn into_output(self, vm: &VirtualMachine) -> Self::Output { - self.map_or_else( - |err| { - vm.push_exception(Some(err)); - -1 - }, - Into::into, - ) + fn into_output(self, _vm: &VirtualMachine) -> isize { + self as isize } } -impl FfiResult for PyResult { - type Output = c_int; +impl FfiResult for c_long { + const ERR_VALUE: c_long = -1; - fn into_output(self, vm: &VirtualMachine) -> Self::Output { - self.unwrap_or_else(|err| { - vm.push_exception(Some(err)); - -1 - }) + fn into_output(self, _vm: &VirtualMachine) -> c_long { + self } } -impl FfiResult for PyResult { - type Output = c_double; +impl FfiResult for c_double { + const ERR_VALUE: c_double = -1.0; - fn into_output(self, vm: &VirtualMachine) -> Self::Output { - self.unwrap_or_else(|err| { - vm.push_exception(Some(err)); - -1.0 - }) + fn into_output(self, _vm: &VirtualMachine) -> c_double { + self } } -impl FfiResult for PyResult<*mut c_char> { - type Output = *mut c_char; +impl FfiResult for bool { + const ERR_VALUE: c_int = -1; - fn into_output(self, vm: &VirtualMachine) -> Self::Output { - self.map(|ptr| ptr.cast_const()).into_output(vm).cast_mut() + fn into_output(self, _vm: &VirtualMachine) -> c_int { + self as c_int } } -impl FfiResult for PyResult<*mut c_void> { - type Output = *mut c_void; +impl FfiResult<()> for PyResult { + const ERR_VALUE: () = (); - fn into_output(self, vm: &VirtualMachine) -> Self::Output { - self.unwrap_or_else(|err| { - vm.push_exception(Some(err)); - core::ptr::null_mut() - }) + fn into_output(self, vm: &VirtualMachine) -> () { + match self { + Err(err) => vm.push_exception(Some(err)), + } } } -impl FfiResult for PyResult<*const c_char> { - type Output = *const c_char; +impl FfiResult<*mut PyObject> for Option +where + T: FfiResult<*mut PyObject>, +{ + const ERR_VALUE: *mut PyObject = T::ERR_VALUE; - fn into_output(self, vm: &VirtualMachine) -> Self::Output { - self.unwrap_or_else(|err| { - vm.push_exception(Some(err)); - core::ptr::null_mut() - }) + fn into_output(self, vm: &VirtualMachine) -> *mut PyObject { + self.map_or_else(|| Self::ERR_VALUE, |obj| obj.into_output(vm)) } } -impl FfiResult for PyResult { - type Output = (); +impl FfiResult for PyResult +where + T: FfiResult, +{ + const ERR_VALUE: Output = T::ERR_VALUE; - fn into_output(self, vm: &VirtualMachine) -> Self::Output { - match self { - Err(err) => vm.push_exception(Some(err)), - } + fn into_output(self, vm: &VirtualMachine) -> Output { + self.map_or_else( + |err| { + vm.push_exception(Some(err)); + T::ERR_VALUE + }, + |obj| obj.into_output(vm), + ) } } From 4d23f7c44648c118ef5a700acc58c6e53b37a4d6 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 13 Apr 2026 13:12:05 +0200 Subject: [PATCH 058/135] Fix unset exception types --- crates/capi/src/pylifecycle.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 817058bc745..3bf6dbdc0c1 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -4,8 +4,8 @@ use crate::object::{ PyUnicode_Type, }; use crate::pyerrors::{ - PyExc_BaseException, PyExc_Exception, PyExc_IndexError, PyExc_OverflowError, PyExc_SystemError, - PyExc_TypeError, + PyExc_AttributeError, PyExc_BaseException, PyExc_Exception, PyExc_IndexError, + PyExc_OverflowError, PyExc_RuntimeError, PyExc_SystemError, PyExc_TypeError, PyExc_ValueError, }; use crate::pystate::attach_vm_to_thread; use core::ffi::c_int; @@ -57,6 +57,9 @@ pub(crate) fn init_static_type_pointers() { PyExc_TypeError.write(exc.type_error.as_object().as_raw().cast_mut()); PyExc_OverflowError.write(exc.overflow_error.as_object().as_raw().cast_mut()); PyExc_IndexError.write(exc.index_error.as_object().as_raw().cast_mut()); + PyExc_AttributeError.write(exc.attribute_error.as_object().as_raw().cast_mut()); + PyExc_RuntimeError.write(exc.runtime_error.as_object().as_raw().cast_mut()); + PyExc_ValueError.write(exc.value_error.as_object().as_raw().cast_mut()); }; } From 56f38231ba0b644cada0cda0929e23d21cd43e49 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 13 Apr 2026 13:13:44 +0200 Subject: [PATCH 059/135] Add `PyBaseObject_Type` static --- crates/capi/src/object.rs | 3 +++ crates/capi/src/pylifecycle.rs | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 8570e8ba08a..f4a6b5d2a88 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -20,6 +20,9 @@ pub type PyTypeObject = Py; #[unsafe(no_mangle)] pub static mut PyType_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); +#[unsafe(no_mangle)] +pub static mut PyBaseObject_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); + #[unsafe(no_mangle)] pub static mut PyLong_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 3bf6dbdc0c1..10804890fe6 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,7 +1,7 @@ use crate::log_stub; use crate::object::{ - PyBool_Type, PyComplex_Type, PyDict_Type, PyLong_Type, PyTuple_Type, PyType_Type, - PyUnicode_Type, + PyBaseObject_Type, PyBool_Type, PyComplex_Type, PyDict_Type, PyLong_Type, PyTuple_Type, + PyType_Type, PyUnicode_Type, }; use crate::pyerrors::{ PyExc_AttributeError, PyExc_BaseException, PyExc_Exception, PyExc_IndexError, @@ -43,6 +43,7 @@ pub(crate) fn init_static_type_pointers() { unsafe { PyType_Type.write(types.type_type); + PyBaseObject_Type.write(types.object_type); PyLong_Type.write(types.int_type); PyTuple_Type.write(types.tuple_type); PyUnicode_Type.write(types.str_type); From 4f6798a5cf65b3b7152b2f57ca33ff4bd57442f5 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 13 Apr 2026 17:35:08 +0200 Subject: [PATCH 060/135] Add `PyObject_SetAttr` --- crates/capi/src/object.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index f4a6b5d2a88..b207c066762 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -131,6 +131,20 @@ pub extern "C" fn PyObject_GetAttr(obj: *mut PyObject, name: *mut PyObject) -> * }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_SetAttr( + obj: *mut PyObject, + name: *mut PyObject, + value: *mut PyObject, +) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let name = unsafe { &*name }.try_downcast_ref::(vm)?; + let value = unsafe { &*value }.to_owned(); + obj.set_attr(name, value, vm) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn PyObject_Repr(obj: *mut PyObject) -> *mut PyObject { with_vm(|vm| { From 685c4b729f9acef6846b3e0438c79f9ff2ab1f76 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 13 Apr 2026 17:35:28 +0200 Subject: [PATCH 061/135] Add `PyObject_GenericGetDict` & `PyObject_GenericSetDict` --- crates/capi/src/object.rs | 48 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index b207c066762..54521c6c841 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,6 +1,5 @@ use crate::{PyObject, with_vm}; -use core::ffi::c_ulong; -use core::ffi::{c_int, c_uint}; +use core::ffi::{c_int, c_uint, c_ulong, c_void}; use core::mem::MaybeUninit; use core::ptr::NonNull; use rustpython_vm::builtins::{PyStr, PyType}; @@ -191,10 +190,34 @@ pub extern "C" fn PyObject_IsTrue(obj: *mut PyObject) -> c_int { }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_GenericGetDict( + obj: *mut PyObject, + _context: *mut c_void, +) -> *mut PyObject { + with_vm(|vm| { + let obj = unsafe { &*obj }; + obj.get_attr("__dict__", vm) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_GenericSetDict( + obj: *mut PyObject, + value: *mut PyObject, + _context: *mut c_void, +) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let value = unsafe { &*value }.to_owned(); + obj.set_attr("__dict__", value, vm) + }) +} + #[cfg(test)] mod tests { use pyo3::prelude::*; - use pyo3::types::{PyBool, PyInt, PyNone, PyString}; + use pyo3::types::{PyBool, PyDict, PyInt, PyNone, PyString}; #[test] fn test_is_truthy() { @@ -266,4 +289,23 @@ mod tests { assert_eq!(implementation, "rustpython"); }) } + + #[test] + fn test_generic_get_dict() { + Python::attach(|py| { + let globals = PyDict::new(py); + py.run(c"class MyClass: ...", None, Some(&globals)).unwrap(); + let my_class = globals.get_item("MyClass").unwrap().unwrap(); + let instance = my_class.call0().unwrap(); + instance.setattr("foo", 42).unwrap(); + let dict = unsafe { + Bound::from_owned_ptr_or_err( + py, + pyo3::ffi::PyObject_GenericGetDict(instance.as_ptr(), std::ptr::null_mut()), + ) + } + .unwrap(); + assert!(dict.get_item("foo").is_ok()); + }) + } } From ac50f2b6db8dba02c10dde2f8502172092989121 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 13 Apr 2026 17:55:07 +0200 Subject: [PATCH 062/135] Fix some clippy warning --- crates/capi/Cargo.toml | 2 +- crates/capi/src/util.rs | 12 ++++++------ crates/vm/src/builtins/capsule.rs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index b4c2188aa83..e7d5bc6f7eb 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -21,7 +21,7 @@ rustpython-vm = { workspace = true, features = ["threading", "compiler"]} nightly = [] [dev-dependencies] -pyo3 = { git = "https://github.com/PyO3/pyo3", features = ["auto-initialize", "abi3-py314"] } +pyo3 = { git = "https://github.com/PyO3/pyo3", features = ["auto-initialize", "abi3"] } [lints] workspace = true diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 5f98bb5e4d9..6fdcea9bc58 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -12,7 +12,7 @@ pub(crate) trait FfiResult { impl FfiResult for () { const ERR_VALUE: () = (); - fn into_output(self, _vm: &VirtualMachine) -> () { + fn into_output(self, _vm: &VirtualMachine) { self } } @@ -37,7 +37,7 @@ where } impl FfiResult<*mut PyObject> for PyObjectRef { - const ERR_VALUE: *mut PyObject = std::ptr::null_mut(); + const ERR_VALUE: *mut PyObject = core::ptr::null_mut(); fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject { self.into_raw().as_ptr() @@ -45,7 +45,7 @@ impl FfiResult<*mut PyObject> for PyObjectRef { } impl FfiResult<*mut PyObject> for *const PyObject { - const ERR_VALUE: *mut PyObject = std::ptr::null_mut(); + const ERR_VALUE: *mut PyObject = core::ptr::null_mut(); fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject { self.cast_mut() @@ -53,7 +53,7 @@ impl FfiResult<*mut PyObject> for *const PyObject { } impl FfiResult for *mut c_void { - const ERR_VALUE: *mut c_void = std::ptr::null_mut(); + const ERR_VALUE: *mut c_void = core::ptr::null_mut(); fn into_output(self, _vm: &VirtualMachine) -> *mut c_void { self @@ -61,7 +61,7 @@ impl FfiResult for *mut c_void { } impl FfiResult<*mut c_char> for *const u8 { - const ERR_VALUE: *mut c_char = std::ptr::null_mut(); + const ERR_VALUE: *mut c_char = core::ptr::null_mut(); fn into_output(self, _vm: &VirtualMachine) -> *mut c_char { self.cast_mut().cast() @@ -103,7 +103,7 @@ impl FfiResult for bool { impl FfiResult<()> for PyResult { const ERR_VALUE: () = (); - fn into_output(self, vm: &VirtualMachine) -> () { + fn into_output(self, vm: &VirtualMachine) { match self { Err(err) => vm.push_exception(Some(err)), } diff --git a/crates/vm/src/builtins/capsule.rs b/crates/vm/src/builtins/capsule.rs index 13c372e993a..9be57c09fed 100644 --- a/crates/vm/src/builtins/capsule.rs +++ b/crates/vm/src/builtins/capsule.rs @@ -36,7 +36,7 @@ impl PyCapsule { } pub fn pointer(&self) -> *mut c_void { - self.ptr.load(std::sync::atomic::Ordering::Relaxed) + self.ptr.load(core::sync::atomic::Ordering::Relaxed) } fn destructor(&self) -> Option { From 9e931f711637a45c3070127c1e6b5d6ffdb647b2 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 14 Apr 2026 14:29:23 +0200 Subject: [PATCH 063/135] Add `PyObject_SetAttrString` --- crates/capi/src/object.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 54521c6c841..0944b0b81b3 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,5 +1,5 @@ use crate::{PyObject, with_vm}; -use core::ffi::{c_int, c_uint, c_ulong, c_void}; +use core::ffi::{CStr, c_char, c_int, c_uint, c_ulong, c_void}; use core::mem::MaybeUninit; use core::ptr::NonNull; use rustpython_vm::builtins::{PyStr, PyType}; @@ -130,6 +130,22 @@ pub extern "C" fn PyObject_GetAttr(obj: *mut PyObject, name: *mut PyObject) -> * }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_SetAttrString( + obj: *mut PyObject, + attr_name: *const c_char, + value: *mut PyObject, +) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let name = unsafe { CStr::from_ptr(attr_name) } + .to_str() + .expect("attribute name must be valid UTF-8"); + let value = unsafe { &*value }.to_owned(); + obj.set_attr(name, value, vm) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn PyObject_SetAttr( obj: *mut PyObject, From 384e15e62f65d1ef3f237d080c54340e6b2e147e Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 14 Apr 2026 14:41:11 +0200 Subject: [PATCH 064/135] Add `PyObject_GC_UnTrack` --- crates/capi/src/lib.rs | 1 + crates/capi/src/objimpl.rs | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 crates/capi/src/objimpl.rs diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 2792315d146..eadbefae472 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -16,6 +16,7 @@ pub(crate) mod longobject; pub(crate) mod methodobject; pub(crate) mod moduleobject; pub(crate) mod object; +pub(crate) mod objimpl; pub(crate) mod pyerrors; pub(crate) mod pylifecycle; pub(crate) mod pystate; diff --git a/crates/capi/src/objimpl.rs b/crates/capi/src/objimpl.rs new file mode 100644 index 00000000000..4242aa9f2f1 --- /dev/null +++ b/crates/capi/src/objimpl.rs @@ -0,0 +1,13 @@ +use crate::PyObject; +use crate::pystate::with_vm; +use rustpython_vm::gc_state; + +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_GC_UnTrack(op: *mut PyObject) { + with_vm(|_vm| { + let obj = unsafe { &*op }; + if obj.is_gc_tracked() { + unsafe { gc_state::gc_state().untrack_object(obj.into()) }; + } + }) +} From 09aab0a9513a36e8c4f11080eec1776a147df542 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 15 Apr 2026 16:01:33 +0200 Subject: [PATCH 065/135] Implement `Py_IS_TYPE` --- crates/capi/src/object.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 0944b0b81b3..a4aecf0eebb 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -46,6 +46,15 @@ pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const PyTypeObject { unsafe { (*op).class() } } +#[unsafe(no_mangle)] +pub extern "C" fn Py_IS_TYPE(op: *mut PyObject, ty: *mut PyTypeObject) -> c_int { + with_vm(|_vm| { + let obj = unsafe { &*op }; + let ty = unsafe { &*ty }; + obj.class().is(ty) + }) +} + #[unsafe(no_mangle)] #[allow(clippy::not_unsafe_ptr_arg_deref)] pub extern "C" fn PyType_GetFlags(ptr: *const PyTypeObject) -> c_ulong { From 7ee1e27108ce247c09885d082ca50690eb126422 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 16 Apr 2026 14:08:57 +0200 Subject: [PATCH 066/135] Add all exception statics --- crates/capi/src/pyerrors.rs | 119 +++++++++++++++++++++++++-------- crates/capi/src/pylifecycle.rs | 18 +---- 2 files changed, 93 insertions(+), 44 deletions(-) diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index 7ee59dc3829..eb86559f846 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -1,38 +1,99 @@ use crate::{PyObject, with_vm}; use core::convert::Infallible; -use core::ffi::CStr; -use core::ffi::{c_char, c_int}; +use core::ffi::{CStr, c_char, c_int}; use core::mem::MaybeUninit; -use rustpython_vm::PyResult; use rustpython_vm::builtins::{PyTuple, PyType}; use rustpython_vm::convert::IntoObject; +use rustpython_vm::exceptions::ExceptionZoo; +use rustpython_vm::{AsObject, PyResult}; + +macro_rules! define_exception_statics { + ($( $(#[$meta:meta])* $export:ident => $zoo:ident ),* $(,)?) => { + $( + $(#[$meta])* + #[unsafe(no_mangle)] + pub static mut $export: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); + )* + + #[allow(static_mut_refs)] + pub(crate) unsafe fn init_exception_statics(exc: &ExceptionZoo) { + unsafe { + $( + $export.write(exc.$zoo.as_object().as_raw().cast_mut()); + )* + } + } + }; +} -#[unsafe(no_mangle)] -pub static mut PyExc_BaseException: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyExc_Exception: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyExc_SystemError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyExc_TypeError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyExc_OverflowError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyExc_IndexError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyExc_AttributeError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyExc_RuntimeError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyExc_ValueError: MaybeUninit<*mut PyObject> = MaybeUninit::uninit(); +define_exception_statics! { + PyExc_BaseException => base_exception_type, + PyExc_BaseExceptionGroup => base_exception_group, + PyExc_SystemExit => system_exit, + PyExc_KeyboardInterrupt => keyboard_interrupt, + PyExc_GeneratorExit => generator_exit, + PyExc_Exception => exception_type, + PyExc_StopIteration => stop_iteration, + PyExc_StopAsyncIteration => stop_async_iteration, + PyExc_ArithmeticError => arithmetic_error, + PyExc_FloatingPointError => floating_point_error, + PyExc_SystemError => system_error, + PyExc_TypeError => type_error, + PyExc_OverflowError => overflow_error, + PyExc_ZeroDivisionError => zero_division_error, + PyExc_AssertionError => assertion_error, + PyExc_IndexError => index_error, + PyExc_KeyError => key_error, + PyExc_LookupError => lookup_error, + PyExc_AttributeError => attribute_error, + PyExc_BufferError => buffer_error, + PyExc_EOFError => eof_error, + PyExc_ImportError => import_error, + PyExc_ModuleNotFoundError => module_not_found_error, + PyExc_MemoryError => memory_error, + PyExc_NameError => name_error, + PyExc_UnboundLocalError => unbound_local_error, + PyExc_OSError => os_error, + PyExc_BlockingIOError => blocking_io_error, + PyExc_ChildProcessError => child_process_error, + PyExc_ConnectionError => connection_error, + PyExc_BrokenPipeError => broken_pipe_error, + PyExc_ConnectionAbortedError => connection_aborted_error, + PyExc_ConnectionRefusedError => connection_refused_error, + PyExc_ConnectionResetError => connection_reset_error, + PyExc_FileExistsError => file_exists_error, + PyExc_FileNotFoundError => file_not_found_error, + PyExc_InterruptedError => interrupted_error, + PyExc_IsADirectoryError => is_a_directory_error, + PyExc_NotADirectoryError => not_a_directory_error, + PyExc_PermissionError => permission_error, + PyExc_ProcessLookupError => process_lookup_error, + PyExc_TimeoutError => timeout_error, + PyExc_ReferenceError => reference_error, + PyExc_RuntimeError => runtime_error, + PyExc_NotImplementedError => not_implemented_error, + PyExc_RecursionError => recursion_error, + PyExc_SyntaxError => syntax_error, + PyExc_IndentationError => indentation_error, + PyExc_TabError => tab_error, + PyExc_ValueError => value_error, + PyExc_UnicodeError => unicode_error, + PyExc_UnicodeDecodeError => unicode_decode_error, + PyExc_UnicodeEncodeError => unicode_encode_error, + PyExc_UnicodeTranslateError => unicode_translate_error, + PyExc_Warning => warning, + PyExc_DeprecationWarning => deprecation_warning, + PyExc_PendingDeprecationWarning => pending_deprecation_warning, + PyExc_RuntimeWarning => runtime_warning, + PyExc_SyntaxWarning => syntax_warning, + PyExc_UserWarning => user_warning, + PyExc_FutureWarning => future_warning, + PyExc_ImportWarning => import_warning, + PyExc_UnicodeWarning => unicode_warning, + PyExc_BytesWarning => bytes_warning, + PyExc_ResourceWarning => resource_warning, + PyExc_EncodingWarning => encoding_warning, +} #[unsafe(no_mangle)] pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject { diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 10804890fe6..cddebcbda2b 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -3,14 +3,11 @@ use crate::object::{ PyBaseObject_Type, PyBool_Type, PyComplex_Type, PyDict_Type, PyLong_Type, PyTuple_Type, PyType_Type, PyUnicode_Type, }; -use crate::pyerrors::{ - PyExc_AttributeError, PyExc_BaseException, PyExc_Exception, PyExc_IndexError, - PyExc_OverflowError, PyExc_RuntimeError, PyExc_SystemError, PyExc_TypeError, PyExc_ValueError, -}; +use crate::pyerrors::init_exception_statics; use crate::pystate::attach_vm_to_thread; use core::ffi::c_int; use rustpython_vm::vm::thread::ThreadedVirtualMachine; -use rustpython_vm::{AsObject, Context, Interpreter}; +use rustpython_vm::{Context, Interpreter}; use std::sync::{Once, OnceLock, mpsc}; static VM_REQUEST_TX: OnceLock>> = @@ -51,16 +48,7 @@ pub(crate) fn init_static_type_pointers() { PyDict_Type.write(types.dict_type); PyComplex_Type.write(types.complex_type); - let exc = &context.exceptions; - PyExc_BaseException.write(exc.base_exception_type.as_object().as_raw().cast_mut()); - PyExc_Exception.write(exc.exception_type.as_object().as_raw().cast_mut()); - PyExc_SystemError.write(exc.system_error.as_object().as_raw().cast_mut()); - PyExc_TypeError.write(exc.type_error.as_object().as_raw().cast_mut()); - PyExc_OverflowError.write(exc.overflow_error.as_object().as_raw().cast_mut()); - PyExc_IndexError.write(exc.index_error.as_object().as_raw().cast_mut()); - PyExc_AttributeError.write(exc.attribute_error.as_object().as_raw().cast_mut()); - PyExc_RuntimeError.write(exc.runtime_error.as_object().as_raw().cast_mut()); - PyExc_ValueError.write(exc.value_error.as_object().as_raw().cast_mut()); + init_exception_statics(&context.exceptions); }; } From 661e7bd2698082701282d695cdd9f8b677ac67db Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 16 Apr 2026 16:00:51 +0200 Subject: [PATCH 067/135] Implement `PyObject_Vectorcall` --- crates/capi/src/abstract_.rs | 46 ++++++++++++++++++++++++++---------- crates/capi/src/util.rs | 8 +++++++ 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index 3ccc043eedc..3ce917538a6 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -2,7 +2,7 @@ use crate::with_vm; use alloc::slice; use core::ffi::c_int; use rustpython_vm::builtins::{PyDict, PyStr, PyTuple}; -use rustpython_vm::{PyObject, PyObjectRef, PyResult}; +use rustpython_vm::{AsObject, PyObject, PyObjectRef}; const PY_VECTORCALL_ARGUMENTS_OFFSET: usize = 1usize << (usize::BITS as usize - 1); @@ -44,24 +44,17 @@ pub unsafe extern "C" fn PyObject_CallMethodObjArgs( } #[unsafe(no_mangle)] -pub extern "C" fn PyObject_VectorcallMethod( - name: *mut PyObject, +pub extern "C" fn PyObject_Vectorcall( + callable: *mut PyObject, args: *const *mut PyObject, nargsf: usize, kwnames: *mut PyObject, ) -> *mut PyObject { - with_vm::(|vm| { + with_vm(|vm| { let args_len = nargsf & !PY_VECTORCALL_ARGUMENTS_OFFSET; - let num_positional_args = args_len - 1; - - let (receiver, args) = unsafe { slice::from_raw_parts(args, args_len) } - .split_first() - .expect("PyObject_VectorcallMethod should always have at least one argument"); - - let method_name = unsafe { (&*name).try_downcast_ref::(vm)? }; - let callable = unsafe { (&**receiver).get_attr(method_name, vm)? }; + let num_positional_args = args_len; - let args = args + let args = unsafe { slice::from_raw_parts(args, args_len) } .iter() .map(|arg| unsafe { &**arg }.to_owned()) .collect::>(); @@ -73,10 +66,37 @@ pub extern "C" fn PyObject_VectorcallMethod( .transpose()? }; + let callable = unsafe { &*callable }; callable.vectorcall(args, num_positional_args, kwnames, vm) }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_VectorcallMethod( + name: *mut PyObject, + args: *const *mut PyObject, + nargsf: usize, + kwnames: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let args_len = nargsf & !PY_VECTORCALL_ARGUMENTS_OFFSET; + + let (receiver, args) = unsafe { slice::from_raw_parts(args, args_len) } + .split_first() + .expect("PyObject_VectorcallMethod should always have at least one argument"); + + let method_name = unsafe { (&*name).try_downcast_ref::(vm)? }; + let callable = unsafe { (&**receiver).get_attr(method_name, vm)? }; + + Ok(PyObject_Vectorcall( + callable.as_object().as_raw().cast_mut(), + args.as_ptr(), + nargsf - 1, + kwnames, + )) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn PyObject_GetItem(obj: *mut PyObject, key: *mut PyObject) -> *mut PyObject { with_vm(|vm| { diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 6fdcea9bc58..295c5958684 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -44,6 +44,14 @@ impl FfiResult<*mut PyObject> for PyObjectRef { } } +impl FfiResult for *mut PyObject { + const ERR_VALUE: *mut PyObject = core::ptr::null_mut(); + + fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject { + self + } +} + impl FfiResult<*mut PyObject> for *const PyObject { const ERR_VALUE: *mut PyObject = core::ptr::null_mut(); From 65c80c805e51d0b81a38f51907f3d4c59651ae0b Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 16 Apr 2026 16:48:37 +0200 Subject: [PATCH 068/135] Add `PyObject_GetAttrString` --- crates/capi/src/object.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index a4aecf0eebb..d60bd2e47f0 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -139,6 +139,22 @@ pub extern "C" fn PyObject_GetAttr(obj: *mut PyObject, name: *mut PyObject) -> * }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_GetAttrString( + obj: *mut PyObject, + attr_name: *const c_char, +) -> *mut PyObject { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let name = unsafe { + CStr::from_ptr(attr_name) + .to_str() + .expect("attribute name must be valid UTF-8") + }; + obj.get_attr(name, vm) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn PyObject_SetAttrString( obj: *mut PyObject, From d2c2c635521ebbd0e3a15d6e9389ecf3fbe471b1 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 17 Apr 2026 08:46:40 +0200 Subject: [PATCH 069/135] Use custom RustPython build of `PyO3` --- Cargo.lock | 10 +++++----- Cargo.toml | 4 ++-- crates/capi/Cargo.toml | 2 +- crates/capi/pyo3-rustpython.config | 2 +- crates/capi/src/object.rs | 26 -------------------------- crates/capi/src/pylifecycle.rs | 14 -------------- 6 files changed, 9 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e2ec1b229f..b487016f717 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2645,7 +2645,7 @@ dependencies = [ [[package]] name = "pyo3" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#e662975d241c56c44dfeab8651d3f79ec1b23bff" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#b675184d6e54ed0f31fa9c42a1c772f773481a4b" dependencies = [ "libc", "once_cell", @@ -2658,7 +2658,7 @@ dependencies = [ [[package]] name = "pyo3-build-config" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#e662975d241c56c44dfeab8651d3f79ec1b23bff" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#b675184d6e54ed0f31fa9c42a1c772f773481a4b" dependencies = [ "target-lexicon", ] @@ -2666,7 +2666,7 @@ dependencies = [ [[package]] name = "pyo3-ffi" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#e662975d241c56c44dfeab8651d3f79ec1b23bff" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#b675184d6e54ed0f31fa9c42a1c772f773481a4b" dependencies = [ "libc", "pyo3-build-config", @@ -2675,7 +2675,7 @@ dependencies = [ [[package]] name = "pyo3-macros" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#e662975d241c56c44dfeab8651d3f79ec1b23bff" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#b675184d6e54ed0f31fa9c42a1c772f773481a4b" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2686,7 +2686,7 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#e662975d241c56c44dfeab8651d3f79ec1b23bff" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#b675184d6e54ed0f31fa9c42a1c772f773481a4b" dependencies = [ "heck", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 61dae08d882..30fc4bbf085 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ rustyline = { workspace = true } [dev-dependencies] criterion = { workspace = true } -pyo3 = { git = "https://github.com/PyO3/pyo3", features = ["auto-initialize"] } +pyo3 = { git = "https://github.com/bschoenmaeckers/pyo3", branch = "RustPython", features = ["auto-initialize"] } rustpython-stdlib = { workspace = true } ruff_python_parser = { workspace = true } @@ -102,7 +102,7 @@ lto = "thin" [patch.crates-io] parking_lot_core = { git = "https://github.com/youknowone/parking_lot", branch = "rustpython" } -pyo3-ffi = { git = "https://github.com/PyO3/pyo3" } +pyo3-ffi = { git = "https://github.com/bschoenmaeckers/pyo3", branch = "RustPython" } # REDOX START, Uncomment when you want to compile/check with redoxer # REDOX END diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index e7d5bc6f7eb..744a3abb525 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -21,7 +21,7 @@ rustpython-vm = { workspace = true, features = ["threading", "compiler"]} nightly = [] [dev-dependencies] -pyo3 = { git = "https://github.com/PyO3/pyo3", features = ["auto-initialize", "abi3"] } +pyo3 = { git = "https://github.com/bschoenmaeckers/pyo3", branch = "RustPython", features = ["auto-initialize", "abi3"] } [lints] workspace = true diff --git a/crates/capi/pyo3-rustpython.config b/crates/capi/pyo3-rustpython.config index e039bd6d6bc..0108787f667 100644 --- a/crates/capi/pyo3-rustpython.config +++ b/crates/capi/pyo3-rustpython.config @@ -1,4 +1,4 @@ -implementation=CPython +implementation=RustPython version=3.14 shared=true abi3=true diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index d60bd2e47f0..c2ffb5687ac 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,6 +1,5 @@ use crate::{PyObject, with_vm}; use core::ffi::{CStr, c_char, c_int, c_uint, c_ulong, c_void}; -use core::mem::MaybeUninit; use core::ptr::NonNull; use rustpython_vm::builtins::{PyStr, PyType}; use rustpython_vm::{AsObject, Context, Py}; @@ -16,30 +15,6 @@ const PY_TPFLAGS_TYPE_SUBCLASS: c_ulong = 1 << 31; pub type PyTypeObject = Py; -#[unsafe(no_mangle)] -pub static mut PyType_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyBaseObject_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyLong_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyTuple_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyUnicode_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyBool_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyDict_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); - -#[unsafe(no_mangle)] -pub static mut PyComplex_Type: MaybeUninit<&'static PyTypeObject> = MaybeUninit::uninit(); - #[unsafe(no_mangle)] pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const PyTypeObject { // SAFETY: The caller must guarantee that `op` is a valid pointer to a `PyObject`. @@ -291,7 +266,6 @@ mod tests { } #[test] - #[ignore = "Instance checking on static type pointers is yet supported"] fn test_static_type_pointers() { Python::attach(|py| { assert!(py.None().bind(py).is_instance_of::()); diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index cddebcbda2b..29e9fd75463 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,8 +1,4 @@ use crate::log_stub; -use crate::object::{ - PyBaseObject_Type, PyBool_Type, PyComplex_Type, PyDict_Type, PyLong_Type, PyTuple_Type, - PyType_Type, PyUnicode_Type, -}; use crate::pyerrors::init_exception_statics; use crate::pystate::attach_vm_to_thread; use core::ffi::c_int; @@ -36,18 +32,8 @@ pub(crate) fn init_static_type_pointers() { "Python already initialized, we should not touch the static type pointers" ); let context = Context::genesis(); - let types = &context.types; unsafe { - PyType_Type.write(types.type_type); - PyBaseObject_Type.write(types.object_type); - PyLong_Type.write(types.int_type); - PyTuple_Type.write(types.tuple_type); - PyUnicode_Type.write(types.str_type); - PyBool_Type.write(types.bool_type); - PyDict_Type.write(types.dict_type); - PyComplex_Type.write(types.complex_type); - init_exception_statics(&context.exceptions); }; } From b4bc0d8f61455a9ab43dc3d485448eb72a288f1d Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 17 Apr 2026 10:04:17 +0200 Subject: [PATCH 070/135] Add basic support for new classes --- crates/capi/src/object.rs | 193 ++++++++++++++++++++++++++++++- crates/capi/src/util.rs | 8 ++ crates/vm/src/types/slot_defs.rs | 3 +- 3 files changed, 201 insertions(+), 3 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index c2ffb5687ac..25e33d09321 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,8 +1,10 @@ use crate::{PyObject, with_vm}; use core::ffi::{CStr, c_char, c_int, c_uint, c_ulong, c_void}; use core::ptr::NonNull; -use rustpython_vm::builtins::{PyStr, PyType}; -use rustpython_vm::{AsObject, Context, Py}; +use rustpython_vm::builtins::{PyDict, PyStr, PyTuple, PyType}; +use rustpython_vm::function::FuncArgs; +use rustpython_vm::types::{PyTypeFlags, PyTypeSlots, SlotAccessor}; +use rustpython_vm::{AsObject, Context, Py, PyObjectRef}; const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; const PY_TPFLAGS_LIST_SUBCLASS: c_ulong = 1 << 25; @@ -105,6 +107,168 @@ pub extern "C" fn PyType_IsSubtype(a: *const PyTypeObject, b: *const PyTypeObjec }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyType_GetSlot(ty: *const PyTypeObject, slot: c_int) -> *mut c_void { + with_vm(|_vm| -> Option<*mut c_void> { + let ty = unsafe { &*ty }; + let slot: u8 = slot + .try_into() + .expect("slot number out of range for SlotAccessor"); + let slot_accessor: SlotAccessor = slot + .try_into() + .expect("invalid slot number for SlotAccessor"); + + match slot_accessor { + SlotAccessor::TpNew => { + extern "C" fn newfunc_wrapper( + subtype: *mut PyTypeObject, + args: *mut PyObject, + kwargs: *mut PyObject, + ) -> *mut PyObject { + with_vm(|vm| { + let subtype = unsafe { &*subtype }; + let mut func_args = FuncArgs::default(); + + if let Some(args_obj) = unsafe { args.as_ref() } { + let tuple = args_obj.try_downcast_ref::(vm)?; + func_args + .args + .extend(tuple.iter().map(|arg| arg.to_owned())); + } + + if let Some(kwargs_obj) = unsafe { kwargs.as_ref() } { + let kwargs = kwargs_obj.try_downcast_ref::(vm)?; + for (key, value) in kwargs.items_vec() { + let key = key.try_downcast::(vm)?; + func_args + .kwargs + .insert(key.to_string_lossy().into_owned(), value); + } + } + + subtype + .slots + .new + .load() + .expect("tp_new slot function pointer is null")( + subtype.to_owned(), + func_args, + vm, + ) + }) + } + + if let Some(vtable) = ty.get_type_data::() { + vtable.new_func.map(|newfunc| newfunc as *mut c_void) + } else { + ty.slots.new.load().map(|_| newfunc_wrapper as *mut c_void) + } + } + _ => { + todo!("Slot {slot_accessor:?} for {ty:?} is not yet implemented in PyType_GetSlot") + } + } + }) +} + +#[repr(C)] +pub struct PyType_Slot { + slot: c_int, + pfunc: *mut c_void, +} + +#[repr(C)] +pub struct PyType_Spec { + name: *const c_char, + basicsize: c_int, + itemsize: c_int, + flags: c_uint, + slots: *mut PyType_Slot, +} + +#[derive(Default)] +struct TypeVTable { + new_func: Option, +} + +type newfunc = unsafe extern "C" fn( + ty: *mut PyTypeObject, + args: *mut PyObject, + kwargs: *mut PyObject, +) -> *mut PyObject; + +#[unsafe(no_mangle)] +pub extern "C" fn PyType_FromSpec(spec: *mut PyType_Spec) -> *mut PyObject { + with_vm(|vm| { + let spec = unsafe { &*spec }; + let name = unsafe { + CStr::from_ptr(spec.name) + .to_str() + .expect("type name must be valid UTF-8") + }; + let mut base = vm.ctx.types.object_type; + let mut slots = PyTypeSlots::heap_default(); + + slots.basicsize = spec.basicsize as _; + slots.itemsize = spec.itemsize as _; + slots.flags = PyTypeFlags::from_bits(spec.flags as u64).expect("invalid flags value"); + + let mut vtable = TypeVTable::default(); + let mut slot_ptr = spec.slots; + while let slot = unsafe { &*slot_ptr } + && slot.slot != 0 + { + let accessor = SlotAccessor::try_from(slot.slot as u8) + .expect("invalid slot number in PyType_Spec"); + + match accessor { + SlotAccessor::TpDealloc => { + slots.del.store(Some(|ty, _vm| { + todo!("tp_dealloc is not yet implemented in PyType_FromSpec for {ty:?}") + })); + } + SlotAccessor::TpBase => base = unsafe { &*slot.pfunc.cast::() }, + SlotAccessor::TpGetset => {} + SlotAccessor::TpMethods => {} + SlotAccessor::TpNew => { + vtable.new_func = Some(unsafe { core::mem::transmute(slot.pfunc) }); + slots.new.store(Some(|ty, args, vm| { + let new_func = ty.get_type_data::().unwrap().new_func.unwrap(); + let kwargs = vm.ctx.new_dict(); + for (name, value) in &args.kwargs { + kwargs.set_item(&*vm.ctx.new_str(name.clone()), value.clone(), vm)?; + } + let args = vm.ctx.new_tuple(args.args); + let result = unsafe { + new_func( + (&*ty) as *const _ as *mut _, + args.as_object().as_raw().cast_mut(), + kwargs.as_object().as_raw().cast_mut(), + ) + }; + + unsafe { Ok(PyObjectRef::from_raw(NonNull::new(result).unwrap())) } + })); + } + SlotAccessor::TpDoc => {} + _ => todo!("Slot {accessor:?} is not yet supported in PyType_FromSpec"), + } + + slot_ptr = unsafe { slot_ptr.add(1) }; + } + + let class = vm.ctx.new_class(None, name, base.to_owned(), slots); + class.init_type_data(vtable).unwrap(); + class + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyType_Freeze(_ty: *mut PyTypeObject) -> c_int { + // TODO: Implement immutable type freezing semantics. + 0 +} + #[unsafe(no_mangle)] pub extern "C" fn PyObject_GetAttr(obj: *mut PyObject, name: *mut PyObject) -> *mut PyObject { with_vm(|vm| { @@ -323,4 +487,29 @@ mod tests { assert!(dict.get_item("foo").is_ok()); }) } + + #[test] + fn test_rust_class() { + #[pyclass] + struct MyClass { + #[pyo3(get)] + num: i32, + } + + #[pymethods] + impl MyClass { + #[new] + fn new(value: i32) -> Self { + MyClass { num: value } + } + + fn method1(&self) -> PyResult { + Ok(10) + } + } + + Python::attach(|py| { + let obj = Bound::new(py, MyClass { num: 3 }).unwrap(); + }); + } } diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 295c5958684..9eabd7842c7 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -118,6 +118,14 @@ impl FfiResult<()> for PyResult { } } +impl FfiResult<*mut c_void> for Option<*mut c_void> { + const ERR_VALUE: *mut c_void = core::ptr::null_mut(); + + fn into_output(self, vm: &VirtualMachine) -> *mut c_void { + self.map_or_else(|| Self::ERR_VALUE, |obj| obj.into_output(vm)) + } +} + impl FfiResult<*mut PyObject> for Option where T: FfiResult<*mut PyObject>, diff --git a/crates/vm/src/types/slot_defs.rs b/crates/vm/src/types/slot_defs.rs index efd1a8913f5..e845e9413e4 100644 --- a/crates/vm/src/types/slot_defs.rs +++ b/crates/vm/src/types/slot_defs.rs @@ -2,6 +2,7 @@ //! //! This module provides a centralized array of all slot definitions, +use num_enum::TryFromPrimitive; use super::{PyComparisonOp, PyTypeSlots}; use crate::builtins::descriptor::SlotFunc; @@ -66,7 +67,7 @@ pub struct SlotDef { /// /// Values match CPython's Py_* slot IDs from typeslots.h. /// Unused slots are included for value reservation. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] #[repr(u8)] pub enum SlotAccessor { // Buffer protocol (1-2) - Reserved, not used in RustPython From 2dc9a828b249a0d4cbe2f1e4f10afc4385da799e Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 17 Apr 2026 12:30:54 +0200 Subject: [PATCH 071/135] Add support for class attributes --- crates/capi/src/object.rs | 82 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 25e33d09321..fbc936ee35c 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,10 +1,11 @@ use crate::{PyObject, with_vm}; use core::ffi::{CStr, c_char, c_int, c_uint, c_ulong, c_void}; use core::ptr::NonNull; -use rustpython_vm::builtins::{PyDict, PyStr, PyTuple, PyType}; +use rustpython_vm::builtins::{PyDict, PyGetSet, PyStr, PyTuple, PyType}; +use rustpython_vm::convert::IntoObject; use rustpython_vm::function::FuncArgs; use rustpython_vm::types::{PyTypeFlags, PyTypeSlots, SlotAccessor}; -use rustpython_vm::{AsObject, Context, Py, PyObjectRef}; +use rustpython_vm::{AsObject, Context, Py, PyObjectRef, PyRef, PyResult, VirtualMachine}; const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; const PY_TPFLAGS_LIST_SUBCLASS: c_ulong = 1 << 25; @@ -186,6 +187,15 @@ pub struct PyType_Spec { slots: *mut PyType_Slot, } +#[repr(C)] +pub struct PyGetSetDef { + name: *const c_char, + get: extern "C" fn(*mut PyObject, usize) -> *mut PyObject, + set: Option c_int>, + doc: *const c_char, + closure: usize, +} + #[derive(Default)] struct TypeVTable { new_func: Option, @@ -201,7 +211,7 @@ type newfunc = unsafe extern "C" fn( pub extern "C" fn PyType_FromSpec(spec: *mut PyType_Spec) -> *mut PyObject { with_vm(|vm| { let spec = unsafe { &*spec }; - let name = unsafe { + let class_name = unsafe { CStr::from_ptr(spec.name) .to_str() .expect("type name must be valid UTF-8") @@ -213,6 +223,7 @@ pub extern "C" fn PyType_FromSpec(spec: *mut PyType_Spec) -> *mut PyObject { slots.itemsize = spec.itemsize as _; slots.flags = PyTypeFlags::from_bits(spec.flags as u64).expect("invalid flags value"); + let mut attributes: &[PyGetSetDef] = &[]; let mut vtable = TypeVTable::default(); let mut slot_ptr = spec.slots; while let slot = unsafe { &*slot_ptr } @@ -228,7 +239,16 @@ pub extern "C" fn PyType_FromSpec(spec: *mut PyType_Spec) -> *mut PyObject { })); } SlotAccessor::TpBase => base = unsafe { &*slot.pfunc.cast::() }, - SlotAccessor::TpGetset => {} + SlotAccessor::TpGetset => { + let start = slot.pfunc.cast::(); + let mut end = start; + while unsafe { !(*end).name.is_null() } { + end = unsafe { end.add(1) } + } + attributes = unsafe { + core::slice::from_raw_parts(start, end.offset_from(start) as usize) + }; + } SlotAccessor::TpMethods => {} SlotAccessor::TpNew => { vtable.new_func = Some(unsafe { core::mem::transmute(slot.pfunc) }); @@ -250,15 +270,60 @@ pub extern "C" fn PyType_FromSpec(spec: *mut PyType_Spec) -> *mut PyObject { unsafe { Ok(PyObjectRef::from_raw(NonNull::new(result).unwrap())) } })); } - SlotAccessor::TpDoc => {} + SlotAccessor::TpDoc => { + let doc = unsafe { + CStr::from_ptr(slot.pfunc.cast::()) + .to_str() + .expect("tp_doc must be a valid UTF-8 string") + }; + slots.doc = Some(doc); + } _ => todo!("Slot {accessor:?} is not yet supported in PyType_FromSpec"), } slot_ptr = unsafe { slot_ptr.add(1) }; } - let class = vm.ctx.new_class(None, name, base.to_owned(), slots); + let class = vm.ctx.new_class(None, class_name, base.to_owned(), slots); class.init_type_data(vtable).unwrap(); + for attribute in attributes { + let name = unsafe { + CStr::from_ptr(attribute.name) + .to_str() + .expect("attribute name must be valid UTF-8") + }; + let closure = attribute.closure; + let getter = attribute.get; + let getset = if let Some(setter) = attribute.set { + todo!(); + unsafe { + vm.ctx.new_getset( + name, + &class, + |obj: PyObjectRef, vm: &VirtualMachine| {}, + |obj: PyObjectRef, value: PyObjectRef, vm: &VirtualMachine| {}, + ) + } + } else { + let class = unsafe { &*((&*class) as *const _) }; + vm.ctx.new_readonly_getset( + name, + &class, + move |obj: PyObjectRef, vm: &VirtualMachine| { + let result = getter(obj.as_raw().cast_mut(), closure); + unsafe { + PyObjectRef::from_raw( + NonNull::new(result).expect("TODO handle error from c function"), + ) + } + }, + ) + }; + class + .attributes + .write() + .insert(vm.ctx.intern_str(name), getset.into_object()); + } class }) } @@ -510,6 +575,11 @@ mod tests { Python::attach(|py| { let obj = Bound::new(py, MyClass { num: 3 }).unwrap(); + + let globals = PyDict::new(py); + globals.set_item("instance", obj).unwrap(); + py.run(c"assert instance.num == 3", Some(&globals), None) + .unwrap(); }); } } From 3fad7e44d81919914a93cc3df77f48710a3d4a94 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 20 Apr 2026 10:03:59 +0200 Subject: [PATCH 072/135] Add basic support for PyO3 class methods --- crates/capi/src/methodobject.rs | 90 ++++++++++++++++++++------------- crates/capi/src/object.rs | 47 ++++++++++++++--- 2 files changed, 95 insertions(+), 42 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index ccbc2fa922e..c518c1b652a 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -4,8 +4,8 @@ use crate::pystate::with_vm; use bitflags::bitflags_match; use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; -use rustpython_vm::function::{FuncArgs, PyMethodFlags}; -use rustpython_vm::{AsObject, PyObjectRef, PyResult, VirtualMachine}; +use rustpython_vm::function::{FuncArgs, HeapMethodDef, PyMethodFlags}; +use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; type PyCFunction = unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject; type PyCFunctionWithKeywords = unsafe extern "C" fn( @@ -23,10 +23,57 @@ pub union PyMethodPointer { #[repr(C)] pub struct PyMethodDef { - ml_name: *const c_char, - ml_meth: PyMethodPointer, - ml_flags: c_int, - ml_doc: *const c_char, + pub(crate) ml_name: *const c_char, + pub(crate) ml_meth: PyMethodPointer, + pub(crate) ml_flags: c_int, + pub(crate) ml_doc: *const c_char, +} + +pub(crate) fn build_method_def( + vm: &VirtualMachine, + ml: &PyMethodDef, + slf: Option, +) -> PyRef { + let name = unsafe { CStr::from_ptr(ml.ml_name) } + .to_str() + .expect("Method name was not valid UTF-8"); + + let doc = NonNull::new(ml.ml_doc.cast_mut()).map(|doc| { + unsafe { CStr::from_ptr(doc.as_ptr()) } + .to_str() + .expect("Method doc was not valid UTF-8") + }); + + let flags = + PyMethodFlags::from_bits(ml.ml_flags as u32).expect("PyMethodDef contains unknown flags"); + + assert!( + !flags.intersects( + PyMethodFlags::METHOD + | PyMethodFlags::FASTCALL + | PyMethodFlags::CLASS + | PyMethodFlags::STATIC + ), + "These flags are not yet supported: {:?}", + flags + ); + + let method = ml.ml_meth; + let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { + if slf.is_some() { + c_function_wrapper(vm, slf.as_ref(), args, method, flags) + } else { + let slf = if args.args.len() > 0 { + Some(args.args.remove(0)) + } else { + None + }; + + c_function_wrapper(vm, slf.as_ref(), args, method, flags) + } + }; + + vm.ctx.new_method_def(name, callable, flags, doc) } fn c_function_wrapper( @@ -45,6 +92,7 @@ fn c_function_wrapper( let ret_ptr = bitflags_match!(flags, { PyMethodFlags::NOARGS => { + debug_assert!(arg_tuple.is_empty(), "Expected no arguments, but got some"); let f = unsafe { method.function }; unsafe { Ok(f(slf_ptr, arg_tuple_ptr)) } }, @@ -80,36 +128,8 @@ pub extern "C" fn PyCMethod_New( ) -> *mut PyObject { with_vm(|vm| -> PyResult { let ml = unsafe { &*ml }; - let name = unsafe { CStr::from_ptr(ml.ml_name) } - .to_str() - .expect("Method name was not valid UTF-8"); - - let doc = unsafe { CStr::from_ptr(ml.ml_doc) } - .to_str() - .expect("Method doc was not valid UTF-8"); - - let flags = PyMethodFlags::from_bits(ml.ml_flags as u32) - .expect("PyMethodDef contains unknown flags"); - - assert!( - !flags.intersects( - PyMethodFlags::METHOD - | PyMethodFlags::FASTCALL - | PyMethodFlags::CLASS - | PyMethodFlags::STATIC - ), - "These flags are not yet supported: {:?}", - flags - ); - - let method = ml.ml_meth; let slf = NonNull::new(slf).map(|ptr| unsafe { ptr.as_ref().to_owned() }); - let callable = move |args: FuncArgs, vm: &VirtualMachine| { - c_function_wrapper(vm, slf.as_ref(), args, method, flags) - }; - - let method = vm.ctx.new_method_def(name, callable, flags, Some(doc)); - Ok(method.build_function(vm).into()) + Ok(build_method_def(vm, ml, slf).build_function(vm).into()) }) } diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index fbc936ee35c..0872bf8b786 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,11 +1,12 @@ +use crate::methodobject::{PyMethodDef, build_method_def}; use crate::{PyObject, with_vm}; use core::ffi::{CStr, c_char, c_int, c_uint, c_ulong, c_void}; use core::ptr::NonNull; -use rustpython_vm::builtins::{PyDict, PyGetSet, PyStr, PyTuple, PyType}; +use rustpython_vm::builtins::{PyDict, PyStr, PyTuple, PyType}; use rustpython_vm::convert::IntoObject; use rustpython_vm::function::FuncArgs; use rustpython_vm::types::{PyTypeFlags, PyTypeSlots, SlotAccessor}; -use rustpython_vm::{AsObject, Context, Py, PyObjectRef, PyRef, PyResult, VirtualMachine}; +use rustpython_vm::{AsObject, Context, Py, PyObjectRef, PyResult, VirtualMachine}; const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; const PY_TPFLAGS_LIST_SUBCLASS: c_ulong = 1 << 25; @@ -209,7 +210,7 @@ type newfunc = unsafe extern "C" fn( #[unsafe(no_mangle)] pub extern "C" fn PyType_FromSpec(spec: *mut PyType_Spec) -> *mut PyObject { - with_vm(|vm| { + with_vm(|vm| -> PyResult { let spec = unsafe { &*spec }; let class_name = unsafe { CStr::from_ptr(spec.name) @@ -224,6 +225,7 @@ pub extern "C" fn PyType_FromSpec(spec: *mut PyType_Spec) -> *mut PyObject { slots.flags = PyTypeFlags::from_bits(spec.flags as u64).expect("invalid flags value"); let mut attributes: &[PyGetSetDef] = &[]; + let mut methods: &[PyMethodDef] = &[]; let mut vtable = TypeVTable::default(); let mut slot_ptr = spec.slots; while let slot = unsafe { &*slot_ptr } @@ -249,7 +251,16 @@ pub extern "C" fn PyType_FromSpec(spec: *mut PyType_Spec) -> *mut PyObject { core::slice::from_raw_parts(start, end.offset_from(start) as usize) }; } - SlotAccessor::TpMethods => {} + SlotAccessor::TpMethods => { + let start = slot.pfunc.cast::(); + let mut end = start; + while unsafe { !(*end).ml_name.is_null() } { + end = unsafe { end.add(1) } + } + methods = unsafe { + core::slice::from_raw_parts(start, end.offset_from(start) as usize) + }; + } SlotAccessor::TpNew => { vtable.new_func = Some(unsafe { core::mem::transmute(slot.pfunc) }); slots.new.store(Some(|ty, args, vm| { @@ -324,7 +335,20 @@ pub extern "C" fn PyType_FromSpec(spec: *mut PyType_Spec) -> *mut PyObject { .write() .insert(vm.ctx.intern_str(name), getset.into_object()); } - class + for method in methods { + let class_static = unsafe { &*((&*class) as *const _) }; + let name = unsafe { + CStr::from_ptr(method.ml_name) + .to_str() + .expect("method name must be valid UTF-8") + }; + let method = build_method_def(vm, method, None).build_method(class_static, vm); + class + .attributes + .write() + .insert(vm.ctx.intern_str(name), method.into()); + } + Ok(class.into()) }) } @@ -569,7 +593,7 @@ mod tests { } fn method1(&self) -> PyResult { - Ok(10) + Ok(self.num + 10) } } @@ -577,9 +601,18 @@ mod tests { let obj = Bound::new(py, MyClass { num: 3 }).unwrap(); let globals = PyDict::new(py); - globals.set_item("instance", obj).unwrap(); + globals.set_item("instance", &obj).unwrap(); py.run(c"assert instance.num == 3", Some(&globals), None) .unwrap(); + + #[cfg(feature = "nightly")] + assert_eq!( + obj.call_method0("method1") + .unwrap() + .extract::() + .unwrap(), + 13 + ); }); } } From 842f54dc4ad69c7712d99b23baae2fe3c9db13b9 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 20 Apr 2026 14:49:18 +0200 Subject: [PATCH 073/135] Add `PyTuple_FromArray` & `PyTuple_Pack` --- Cargo.lock | 10 ++++----- crates/capi/src/tupleobject.rs | 41 ++++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 61fd41bd105..529bbbd7657 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2645,7 +2645,7 @@ dependencies = [ [[package]] name = "pyo3" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#b675184d6e54ed0f31fa9c42a1c772f773481a4b" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#09a96fd9c2503cab2f3c1a33e5fd9c7828d9d3dd" dependencies = [ "libc", "once_cell", @@ -2658,7 +2658,7 @@ dependencies = [ [[package]] name = "pyo3-build-config" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#b675184d6e54ed0f31fa9c42a1c772f773481a4b" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#09a96fd9c2503cab2f3c1a33e5fd9c7828d9d3dd" dependencies = [ "target-lexicon", ] @@ -2666,7 +2666,7 @@ dependencies = [ [[package]] name = "pyo3-ffi" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#b675184d6e54ed0f31fa9c42a1c772f773481a4b" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#09a96fd9c2503cab2f3c1a33e5fd9c7828d9d3dd" dependencies = [ "libc", "pyo3-build-config", @@ -2675,7 +2675,7 @@ dependencies = [ [[package]] name = "pyo3-macros" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#b675184d6e54ed0f31fa9c42a1c772f773481a4b" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#09a96fd9c2503cab2f3c1a33e5fd9c7828d9d3dd" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2686,7 +2686,7 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#b675184d6e54ed0f31fa9c42a1c772f773481a4b" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#09a96fd9c2503cab2f3c1a33e5fd9c7828d9d3dd" dependencies = [ "heck", "proc-macro2", diff --git a/crates/capi/src/tupleobject.rs b/crates/capi/src/tupleobject.rs index e34f457b0fd..eb19c1dfc45 100644 --- a/crates/capi/src/tupleobject.rs +++ b/crates/capi/src/tupleobject.rs @@ -10,7 +10,34 @@ pub extern "C" fn PyTuple_New(len: isize) -> *mut PyObject { return Ok(vm.ctx.empty_tuple.to_owned()); } - Err(vm.new_not_implemented_error("PyTuple_New is not yet implemented")) + Err(vm.new_not_implemented_error("PyTuple_New for non zero sized tuples is not supported, use PyTuple_Pack or PyTuple_FromArray instead")) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyTuple_FromArray(array: *const *mut PyObject, size: isize) -> *mut PyObject { + with_vm(|vm| { + let slice = unsafe { std::slice::from_raw_parts(array, size as usize) }; + let elements = slice + .iter() + .map(|ptr| unsafe { &**ptr }.to_owned()) + .collect::>(); + vm.new_tuple(elements) + }) +} + +#[unsafe(no_mangle)] +#[cfg(feature = "nightly")] +pub unsafe extern "C" fn PyTuple_Pack(len: isize, mut args: ...) -> *mut PyObject { + with_vm(|vm| { + let mut items = vec![]; + + for _ in 0..len { + let item = unsafe { &*args.arg::<*mut PyObject>() }; + items.push(item.to_owned()); + } + + vm.new_tuple(items) }) } @@ -20,9 +47,7 @@ pub extern "C" fn PyTuple_SetItem( _pos: isize, _value: *mut PyObject, ) -> *mut PyObject { - with_vm::(|vm| { - Err(vm.new_not_implemented_error("PyTuple_SetItem is not yet implemented")) - }) + with_vm::(|vm| Err(vm.new_not_implemented_error("Tuple objects are immutable"))) } #[unsafe(no_mangle)] @@ -60,4 +85,12 @@ mod tests { assert_eq!(tuple.len(), 0); }) } + + #[test] + fn test_tuple_into_python() { + Python::attach(|py| { + let tuple = (1, 2, 3).into_pyobject(py).unwrap(); + assert_eq!(tuple.len(), 3); + }) + } } From 2e03e5d0ff25b534dfeecf3bf1c4d7ab58a1fee2 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 20 Apr 2026 16:38:26 +0200 Subject: [PATCH 074/135] Add `PyFloat` support --- crates/capi/src/floatobject.rs | 37 ++++++++++++++++++++++++++++++++++ crates/capi/src/lib.rs | 1 + 2 files changed, 38 insertions(+) create mode 100644 crates/capi/src/floatobject.rs diff --git a/crates/capi/src/floatobject.rs b/crates/capi/src/floatobject.rs new file mode 100644 index 00000000000..bc4f8e54e77 --- /dev/null +++ b/crates/capi/src/floatobject.rs @@ -0,0 +1,37 @@ +use crate::{PyObject, with_vm}; +use core::ffi::c_double; +use rustpython_vm::builtins::PyFloat; + +#[unsafe(no_mangle)] +pub extern "C" fn PyFloat_FromDouble(value: c_double) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_float(value)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyFloat_AsDouble(obj: *mut PyObject) -> c_double { + with_vm(|vm| { + let obj_ref = unsafe { &*obj }; + let float_obj = obj_ref + .to_owned() + .try_downcast::(vm) + .or_else(|_| obj_ref.try_float(vm))?; + + Ok(float_obj.to_f64()) + }) +} + +#[cfg(test)] +mod tests { + use core::f64::consts::PI; + use pyo3::prelude::*; + use pyo3::types::PyFloat; + + #[test] + fn test_py_float() { + Python::attach(|py| { + let pi = PyFloat::new(py, PI); + assert!(pi.is_instance_of::()); + assert_eq!(pi.extract::().unwrap(), 123.0); + }) + } +} diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index eadbefae472..68c1a5ad28a 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -10,6 +10,7 @@ pub(crate) mod capsule; pub(crate) mod ceval; pub(crate) mod complexobject; pub(crate) mod dictobject; +pub(crate) mod floatobject; pub(crate) mod import; pub(crate) mod listobject; pub(crate) mod longobject; From 535861bb4af2f4b09f035c9ae796f71c1ac89534 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 20 Apr 2026 16:40:47 +0200 Subject: [PATCH 075/135] Add `Py_NewRef` --- crates/capi/src/refcount.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/capi/src/refcount.rs b/crates/capi/src/refcount.rs index fc73f78b616..700561143e5 100644 --- a/crates/capi/src/refcount.rs +++ b/crates/capi/src/refcount.rs @@ -16,6 +16,11 @@ pub extern "C" fn _Py_IncRef(op: *mut PyObject) { core::mem::forget(unsafe { (*op).to_owned() }); } +#[unsafe(no_mangle)] +pub extern "C" fn Py_NewRef(op: *mut PyObject) -> *mut PyObject { + with_vm(|_vm| unsafe { (*op).to_owned() }) +} + #[unsafe(no_mangle)] pub extern "C" fn Py_REFCNT(op: *mut PyObject) -> isize { with_vm(|_vm| unsafe { &*op }.strong_count()) From 1ff142baab62cbc8b1838cec434a8b4766baaae1 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 20 Apr 2026 16:57:58 +0200 Subject: [PATCH 076/135] Add `PyLong_AsUnsignedLongLong` --- crates/capi/src/longobject.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index 62a32ef13a2..a8e7341fed1 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -50,17 +50,38 @@ pub extern "C" fn PyLong_AsLong(obj: *mut PyObject) -> c_long { }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_AsUnsignedLongLong(obj: *mut PyObject) -> c_longlong { + with_vm::, _>(|vm| { + unsafe { &*obj } + .to_owned() + .try_downcast::(vm)? + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C long long")) + }) +} + #[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyInt; #[test] - fn test_py_int() { + fn test_py_int_u32() { Python::attach(|py| { let number = PyInt::new(py, 123); assert!(number.is_instance_of::()); assert_eq!(number.extract::().unwrap(), 123); }) } + + #[test] + fn test_py_int_u64() { + Python::attach(|py| { + let number = PyInt::new(py, 123u64); + assert!(number.is_instance_of::()); + assert_eq!(number.extract::().unwrap(), 123); + }) + } } From f2e824ac90c99fcecf1951b38ee66578b61c1a9a Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 20 Apr 2026 17:02:55 +0200 Subject: [PATCH 077/135] Add `PyNumber_Index` --- Cargo.lock | 10 +++++----- crates/capi/src/abstract_.rs | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 529bbbd7657..92234d8c4ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2645,7 +2645,7 @@ dependencies = [ [[package]] name = "pyo3" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#09a96fd9c2503cab2f3c1a33e5fd9c7828d9d3dd" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#982e68a57411c171747379d100e86e9562e4810d" dependencies = [ "libc", "once_cell", @@ -2658,7 +2658,7 @@ dependencies = [ [[package]] name = "pyo3-build-config" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#09a96fd9c2503cab2f3c1a33e5fd9c7828d9d3dd" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#982e68a57411c171747379d100e86e9562e4810d" dependencies = [ "target-lexicon", ] @@ -2666,7 +2666,7 @@ dependencies = [ [[package]] name = "pyo3-ffi" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#09a96fd9c2503cab2f3c1a33e5fd9c7828d9d3dd" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#982e68a57411c171747379d100e86e9562e4810d" dependencies = [ "libc", "pyo3-build-config", @@ -2675,7 +2675,7 @@ dependencies = [ [[package]] name = "pyo3-macros" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#09a96fd9c2503cab2f3c1a33e5fd9c7828d9d3dd" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#982e68a57411c171747379d100e86e9562e4810d" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2686,7 +2686,7 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#09a96fd9c2503cab2f3c1a33e5fd9c7828d9d3dd" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#982e68a57411c171747379d100e86e9562e4810d" dependencies = [ "heck", "proc-macro2", diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index 3ce917538a6..d7828b3ce03 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -148,6 +148,11 @@ pub extern "C" fn PySequence_Contains(obj: *mut PyObject, value: *mut PyObject) }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyNumber_Index(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| unsafe { &*obj }.try_index(vm)) +} + #[cfg(test)] mod tests { use pyo3::prelude::*; From 59a5fa69f906d95b7f1ee124d8c0467b33a2fe65 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 20 Apr 2026 17:30:59 +0200 Subject: [PATCH 078/135] Fix float test --- Cargo.lock | 10 +++++----- crates/capi/src/floatobject.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92234d8c4ab..c4ecd7b4a38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2645,7 +2645,7 @@ dependencies = [ [[package]] name = "pyo3" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#982e68a57411c171747379d100e86e9562e4810d" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#99eedb49255e389ecbc77257c1d711255b513081" dependencies = [ "libc", "once_cell", @@ -2658,7 +2658,7 @@ dependencies = [ [[package]] name = "pyo3-build-config" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#982e68a57411c171747379d100e86e9562e4810d" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#99eedb49255e389ecbc77257c1d711255b513081" dependencies = [ "target-lexicon", ] @@ -2666,7 +2666,7 @@ dependencies = [ [[package]] name = "pyo3-ffi" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#982e68a57411c171747379d100e86e9562e4810d" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#99eedb49255e389ecbc77257c1d711255b513081" dependencies = [ "libc", "pyo3-build-config", @@ -2675,7 +2675,7 @@ dependencies = [ [[package]] name = "pyo3-macros" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#982e68a57411c171747379d100e86e9562e4810d" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#99eedb49255e389ecbc77257c1d711255b513081" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2686,7 +2686,7 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#982e68a57411c171747379d100e86e9562e4810d" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#99eedb49255e389ecbc77257c1d711255b513081" dependencies = [ "heck", "proc-macro2", diff --git a/crates/capi/src/floatobject.rs b/crates/capi/src/floatobject.rs index bc4f8e54e77..5c0284408f6 100644 --- a/crates/capi/src/floatobject.rs +++ b/crates/capi/src/floatobject.rs @@ -31,7 +31,7 @@ mod tests { Python::attach(|py| { let pi = PyFloat::new(py, PI); assert!(pi.is_instance_of::()); - assert_eq!(pi.extract::().unwrap(), 123.0); + assert_eq!(pi.extract::().unwrap(), PI); }) } } From b8a364626afd5c08155f38d7e54576a89ffc6a94 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 22 Apr 2026 12:40:25 +0200 Subject: [PATCH 079/135] Implement `PyTraceBack_Print` --- crates/capi/src/traceback.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/capi/src/traceback.rs b/crates/capi/src/traceback.rs index c73c952d5e7..a0741f99be0 100644 --- a/crates/capi/src/traceback.rs +++ b/crates/capi/src/traceback.rs @@ -1,9 +1,21 @@ -use core::ffi::c_int; - use crate::PyObject; +use crate::pystate::with_vm; +use core::ffi::c_int; +use rustpython_vm::function::{FuncArgs, KwArgs}; #[unsafe(no_mangle)] -pub extern "C" fn PyTraceBack_Print(_tb: *mut PyObject, _file: *mut PyObject) -> c_int { - crate::log_stub("PyTraceBack_Print"); - -1 +pub extern "C" fn PyTraceBack_Print(tb: *mut PyObject, file: *mut PyObject) -> c_int { + with_vm(|vm| { + let tb = unsafe { &*tb }; + let file = unsafe { &*file }; + let tb_module = vm.import("traceback", 0)?; + let print_tb = tb_module.get_attr("print_tb", vm)?; + + let kwargs: KwArgs = [("file".to_string(), file.to_owned())] + .into_iter() + .collect(); + print_tb.call(FuncArgs::new(vec![tb.to_owned()], kwargs), vm)?; + + Ok(()) + }) } From 8d7b7d7bd88f23bab06f986dca6c4d894b402b8a Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 22 Apr 2026 14:26:18 +0200 Subject: [PATCH 080/135] Implement `PyUnicode_AsEncodedString` --- crates/capi/src/unicodeobject.rs | 34 +++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index 501460777c2..8e1ba55f006 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -1,6 +1,5 @@ use crate::{PyObject, with_vm}; -use core::ffi::{c_char, c_int}; -use core::ptr; +use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; use core::slice; use core::str; @@ -47,12 +46,33 @@ pub extern "C" fn PyUnicode_AsUTF8AndSize(obj: *mut PyObject, size: *mut isize) #[unsafe(no_mangle)] pub extern "C" fn PyUnicode_AsEncodedString( - _unicode: *mut PyObject, - _encoding: *const c_char, - _errors: *const c_char, + unicode: *mut PyObject, + encoding: *const c_char, + errors: *const c_char, ) -> *mut PyObject { - crate::log_stub("PyUnicode_AsEncodedString"); - ptr::null_mut() + with_vm(|vm| { + let unicode = unsafe { &*unicode } + .try_downcast_ref::(vm)? + .to_owned(); + let encoding = if encoding.is_null() { + "utf-8" + } else { + unsafe { CStr::from_ptr(encoding) } + .to_str() + .expect("encoding must be valid UTF-8") + }; + let errors = if errors.is_null() { + None + } else { + let errors = unsafe { CStr::from_ptr(errors) } + .to_str() + .expect("errors must be valid UTF-8"); + Some(vm.ctx.new_utf8_str(errors)) + }; + vm.state + .codec_registry + .encode_text(unicode, encoding, errors, vm) + }) } #[unsafe(no_mangle)] From c49725a1506e7b732a3434be4170cf3d6ec0d285 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 28 Apr 2026 13:31:07 +0200 Subject: [PATCH 081/135] Add `PyList_Append` --- crates/capi/src/listobject.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/capi/src/listobject.rs b/crates/capi/src/listobject.rs index 8f84c2e6cba..e30ac3f4d4b 100644 --- a/crates/capi/src/listobject.rs +++ b/crates/capi/src/listobject.rs @@ -52,6 +52,15 @@ pub extern "C" fn PyList_SetItem(list: *mut PyObject, index: isize, item: *mut P }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyList_Append(list: *mut PyObject, item: *mut PyObject) -> c_int { + with_vm(|vm| { + let list = unsafe { &*list }.try_downcast_ref::(vm)?; + let item = unsafe { &*item }.to_owned(); + Ok(list.borrow_vec_mut().push(item)) + }) +} + #[cfg(test)] mod tests { use pyo3::exceptions::PyIndexError; @@ -92,4 +101,15 @@ mod tests { ); }) } + + #[test] + fn test_list_append() { + Python::attach(|py| { + let list = PyList::empty(py); + assert_eq!(list.len(), 0); + list.append(1).unwrap(); + assert_eq!(list.len(), 1); + assert_eq!(list.get_item(0).unwrap().extract::().unwrap(), 1); + }) + } } From d969389892c77b27a87bd1584004df4d678b68f8 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 28 Apr 2026 16:24:50 +0200 Subject: [PATCH 082/135] Add type check functions --- Cargo.lock | 10 +++++----- crates/capi/src/object.rs | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0e25182188..5d6305214c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2695,7 +2695,7 @@ dependencies = [ [[package]] name = "pyo3" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#99eedb49255e389ecbc77257c1d711255b513081" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#68bfc26d86b9d3e9c0d4fcc6a43663774ede66d6" dependencies = [ "libc", "once_cell", @@ -2708,7 +2708,7 @@ dependencies = [ [[package]] name = "pyo3-build-config" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#99eedb49255e389ecbc77257c1d711255b513081" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#68bfc26d86b9d3e9c0d4fcc6a43663774ede66d6" dependencies = [ "target-lexicon", ] @@ -2716,7 +2716,7 @@ dependencies = [ [[package]] name = "pyo3-ffi" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#99eedb49255e389ecbc77257c1d711255b513081" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#68bfc26d86b9d3e9c0d4fcc6a43663774ede66d6" dependencies = [ "libc", "pyo3-build-config", @@ -2725,7 +2725,7 @@ dependencies = [ [[package]] name = "pyo3-macros" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#99eedb49255e389ecbc77257c1d711255b513081" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#68bfc26d86b9d3e9c0d4fcc6a43663774ede66d6" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2736,7 +2736,7 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#99eedb49255e389ecbc77257c1d711255b513081" +source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#68bfc26d86b9d3e9c0d4fcc6a43663774ede66d6" dependencies = [ "heck", "proc-macro2", diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 0872bf8b786..e7fdbd296f6 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -19,6 +19,25 @@ const PY_TPFLAGS_TYPE_SUBCLASS: c_ulong = 1 << 31; pub type PyTypeObject = Py; +macro_rules! define_py_check { + ($name:ident, $type_name:ident) => { + #[unsafe(no_mangle)] + pub extern "C" fn $name(obj: *mut PyObject) -> c_int { + with_vm(|vm| unsafe { (*obj).is_instance(vm.ctx.types.$type_name.as_object(), vm) }) + } + }; + (exact $name:ident, $type_name:ident) => { + #[unsafe(no_mangle)] + pub extern "C" fn $name(obj: *mut PyObject) -> c_int { + with_vm(|vm| unsafe { (*obj).class().is(vm.ctx.types.$type_name) }) + } + }; +} + +define_py_check!(PyFloat_Check, float_type); +define_py_check!(PyModule_Check, module_type); +define_py_check!(PyBool_Check, bool_type); + #[unsafe(no_mangle)] pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const PyTypeObject { // SAFETY: The caller must guarantee that `op` is a valid pointer to a `PyObject`. From d25d6c8493027b8212e58403d76d049fa23284ea Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 28 Apr 2026 18:20:08 +0200 Subject: [PATCH 083/135] Add `PyObject_Call` --- crates/capi/src/abstract_.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index d7828b3ce03..7c35df83735 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -2,10 +2,43 @@ use crate::with_vm; use alloc::slice; use core::ffi::c_int; use rustpython_vm::builtins::{PyDict, PyStr, PyTuple}; +use rustpython_vm::function::{FuncArgs, KwArgs}; use rustpython_vm::{AsObject, PyObject, PyObjectRef}; const PY_VECTORCALL_ARGUMENTS_OFFSET: usize = 1usize << (usize::BITS as usize - 1); +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_Call( + callable: *mut PyObject, + args: *mut PyObject, + kwargs: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let callable = unsafe { &*callable }; + let args = unsafe { &*args } + .try_downcast_ref::(vm)? + .iter() + .cloned() + .collect::>(); + + let kwargs: KwArgs = unsafe { kwargs.as_ref() } + .map(|kwargs| kwargs.try_downcast_ref::(vm)) + .transpose()? + .map_or_else( + || KwArgs::default(), + |kwargs| { + kwargs + .items_vec() + .iter() + .map(|(key, value)| todo!()) + .collect() + }, + ); + + callable.call_with_args(FuncArgs::new(args, kwargs), vm) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn PyObject_CallNoArgs(callable: *mut PyObject) -> *mut PyObject { with_vm(|vm| { From 5ea41e15e25847456f80a20bc143324400739370 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 28 Apr 2026 18:33:41 +0200 Subject: [PATCH 084/135] Add support for `FASTCALL`/`STATIC`/`CLASS` c functions --- crates/capi/src/methodobject.rs | 123 +++++++++++++++++++++----------- crates/capi/src/object.rs | 32 ++++++++- 2 files changed, 110 insertions(+), 45 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index c518c1b652a..99712bc6900 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -13,12 +13,19 @@ type PyCFunctionWithKeywords = unsafe extern "C" fn( args: *mut PyObject, kwargs: *mut PyObject, ) -> *mut PyObject; +type PyCFunctionFastWithKeywords = unsafe extern "C" fn( + slf: *mut PyObject, + args: *const *mut PyObject, + nargs: isize, + kwnames: *mut PyObject, +) -> *mut PyObject; #[repr(C)] #[derive(Copy, Clone)] pub union PyMethodPointer { function: PyCFunction, function_with_keywords: PyCFunctionWithKeywords, + function_fast_with_keywords: PyCFunctionFastWithKeywords, } #[repr(C)] @@ -48,29 +55,17 @@ pub(crate) fn build_method_def( PyMethodFlags::from_bits(ml.ml_flags as u32).expect("PyMethodDef contains unknown flags"); assert!( - !flags.intersects( - PyMethodFlags::METHOD - | PyMethodFlags::FASTCALL - | PyMethodFlags::CLASS - | PyMethodFlags::STATIC - ), + !flags.intersects(PyMethodFlags::METHOD), "These flags are not yet supported: {:?}", flags ); let method = ml.ml_meth; let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - if slf.is_some() { - c_function_wrapper(vm, slf.as_ref(), args, method, flags) - } else { - let slf = if args.args.len() > 0 { - Some(args.args.remove(0)) - } else { - None - }; - - c_function_wrapper(vm, slf.as_ref(), args, method, flags) + if let Some(slf) = slf.as_ref() { + args.args.insert(0, slf.clone()); } + c_function_wrapper(vm, args, method, flags) }; vm.ctx.new_method_def(name, callable, flags, doc) @@ -78,39 +73,83 @@ pub(crate) fn build_method_def( fn c_function_wrapper( vm: &VirtualMachine, - slf: Option<&PyObjectRef>, mut args: FuncArgs, method: PyMethodPointer, - flags: PyMethodFlags, + mut flags: PyMethodFlags, ) -> PyResult { + let slf = if flags.contains(PyMethodFlags::STATIC) { + None + } else { + if !args.args.is_empty() { + Some(args.args.remove(0)) + } else { + None + } + }; + + flags.remove(PyMethodFlags::STATIC | PyMethodFlags::CLASS); + let slf_ptr = slf .map(|slf| slf.as_object().as_raw().cast_mut()) .unwrap_or_default(); - let arg_tuple = vm.ctx.new_tuple(core::mem::take(&mut args.args)); - let arg_tuple_ptr = arg_tuple.as_object().as_raw().cast_mut(); - - let ret_ptr = bitflags_match!(flags, { - PyMethodFlags::NOARGS => { - debug_assert!(arg_tuple.is_empty(), "Expected no arguments, but got some"); - let f = unsafe { method.function }; - unsafe { Ok(f(slf_ptr, arg_tuple_ptr)) } - }, - PyMethodFlags::VARARGS => { - let f = unsafe { method.function }; - unsafe { Ok(f(slf_ptr, arg_tuple_ptr)) } - }, - PyMethodFlags::VARARGS | PyMethodFlags::KEYWORDS => { - let f = unsafe { method.function_with_keywords }; - let kwargs = vm.ctx.new_dict(); - for (k, v) in args.kwargs { - kwargs.set_item(&*k, v, vm)?; - } - let kwargs_ptr = kwargs.as_object().as_raw().cast_mut(); - unsafe { Ok(f(slf_ptr, arg_tuple_ptr, kwargs_ptr)) } - }, - _ => panic!("Unexpected flags value: {flags:?}"), - })?; + let ret_ptr = { + if flags.contains(PyMethodFlags::FASTCALL) { + bitflags_match!(flags, { + PyMethodFlags::KEYWORDS | PyMethodFlags::FASTCALL => { + let f = unsafe { method.function_fast_with_keywords }; + let nargs = args.args.len(); + let mut fastcall_args = args.args; + let mut kwnames_tuple = None; + if !args.kwargs.is_empty() { + let mut kwnames = Vec::with_capacity(args.kwargs.len()); + for (k, v) in args.kwargs { + kwnames.push(vm.ctx.new_str(k).into()); + fastcall_args.push(v); + } + kwnames_tuple = Some(vm.ctx.new_tuple(kwnames)); + } + + let fastcall_arg_ptrs = fastcall_args + .iter() + .map(|obj| obj.as_object().as_raw().cast_mut()) + .collect::>(); + let kwnames_ptr = kwnames_tuple + .as_ref() + .map(|tuple| tuple.as_object().as_raw().cast_mut()) + .unwrap_or(core::ptr::null_mut()); + + unsafe { Ok(f(slf_ptr, fastcall_arg_ptrs.as_ptr(), nargs as isize, kwnames_ptr)) } + } + _ => panic!("Unexpected flags value: {flags:?}"), + })? + } else { + let arg_tuple = vm.ctx.new_tuple(core::mem::take(&mut args.args)); + let arg_tuple_ptr = arg_tuple.as_object().as_raw().cast_mut(); + + bitflags_match!(flags, { + PyMethodFlags::NOARGS => { + debug_assert!(arg_tuple.is_empty(), "Expected no arguments, but got some"); + let f = unsafe { method.function }; + unsafe { Ok(f(slf_ptr, arg_tuple_ptr)) } + }, + PyMethodFlags::VARARGS => { + let f = unsafe { method.function }; + unsafe { Ok(f(slf_ptr, arg_tuple_ptr)) } + }, + PyMethodFlags::VARARGS | PyMethodFlags::KEYWORDS => { + let f = unsafe { method.function_with_keywords }; + let kwargs = vm.ctx.new_dict(); + for (k, v) in args.kwargs { + kwargs.set_item(&*k, v, vm)?; + } + let kwargs_ptr = kwargs.as_object().as_raw().cast_mut(); + unsafe { Ok(f(slf_ptr, arg_tuple_ptr, kwargs_ptr)) } + }, + _ => panic!("Unexpected flags value: {flags:?}"), + })? + } + }; let ret_ptr = NonNull::new(ret_ptr).ok_or_else(|| { vm.take_raised_exception() diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index e7fdbd296f6..c2bfaef5759 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -505,7 +505,7 @@ pub extern "C" fn PyObject_GenericSetDict( #[cfg(test)] mod tests { use pyo3::prelude::*; - use pyo3::types::{PyBool, PyDict, PyInt, PyNone, PyString}; + use pyo3::types::{PyBool, PyDict, PyInt, PyNone, PyString, PyType}; #[test] fn test_is_truthy() { @@ -614,6 +614,17 @@ mod tests { fn method1(&self) -> PyResult { Ok(self.num + 10) } + + #[staticmethod] + fn static_method(a: i32, b: i32) -> PyResult { + Ok(a + b) + } + + #[classmethod] + fn cls_method(cls: &Bound<'_, PyType>) -> PyResult { + // assert!(cls.is_subclass_of::()?); + Ok(10) + } } Python::attach(|py| { @@ -624,14 +635,29 @@ mod tests { py.run(c"assert instance.num == 3", Some(&globals), None) .unwrap(); - #[cfg(feature = "nightly")] assert_eq!( - obj.call_method0("method1") + obj.call_method1("method1", ()) .unwrap() .extract::() .unwrap(), 13 ); + + assert_eq!( + obj.call_method1("static_method", (5, 8)) + .unwrap() + .extract::() + .unwrap(), + 13 + ); + + assert_eq!( + obj.call_method1("cls_method", ()) + .unwrap() + .extract::() + .unwrap(), + 10 + ); }); } } From 6745ce437ba3fef21e63a404bbe6f66444b8748f Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 28 Apr 2026 18:39:44 +0200 Subject: [PATCH 085/135] Add module definition support --- crates/capi/src/methodobject.rs | 4 +- crates/capi/src/moduleobject.rs | 131 ++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 99712bc6900..5261843a626 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -62,7 +62,9 @@ pub(crate) fn build_method_def( let method = ml.ml_meth; let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - if let Some(slf) = slf.as_ref() { + if let Some(slf) = slf.as_ref() + && !flags.contains(PyMethodFlags::STATIC) + { args.args.insert(0, slf.clone()); } c_function_wrapper(vm, args, method, flags) diff --git a/crates/capi/src/moduleobject.rs b/crates/capi/src/moduleobject.rs index 29dc1a00203..2a126de75ba 100644 --- a/crates/capi/src/moduleobject.rs +++ b/crates/capi/src/moduleobject.rs @@ -1,6 +1,87 @@ use crate::PyObject; +use crate::methodobject::PyMethodDef; use crate::pystate::with_vm; +use rustpython_vm::AsObject; use rustpython_vm::builtins::PyModule; +use std::ffi::{CStr, c_char, c_int, c_void}; + +const PY_MOD_CREATE: c_int = 1; +const PY_MOD_EXEC: c_int = 2; +const PY_MOD_GIL: c_int = 4; + +#[repr(C)] +pub struct PyModuleDef { + m_base: [u8; 40], + m_name: *const c_char, + m_doc: *const c_char, + m_size: isize, + m_methods: *mut PyMethodDef, + m_slots: *mut PyModuleDef_Slot, + m_traverse: *mut c_void, + m_clear: *mut c_void, + m_free: *mut c_void, +} + +#[repr(C)] +pub struct PyModuleDef_Slot { + id: c_int, + value: PyModuleDef_SlotValue, +} + +union PyModuleDef_SlotValue { + exec_module: extern "C" fn(*mut PyObject) -> c_int, + gil_used: usize, +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyModuleDef_Init(def: *mut PyModuleDef) -> *mut PyObject { + with_vm(|vm| { + let name = unsafe { CStr::from_ptr((&*def).m_name) } + .to_str() + .expect("Module name is not valid UTF-8"); + let doc = unsafe { CStr::from_ptr((*def).m_doc) } + .to_str() + .expect("Module doc is not valid UTF-8"); + let dict = vm.ctx.new_dict(); + + let module = vm.new_module(name, dict, Some(vm.ctx.new_str(doc))); + + let mut slot_ptr = unsafe { (*def).m_slots }; + while let slot = unsafe { &*slot_ptr } + && slot.id != 0 + { + match slot.id { + PY_MOD_CREATE => { + return Err(vm.new_import_error( + "RustPython does not support modules that define a create slot", + vm.ctx.new_str(name), + )); + } + PY_MOD_EXEC => { + let exec_module = unsafe { slot.value.exec_module }; + if exec_module(module.as_object().as_raw().cast_mut()) != 0 { + return Err(vm + .take_raised_exception() + .expect("Module exec function failed without setting an exception")); + } + } + PY_MOD_GIL => { + if unsafe { slot.value.gil_used == 0 } { + return Err(vm.new_import_error( + "RustPython does not support modules that require the GIL", + vm.ctx.new_str(name), + )); + } + } + _ => todo!("Got unknown PyModuleDef_Slot with id {}", slot.id), + } + + slot_ptr = unsafe { slot_ptr.add(1) }; + } + + Ok(module) + }) +} #[unsafe(no_mangle)] pub extern "C" fn PyModule_GetNameObject(module: *mut PyObject) -> *mut PyObject { @@ -9,3 +90,53 @@ pub extern "C" fn PyModule_GetNameObject(module: *mut PyObject) -> *mut PyObject module.get_attr("__name__", vm) }) } + +#[cfg(test)] +mod tests { + use super::PyModuleDef; + use core::mem::offset_of; + use pyo3::prelude::*; + + #[test] + fn test_create_module() { + const { + assert!( + offset_of!(PyModuleDef, m_name) == size_of::(), + "PyModuleDef::m_base was not the expected size" + ); + }; + + #[pymodule] + mod my_extension { + use pyo3::prelude::*; + + #[pymodule_export] + const PI: f64 = std::f64::consts::PI; + + #[pyfunction] // Inline definition of a pyfunction, also made available to Python + fn triple(x: usize) -> usize { + x * 3 + } + } + + Python::attach(|py| { + let module = unsafe { + Borrowed::from_ptr(py, my_extension::__pyo3_init()).cast_unchecked::() + }; + assert_eq!(module.name().unwrap(), "my_extension"); + + module.getattr("PI").unwrap().extract::().unwrap(); + + assert_eq!( + module + .getattr("triple") + .unwrap() + .call1((10,)) + .unwrap() + .extract::() + .unwrap(), + 30 + ); + }) + } +} From c7b4f405ae1ce9a24ebd02bc71c61c4e2f059c26 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 29 Apr 2026 11:08:00 +0200 Subject: [PATCH 086/135] Implement `PyTuple_GetSlice` --- crates/capi/src/tupleobject.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/capi/src/tupleobject.rs b/crates/capi/src/tupleobject.rs index eb19c1dfc45..4c76d2cb00e 100644 --- a/crates/capi/src/tupleobject.rs +++ b/crates/capi/src/tupleobject.rs @@ -73,6 +73,21 @@ pub extern "C" fn PyTuple_GetItem(tuple: *mut PyObject, pos: isize) -> *mut PyOb }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyTuple_GetSlice(tuple: *mut PyObject, low: isize, high: isize) -> *mut PyObject { + with_vm(|vm| { + let tuple = unsafe { &*tuple }.try_downcast_ref::(vm)?; + let view = tuple + .iter() + .skip(low as usize) + .take((high - low) as usize) + .cloned() + .collect(); + + Ok(vm.ctx.new_tuple(view)) + }) +} + #[cfg(test)] mod tests { use pyo3::prelude::*; @@ -93,4 +108,13 @@ mod tests { assert_eq!(tuple.len(), 3); }) } + + #[test] + fn test_tuple_get_slice() { + Python::attach(|py| { + let tuple = (1, 2, 3).into_pyobject(py).unwrap(); + let slice = tuple.get_slice(1, 2); + assert_eq!(slice.extract::<(u32,)>().unwrap(), (2,)); + }) + } } From 931bdd8fff3984bab3181c7b4fb14f5b259e8d5d Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 29 Apr 2026 14:35:03 +0200 Subject: [PATCH 087/135] Implement `Py_GetVersion` --- crates/capi/src/pylifecycle.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 29e9fd75463..dccaf166cfa 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,10 +1,11 @@ use crate::log_stub; use crate::pyerrors::init_exception_statics; use crate::pystate::attach_vm_to_thread; -use core::ffi::c_int; +use core::ffi::{c_char, c_int}; +use rustpython_vm::version::get_version; use rustpython_vm::vm::thread::ThreadedVirtualMachine; use rustpython_vm::{Context, Interpreter}; -use std::sync::{Once, OnceLock, mpsc}; +use std::sync::{LazyLock, Once, OnceLock, mpsc}; static VM_REQUEST_TX: OnceLock>> = OnceLock::new(); @@ -92,3 +93,22 @@ 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(get_version); + VERSION.as_str().as_ptr() as *const c_char +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + + #[test] + fn test_get_version() { + Python::attach(|py| { + let version = py.version_info(); + assert!(version >= (3, 14)); + }); + } +} From 020a5c9c63eab05300ab10d125549199fb66fa58 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 29 Apr 2026 15:01:20 +0200 Subject: [PATCH 088/135] Implement `PyCapsule_IsValid` --- crates/capi/src/capsule.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/capi/src/capsule.rs b/crates/capi/src/capsule.rs index b990bd62978..5b3a6b103e6 100644 --- a/crates/capi/src/capsule.rs +++ b/crates/capi/src/capsule.rs @@ -1,6 +1,6 @@ use crate::PyObject; use crate::pystate::with_vm; -use core::ffi::{c_char, c_void}; +use core::ffi::{c_char, c_int, c_void}; use rustpython_vm::builtins::PyCapsule; #[allow(non_camel_case_types)] @@ -26,6 +26,19 @@ pub extern "C" fn PyCapsule_GetPointer( }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyCapsule_IsValid(capsule: *mut PyObject, _name: *const c_char) -> c_int { + with_vm(|vm| { + let Some(capsule) = + unsafe { capsule.as_ref() }.and_then(|obj| obj.downcast_ref_if_exact::(vm)) + else { + return false; + }; + + !capsule.pointer().is_null() + }) +} + #[unsafe(no_mangle)] pub extern "C" fn PyCapsule_GetName(_capsule: *mut PyObject) -> *const c_char { core::ptr::null_mut() @@ -41,6 +54,7 @@ mod tests { Python::attach(|py| { let value = String::from("Some data"); let capsule = PyCapsule::new_with_value(py, value, c"my_capsule").unwrap(); + assert!(capsule.is_valid_checked(Some(c"my_capsule"))); let ptr = capsule.pointer_checked(Some(c"my_capsule")).unwrap(); assert_eq!(unsafe { ptr.cast::().as_ref() }, "Some data"); }) From efda7cc99fb7b6e4ae03b873da35da31f4b08e39 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 29 Apr 2026 16:51:59 +0200 Subject: [PATCH 089/135] Refactor c function trampolines --- crates/capi/src/methodobject.rs | 259 +++++++++++++++++++++----------- 1 file changed, 172 insertions(+), 87 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 5261843a626..33a3cb003f0 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -1,7 +1,6 @@ use crate::PyObject; use crate::object::PyTypeObject; use crate::pystate::with_vm; -use bitflags::bitflags_match; use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; use rustpython_vm::function::{FuncArgs, HeapMethodDef, PyMethodFlags}; @@ -36,70 +35,140 @@ pub struct PyMethodDef { pub(crate) ml_doc: *const c_char, } -pub(crate) fn build_method_def( - vm: &VirtualMachine, - ml: &PyMethodDef, - slf: Option, -) -> PyRef { - let name = unsafe { CStr::from_ptr(ml.ml_name) } - .to_str() - .expect("Method name was not valid UTF-8"); +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct FunctionType { + call: CallConvention, + binding: Binding, +} - let doc = NonNull::new(ml.ml_doc.cast_mut()).map(|doc| { - unsafe { CStr::from_ptr(doc.as_ptr()) } - .to_str() - .expect("Method doc was not valid UTF-8") - }); +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum CallConvention { + NoArgs, + OneArg, + VarArgs, + VarArgsKeywords, + FastCall, + FastCallKeywords, +} - let flags = - PyMethodFlags::from_bits(ml.ml_flags as u32).expect("PyMethodDef contains unknown flags"); +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum Binding { + Instance, + Class, + Static, +} - assert!( - !flags.intersects(PyMethodFlags::METHOD), - "These flags are not yet supported: {:?}", - flags - ); - - let method = ml.ml_meth; - let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - if let Some(slf) = slf.as_ref() - && !flags.contains(PyMethodFlags::STATIC) - { - args.args.insert(0, slf.clone()); +impl Binding { + fn self_ptr(self, args: &mut FuncArgs, slf: Option<&PyObjectRef>) -> *mut PyObject { + if self == Binding::Static { + core::ptr::null_mut() + } else if let Some(slf) = slf { + slf.as_object().as_raw().cast_mut() + } else if !args.args.is_empty() { + args.args.remove(0).as_object().as_raw().cast_mut() + } else { + core::ptr::null_mut() } - c_function_wrapper(vm, args, method, flags) - }; - - vm.ctx.new_method_def(name, callable, flags, doc) + } } -fn c_function_wrapper( - vm: &VirtualMachine, - mut args: FuncArgs, - method: PyMethodPointer, - mut flags: PyMethodFlags, -) -> PyResult { - let slf = if flags.contains(PyMethodFlags::STATIC) { - None - } else { - if !args.args.is_empty() { - Some(args.args.remove(0)) - } else { - None +impl FunctionType { + fn from_flags(flags: PyMethodFlags) -> Self { + if flags.contains(PyMethodFlags::METHOD) { + todo!("METH_METHOD is not supported yet") } - }; - flags.remove(PyMethodFlags::STATIC | PyMethodFlags::CLASS); + let binding = if flags.contains(PyMethodFlags::STATIC) { + debug_assert!(!flags.contains(PyMethodFlags::CLASS)); + Binding::Static + } else if flags.contains(PyMethodFlags::CLASS) { + Binding::Class + } else { + Binding::Instance + }; + + let call_flags = flags + & (PyMethodFlags::VARARGS + | PyMethodFlags::KEYWORDS + | PyMethodFlags::NOARGS + | PyMethodFlags::O + | PyMethodFlags::FASTCALL); - let slf_ptr = slf - .map(|slf| slf.as_object().as_raw().cast_mut()) - .unwrap_or_default(); + let call = if call_flags == PyMethodFlags::NOARGS { + CallConvention::NoArgs + } else if call_flags == PyMethodFlags::O { + CallConvention::OneArg + } else if call_flags == PyMethodFlags::VARARGS { + CallConvention::VarArgs + } else if call_flags == (PyMethodFlags::VARARGS | PyMethodFlags::KEYWORDS) { + CallConvention::VarArgsKeywords + } else if call_flags == PyMethodFlags::FASTCALL { + CallConvention::FastCall + } else if call_flags == (PyMethodFlags::FASTCALL | PyMethodFlags::KEYWORDS) { + CallConvention::FastCallKeywords + } else { + todo!("unsupported or invalid calling-convention flags"); + }; - let ret_ptr = { - if flags.contains(PyMethodFlags::FASTCALL) { - bitflags_match!(flags, { - PyMethodFlags::KEYWORDS | PyMethodFlags::FASTCALL => { - let f = unsafe { method.function_fast_with_keywords }; + Self { call, binding } + } + + fn build_heap_method_def( + self, + vm: &VirtualMachine, + name: &'static str, + flags: PyMethodFlags, + method: PyMethodPointer, + slf: Option, + doc: Option<&'static str>, + ) -> PyRef { + let binding = self.binding; + match self.call { + CallConvention::NoArgs => { + let f = unsafe { method.function }; + let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { + let slf_ptr = binding.self_ptr(&mut args, slf.as_ref()); + let arg_tuple = vm.ctx.new_tuple(args.args); + debug_assert!(arg_tuple.is_empty(), "Expected no arguments, but got some"); + let ret_ptr = unsafe { f(slf_ptr, arg_tuple.as_object().as_raw().cast_mut()) }; + ret_ptr_to_pyresult(vm, ret_ptr) + }; + vm.ctx.new_method_def(name, callable, flags, doc) + } + CallConvention::VarArgs => { + let f = unsafe { method.function }; + let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { + let slf_ptr = binding.self_ptr(&mut args, slf.as_ref()); + let arg_tuple = vm.ctx.new_tuple(args.args); + let ret_ptr = unsafe { f(slf_ptr, arg_tuple.as_object().as_raw().cast_mut()) }; + ret_ptr_to_pyresult(vm, ret_ptr) + }; + vm.ctx.new_method_def(name, callable, flags, doc) + } + CallConvention::VarArgsKeywords => { + let f = unsafe { method.function_with_keywords }; + let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { + let slf_ptr = binding.self_ptr(&mut args, slf.as_ref()); + let arg_tuple = vm.ctx.new_tuple(args.args); + let kwargs = vm.ctx.new_dict(); + for (k, v) in args.kwargs { + kwargs.set_item(&*k, v, vm)?; + } + let ret_ptr = unsafe { + f( + slf_ptr, + arg_tuple.as_object().as_raw().cast_mut(), + kwargs.as_object().as_raw().cast_mut(), + ) + }; + ret_ptr_to_pyresult(vm, ret_ptr) + }; + vm.ctx.new_method_def(name, callable, flags, doc) + } + CallConvention::FastCallKeywords => { + let f = unsafe { method.function_fast_with_keywords }; + let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { + let slf_ptr = binding.self_ptr(&mut args, slf.as_ref()); let nargs = args.args.len(); let mut fastcall_args = args.args; let mut kwnames_tuple = None; @@ -111,7 +180,6 @@ fn c_function_wrapper( } kwnames_tuple = Some(vm.ctx.new_tuple(kwnames)); } - let fastcall_arg_ptrs = fastcall_args .iter() .map(|obj| obj.as_object().as_raw().cast_mut()) @@ -120,39 +188,35 @@ fn c_function_wrapper( .as_ref() .map(|tuple| tuple.as_object().as_raw().cast_mut()) .unwrap_or(core::ptr::null_mut()); - - unsafe { Ok(f(slf_ptr, fastcall_arg_ptrs.as_ptr(), nargs as isize, kwnames_ptr)) } - } - _ => panic!("Unexpected flags value: {flags:?}"), - })? - } else { - let arg_tuple = vm.ctx.new_tuple(core::mem::take(&mut args.args)); - let arg_tuple_ptr = arg_tuple.as_object().as_raw().cast_mut(); - - bitflags_match!(flags, { - PyMethodFlags::NOARGS => { - debug_assert!(arg_tuple.is_empty(), "Expected no arguments, but got some"); - let f = unsafe { method.function }; - unsafe { Ok(f(slf_ptr, arg_tuple_ptr)) } - }, - PyMethodFlags::VARARGS => { - let f = unsafe { method.function }; - unsafe { Ok(f(slf_ptr, arg_tuple_ptr)) } - }, - PyMethodFlags::VARARGS | PyMethodFlags::KEYWORDS => { - let f = unsafe { method.function_with_keywords }; - let kwargs = vm.ctx.new_dict(); - for (k, v) in args.kwargs { - kwargs.set_item(&*k, v, vm)?; - } - let kwargs_ptr = kwargs.as_object().as_raw().cast_mut(); - unsafe { Ok(f(slf_ptr, arg_tuple_ptr, kwargs_ptr)) } - }, - _ => panic!("Unexpected flags value: {flags:?}"), - })? + let ret_ptr = unsafe { + f( + slf_ptr, + fastcall_arg_ptrs.as_ptr(), + nargs as isize, + kwnames_ptr, + ) + }; + ret_ptr_to_pyresult(vm, ret_ptr) + }; + vm.ctx.new_method_def(name, callable, flags, doc) + } + CallConvention::FastCall => { + let callable = move |_args: FuncArgs, _vm: &VirtualMachine| -> PyResult { + todo!("METH_FASTCALL without METH_KEYWORDS is not supported yet") + }; + vm.ctx.new_method_def(name, callable, flags, doc) + } + CallConvention::OneArg => { + let callable = move |_args: FuncArgs, _vm: &VirtualMachine| -> PyResult { + todo!("METH_O is not supported yet") + }; + vm.ctx.new_method_def(name, callable, flags, doc) + } } - }; + } +} +fn ret_ptr_to_pyresult(vm: &VirtualMachine, ret_ptr: *mut PyObject) -> PyResult { let ret_ptr = NonNull::new(ret_ptr).ok_or_else(|| { vm.take_raised_exception() .expect("Native function returned NULL, but there was no exception set") @@ -160,6 +224,27 @@ fn c_function_wrapper( Ok(unsafe { PyObjectRef::from_raw(ret_ptr) }) } +pub(crate) fn build_method_def( + vm: &VirtualMachine, + ml: &PyMethodDef, + slf: Option, +) -> PyRef { + let name = unsafe { CStr::from_ptr(ml.ml_name) } + .to_str() + .expect("Method name was not valid UTF-8"); + + let doc = NonNull::new(ml.ml_doc.cast_mut()).map(|doc| { + unsafe { CStr::from_ptr(doc.as_ptr()) } + .to_str() + .expect("Method doc was not valid UTF-8") + }); + + let flags = + PyMethodFlags::from_bits(ml.ml_flags as u32).expect("PyMethodDef contains unknown flags"); + + FunctionType::from_flags(flags).build_heap_method_def(vm, name, flags, ml.ml_meth, slf, doc) +} + #[unsafe(no_mangle)] pub extern "C" fn PyCMethod_New( ml: *mut PyMethodDef, From 88b4bbce2a1fc8d5c688e69ce3c31430cf8cd3e6 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 30 Apr 2026 13:59:28 +0200 Subject: [PATCH 090/135] Implement `PyList_Insert` --- crates/capi/src/listobject.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/capi/src/listobject.rs b/crates/capi/src/listobject.rs index e30ac3f4d4b..33d4c90bd5f 100644 --- a/crates/capi/src/listobject.rs +++ b/crates/capi/src/listobject.rs @@ -61,6 +61,20 @@ pub extern "C" fn PyList_Append(list: *mut PyObject, item: *mut PyObject) -> c_i }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyList_Insert(list: *mut PyObject, index: isize, item: *mut PyObject) -> c_int { + with_vm(|vm| { + let list = unsafe { &*list }.try_downcast_ref::(vm)?; + let item = unsafe { &*item }.to_owned(); + let mut vec = list.borrow_vec_mut(); + if index as usize > vec.len() { + Err(vm.new_index_error(format!("list index out of range: {index}"))) + } else { + Ok(vec.insert(index as _, item)) + } + }) +} + #[cfg(test)] mod tests { use pyo3::exceptions::PyIndexError; @@ -112,4 +126,15 @@ mod tests { assert_eq!(list.get_item(0).unwrap().extract::().unwrap(), 1); }) } + + #[test] + fn test_list_insert() { + Python::attach(|py| { + let list = PyList::empty(py); + assert_eq!(list.len(), 0); + list.insert(0, 1).unwrap(); + assert_eq!(list.len(), 1); + assert!(list.insert(2, 3).is_err()); + }) + } } From 1a3e888efc1b5c17d8c4ef2bc71c1efae068dec8 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 30 Apr 2026 14:04:32 +0200 Subject: [PATCH 091/135] Implement `PyList_Reverse` --- crates/capi/src/listobject.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/capi/src/listobject.rs b/crates/capi/src/listobject.rs index 33d4c90bd5f..5e0ca5d18d7 100644 --- a/crates/capi/src/listobject.rs +++ b/crates/capi/src/listobject.rs @@ -75,6 +75,14 @@ pub extern "C" fn PyList_Insert(list: *mut PyObject, index: isize, item: *mut Py }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyList_Reverse(list: *mut PyObject) -> c_int { + with_vm(|vm| { + let list = unsafe { &*list }.try_downcast_ref::(vm)?; + Ok(list.borrow_vec_mut().reverse()) + }) +} + #[cfg(test)] mod tests { use pyo3::exceptions::PyIndexError; @@ -137,4 +145,14 @@ mod tests { assert!(list.insert(2, 3).is_err()); }) } + + #[test] + fn test_list_reverse() { + Python::attach(|py| { + let list = PyList::new(py, &[1, 2, 3]).unwrap(); + list.reverse().unwrap(); + assert_eq!(list.get_item(0).unwrap().extract::().unwrap(), 3); + assert_eq!(list.get_item(2).unwrap().extract::().unwrap(), 1); + }) + } } From 183526868279dda644abaec846e5c5a2ee07ecc8 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 1 May 2026 12:58:16 +0200 Subject: [PATCH 092/135] Allow `PyBytes_FromStringAndSize` with uninitialized bytes --- crates/capi/src/bytesobject.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/capi/src/bytesobject.rs b/crates/capi/src/bytesobject.rs index 8000cd22544..d2028574de1 100644 --- a/crates/capi/src/bytesobject.rs +++ b/crates/capi/src/bytesobject.rs @@ -5,13 +5,14 @@ use rustpython_vm::builtins::PyBytes; #[unsafe(no_mangle)] pub extern "C" fn PyBytes_FromStringAndSize(bytes: *mut c_char, len: isize) -> *mut PyObject { with_vm(|vm| { - if bytes.is_null() { - todo!("PyBytes_FromStringAndSize with null bytes is not yet implemented"); + let data = if bytes.is_null() { + let mut data = Vec::with_capacity(len as usize); + unsafe { data.set_len(len as usize) }; + data } else { - let bytes_slice = - unsafe { core::slice::from_raw_parts(bytes as *const u8, len as usize) }; - vm.ctx.new_bytes(bytes_slice.to_vec()) - } + unsafe { core::slice::from_raw_parts(bytes as *const u8, len as usize) }.to_vec() + }; + vm.ctx.new_bytes(data) }) } @@ -43,4 +44,16 @@ mod tests { assert_eq!(bytes.as_bytes(), b"Hello, World!"); }) } + + #[test] + fn test_bytes_uninit() { + Python::attach(|py| { + let bytes = PyBytes::new_with(py, 13, |data| { + data.copy_from_slice(b"Hello, World!"); + Ok(()) + }) + .unwrap(); + assert_eq!(bytes.as_bytes(), b"Hello, World!"); + }) + } } From 2562f9dd748e54fd5d467c300d9adb69fec06391 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 5 May 2026 12:42:11 +0200 Subject: [PATCH 093/135] Add basic capi error support --- crates/capi/src/lib.rs | 16 +- crates/capi/src/pyerrors.rs | 269 +++++++++++++++++++++++++++++++++ crates/capi/src/pylifecycle.rs | 5 +- crates/capi/src/pystate.rs | 9 +- crates/capi/src/util.rs | 137 +++++++++++++++++ crates/vm/src/builtins/type.rs | 2 +- crates/vm/src/vm/mod.rs | 17 ++- src/lib.rs | 2 +- 8 files changed, 449 insertions(+), 8 deletions(-) create mode 100644 crates/capi/src/pyerrors.rs create mode 100644 crates/capi/src/util.rs diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index c6a250da724..3207149e316 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -1,15 +1,17 @@ #![allow(clippy::missing_safety_doc)] +use crate::pyerrors::init_exception_statics; use crate::pylifecycle::MAIN_INTERP; -use rustpython_vm::Interpreter; pub use rustpython_vm::PyObject; +use rustpython_vm::{Context, Interpreter}; use std::sync::MutexGuard; extern crate alloc; - +pub mod pyerrors; pub mod pylifecycle; pub mod pystate; pub mod refcount; +mod util; /// Get main interpreter of this process. Will be None if it has not been initialized yet. pub fn get_main_interpreter() -> MutexGuard<'static, Option> { @@ -17,3 +19,13 @@ pub fn get_main_interpreter() -> MutexGuard<'static, Option> { .lock() .expect("Failed to lock interpreter mutex") } + +/// Set the main interpreter of this process. This method will panic when there is already an +/// interpreter set. +pub fn set_main_interpreter(interpreter: Interpreter) { + let mut interp = get_main_interpreter(); + assert!(interp.is_none(), "Main interpreter is already set"); + // Safety: Interpreter was not initialized before, so we can safely assume the statics are not used + unsafe { init_exception_statics(&Context::genesis().exceptions) }; + *interp = Some(interpreter); +} diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs new file mode 100644 index 00000000000..ec5578e48fa --- /dev/null +++ b/crates/capi/src/pyerrors.rs @@ -0,0 +1,269 @@ +use crate::PyObject; +use crate::pystate::with_vm; +use core::convert::Infallible; +use core::ffi::{CStr, c_char, c_int}; +use core::ptr::NonNull; +use rustpython_vm::builtins::{PyBaseException, PyTuple, PyType}; +use rustpython_vm::convert::IntoObject; +use rustpython_vm::exceptions::ExceptionZoo; +use rustpython_vm::{AsObject, PyObjectRef, PyResult}; + +macro_rules! define_exception_statics { + ($( $(#[$meta:meta])* $export:ident => $exc:ident ),* $(,)?) => { + $( + $(#[$meta])* + #[unsafe(no_mangle)] + pub static mut $export: *mut PyObject = core::ptr::null_mut(); + )* + + #[allow(static_mut_refs)] + pub(crate) unsafe fn init_exception_statics(zoo: &'static ExceptionZoo) { + unsafe { + $( + $export = zoo.$exc.as_object().as_raw().cast_mut(); + )* + } + } + }; +} + +define_exception_statics! { + PyExc_BaseException => base_exception_type, + PyExc_BaseExceptionGroup => base_exception_group, + PyExc_SystemExit => system_exit, + PyExc_KeyboardInterrupt => keyboard_interrupt, + PyExc_GeneratorExit => generator_exit, + PyExc_Exception => exception_type, + PyExc_StopIteration => stop_iteration, + PyExc_StopAsyncIteration => stop_async_iteration, + PyExc_ArithmeticError => arithmetic_error, + PyExc_FloatingPointError => floating_point_error, + PyExc_SystemError => system_error, + PyExc_TypeError => type_error, + PyExc_OverflowError => overflow_error, + PyExc_ZeroDivisionError => zero_division_error, + PyExc_AssertionError => assertion_error, + PyExc_IndexError => index_error, + PyExc_KeyError => key_error, + PyExc_LookupError => lookup_error, + PyExc_AttributeError => attribute_error, + PyExc_BufferError => buffer_error, + PyExc_EOFError => eof_error, + PyExc_ImportError => import_error, + PyExc_ModuleNotFoundError => module_not_found_error, + PyExc_MemoryError => memory_error, + PyExc_NameError => name_error, + PyExc_UnboundLocalError => unbound_local_error, + PyExc_OSError => os_error, + PyExc_BlockingIOError => blocking_io_error, + PyExc_ChildProcessError => child_process_error, + PyExc_ConnectionError => connection_error, + PyExc_BrokenPipeError => broken_pipe_error, + PyExc_ConnectionAbortedError => connection_aborted_error, + PyExc_ConnectionRefusedError => connection_refused_error, + PyExc_ConnectionResetError => connection_reset_error, + PyExc_FileExistsError => file_exists_error, + PyExc_FileNotFoundError => file_not_found_error, + PyExc_InterruptedError => interrupted_error, + PyExc_IsADirectoryError => is_a_directory_error, + PyExc_NotADirectoryError => not_a_directory_error, + PyExc_PermissionError => permission_error, + PyExc_ProcessLookupError => process_lookup_error, + PyExc_TimeoutError => timeout_error, + PyExc_ReferenceError => reference_error, + PyExc_RuntimeError => runtime_error, + PyExc_NotImplementedError => not_implemented_error, + PyExc_RecursionError => recursion_error, + PyExc_SyntaxError => syntax_error, + PyExc_IndentationError => indentation_error, + PyExc_TabError => tab_error, + PyExc_ValueError => value_error, + PyExc_UnicodeError => unicode_error, + PyExc_UnicodeDecodeError => unicode_decode_error, + PyExc_UnicodeEncodeError => unicode_encode_error, + PyExc_UnicodeTranslateError => unicode_translate_error, + PyExc_Warning => warning, + PyExc_DeprecationWarning => deprecation_warning, + PyExc_PendingDeprecationWarning => pending_deprecation_warning, + PyExc_RuntimeWarning => runtime_warning, + PyExc_SyntaxWarning => syntax_warning, + PyExc_UserWarning => user_warning, + PyExc_FutureWarning => future_warning, + PyExc_ImportWarning => import_warning, + PyExc_UnicodeWarning => unicode_warning, + PyExc_BytesWarning => bytes_warning, + PyExc_ResourceWarning => resource_warning, + PyExc_EncodingWarning => encoding_warning, +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyErr_Occurred() -> *mut PyObject { + with_vm(|vm| { + vm.current_exception() + .map(|exc| exc.class().as_object().as_raw()) + .unwrap_or_default() + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject { + with_vm(|vm| { + vm.take_raised_exception() + .map(|exc| exc.into_object().into_raw().as_ptr()) + .unwrap_or_default() + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyErr_SetRaisedException(exc: *mut PyObject) { + with_vm(|vm| { + if let Some(exc) = NonNull::new(exc) { + let exception = unsafe { PyObjectRef::from_raw(exc).downcast_unchecked() }; + vm.set_exception(Some(exception)); + } else { + vm.set_exception(None); + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyErr_SetObject(exception: *mut PyObject, value: *mut PyObject) { + with_vm::, _>(|vm| { + let exc_type = unsafe { (&*exception).to_owned() }; + let exc_val = unsafe { (&*value).to_owned() }; + + let normalized = vm.normalize_exception(exc_type, exc_val, vm.ctx.none())?; + Err(normalized) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyErr_SetString(exception: *mut PyObject, message: *const c_char) { + with_vm::, _>(|vm| { + let exc_type = unsafe { &*exception }.try_downcast_ref::(vm)?; + + let Ok(message) = unsafe { CStr::from_ptr(message) }.to_str() else { + return Err(vm.new_type_error("Exception message is not valid UTF-8")); + }; + + let exc = vm.invoke_exception( + exc_type.to_owned(), + vec![vm.ctx.new_str(message).into_object()], + )?; + + Err(exc) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyErr_PrintEx(_set_sys_last_vars: c_int) { + with_vm(|vm| { + let exception = vm + .take_raised_exception() + .expect("No exception set in PyErr_PrintEx"); + + vm.print_exception(exception); + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyErr_DisplayException(exc: *mut PyObject) { + with_vm(|vm| { + let exception = unsafe { &*exc } + .downcast_ref::() + .expect("PyErr_DisplayException exc must be an exception instance") + .to_owned(); + + vm.print_exception(exception); + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyErr_WriteUnraisable(obj: *mut PyObject) { + with_vm(|vm| { + let exception = vm + .take_raised_exception() + .expect("No exception set in PyErr_WriteUnraisable"); + + let object = unsafe { vm.unwrap_or_none(obj.as_ref().map(|obj| obj.to_owned())) }; + + vm.run_unraisable(exception, None, object) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyErr_NewException( + name: *const c_char, + base: *mut PyObject, + dict: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let (module, name) = unsafe { + CStr::from_ptr(name) + .to_str() + .expect("Exception name is not valid UTF-8") + .rsplit_once('.') + .expect("Exception name must be of the form 'module.ExceptionName'") + }; + + let bases = unsafe { base.as_ref() }.map(|bases| { + if let Some(ty) = bases.downcast_ref::() { + vec![ty.to_owned()] + } else if let Some(tuple) = bases.downcast_ref::() { + tuple + .iter() + .map(|item| item.to_owned().downcast()) + .collect::, _>>() + .expect("PyErr_NewException base tuple must contain only types") + } else { + panic!("PyErr_NewException base must be a type or a tuple of types"); + } + }); + + assert!( + dict.is_null(), + "PyErr_NewException with non-null dict is not supported yet" + ); + + vm.ctx.new_exception_type(module, name, bases) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyErr_NewExceptionWithDoc( + name: *const c_char, + _doc: *const c_char, + base: *mut PyObject, + dict: *mut PyObject, +) -> *mut PyObject { + unsafe { PyErr_NewException(name, base, dict) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyErr_GivenExceptionMatches( + given: *mut PyObject, + exc: *mut PyObject, +) -> c_int { + with_vm(|vm| { + let given = unsafe { &*given }; + let exc = unsafe { &*exc }; + + given.is_subclass(exc, vm) + }) +} + +#[cfg(test)] +mod tests { + use pyo3::exceptions::PyTypeError; + use pyo3::prelude::*; + + #[test] + fn test_raised_exception() { + Python::attach(|py| { + PyTypeError::new_err(py.None()).restore(py); + assert!(PyErr::occurred(py)); + assert!(unsafe { !pyo3::ffi::PyErr_GetRaisedException().is_null() }); + assert!(!PyErr::occurred(py)); + }) + } +} diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 6e986c98f4e..6760b2822a3 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,8 +1,9 @@ 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 rustpython_vm::Interpreter; use rustpython_vm::vm::thread::ThreadedVirtualMachine; +use rustpython_vm::{Context, Interpreter}; use std::sync::Mutex; pub(crate) static MAIN_INTERP: Mutex> = Mutex::new(None); @@ -29,6 +30,8 @@ pub extern "C" fn Py_Initialize() { pub extern "C" fn Py_InitializeEx(_initsigs: c_int) { let mut interp = get_main_interpreter(); if interp.is_none() { + // Safety: Interpreter was not initialized before, so we can safely assume the statics are not used + unsafe { init_exception_statics(&Context::genesis().exceptions) }; *interp = Interpreter::with_init(Default::default(), |_vm| {}).into(); drop(interp); ensure_thread_has_vm_attached(); diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index 107750be89e..ecbff21713d 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -1,10 +1,17 @@ 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, + CurrentVmAttachState, attach_current_thread, release_current_thread, with_current_vm, }; +#[allow(dead_code)] +pub(crate) fn with_vm, O>(f: impl FnOnce(&VirtualMachine) -> R) -> O { + with_current_vm(|vm| f(vm).into_output(vm)) +} + #[allow(non_camel_case_types)] type PyGILState_STATE = c_int; const PYGILSTATE_LOCKED: PyGILState_STATE = 0; diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs new file mode 100644 index 00000000000..95e11ff576e --- /dev/null +++ b/crates/capi/src/util.rs @@ -0,0 +1,137 @@ +use crate::PyObject; +use core::convert::Infallible; +use core::ffi::{c_char, c_double, c_int, c_long, c_void}; +use rustpython_vm::{PyObjectRef, PyRef, PyResult, VirtualMachine}; + +pub(crate) trait FfiResult { + const ERR_VALUE: Output; + + fn into_output(self, vm: &VirtualMachine) -> Output; +} + +impl FfiResult for () { + const ERR_VALUE: () = (); + + fn into_output(self, _vm: &VirtualMachine) { + self + } +} + +impl FfiResult for () { + const ERR_VALUE: c_int = -1; + + fn into_output(self, _vm: &VirtualMachine) -> c_int { + 0 + } +} + +impl FfiResult<*mut PyObject> for PyRef +where + Self: Into, +{ + const ERR_VALUE: *mut PyObject = core::ptr::null_mut(); + + fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject { + self.into().into_raw().as_ptr() + } +} + +impl FfiResult<*mut PyObject> for PyObjectRef { + const ERR_VALUE: *mut PyObject = core::ptr::null_mut(); + + fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject { + self.into_raw().as_ptr() + } +} + +impl FfiResult for *mut PyObject { + const ERR_VALUE: *mut PyObject = core::ptr::null_mut(); + + fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject { + self + } +} + +impl FfiResult<*mut PyObject> for *const PyObject { + const ERR_VALUE: *mut PyObject = core::ptr::null_mut(); + + fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject { + self.cast_mut() + } +} + +impl FfiResult for *mut c_void { + const ERR_VALUE: *mut c_void = core::ptr::null_mut(); + + fn into_output(self, _vm: &VirtualMachine) -> *mut c_void { + self + } +} + +impl FfiResult<*mut c_char> for *const u8 { + const ERR_VALUE: *mut c_char = core::ptr::null_mut(); + + fn into_output(self, _vm: &VirtualMachine) -> *mut c_char { + self.cast_mut().cast() + } +} + +impl FfiResult for usize { + const ERR_VALUE: isize = -1; + + fn into_output(self, _vm: &VirtualMachine) -> isize { + self.try_into() + .expect("Output value is too large to fit into target type") + } +} + +impl FfiResult for c_long { + const ERR_VALUE: c_long = -1; + + fn into_output(self, _vm: &VirtualMachine) -> c_long { + self + } +} + +impl FfiResult for c_double { + const ERR_VALUE: c_double = -1.0; + + fn into_output(self, _vm: &VirtualMachine) -> c_double { + self + } +} + +impl FfiResult for bool { + const ERR_VALUE: c_int = -1; + + fn into_output(self, _vm: &VirtualMachine) -> c_int { + self as c_int + } +} + +impl FfiResult<()> for PyResult { + const ERR_VALUE: () = (); + + fn into_output(self, vm: &VirtualMachine) { + match self { + Err(err) => vm.set_exception(Some(err)), + } + } +} + +impl FfiResult for PyResult +where + T: FfiResult, +{ + const ERR_VALUE: Output = T::ERR_VALUE; + + fn into_output(self, vm: &VirtualMachine) -> Output { + self.map_or_else( + |err| { + vm.set_exception(Some(err)); + T::ERR_VALUE + }, + |obj| obj.into_output(vm), + ) + } +} diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index 6208bc5ebfe..d535618982f 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -1263,7 +1263,7 @@ impl PyType { } impl Py { - pub(crate) fn is_subtype(&self, other: &Self) -> bool { + pub fn is_subtype(&self, other: &Self) -> bool { is_subtype_with_mro(&self.mro.read(), self, other) } diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 3cfe4642907..e55a68fb805 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -2060,12 +2060,12 @@ impl VirtualMachine { exc } - pub(crate) fn current_exception(&self) -> Option { + pub fn current_exception(&self) -> Option { self.exceptions.borrow().stack.last().cloned().flatten() } /// Set the current exc_info slot value (PUSH_EXC_INFO / POP_EXCEPT). - pub(crate) fn set_exception(&self, exc: Option) { + pub fn set_exception(&self, exc: Option) { // don't be holding the RefCell guard while __del__ is called let mut excs = self.exceptions.borrow_mut(); debug_assert!( @@ -2084,6 +2084,19 @@ impl VirtualMachine { thread::update_thread_exception(self.topmost_exception()); } + pub fn take_raised_exception(&self) -> Option { + let mut excs = self.exceptions.borrow_mut(); + if let Some(top) = excs.stack.last_mut() { + let exc = top.take(); + drop(excs); + #[cfg(feature = "threading")] + thread::update_thread_exception(self.topmost_exception()); + exc + } else { + None + } + } + pub(crate) fn contextualize_exception(&self, exception: &Py) { if let Some(context_exc) = self.topmost_exception() && !context_exc.is(exception) diff --git a/src/lib.rs b/src/lib.rs index a8fd5034d83..d04b63d6572 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,7 +121,7 @@ pub fn run(mut builder: InterpreterBuilder) -> ExitCode { let exitcode = cfg_select! { feature = "capi" => {{ let local_vm = interp.enter(|vm| vm.new_thread()); - *(rustpython_capi::get_main_interpreter()) = Some(interp); + rustpython_capi::set_main_interpreter(interp); let result = local_vm.run(|vm| run_rustpython(vm, run_mode)); rustpython_capi::get_main_interpreter().take().unwrap().finalize(result.err()) }}, From 40d97ea83cf8bfca8c2b50197f775c3354f5e895 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 5 May 2026 20:57:54 +0200 Subject: [PATCH 094/135] Add missing symbols to make tests compile again --- crates/capi/src/lib.rs | 2 + crates/capi/src/object.rs | 83 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 crates/capi/src/object.rs diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 3207149e316..c1bac97cd6b 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -7,6 +7,8 @@ use rustpython_vm::{Context, Interpreter}; use std::sync::MutexGuard; extern crate alloc; + +pub mod object; pub mod pyerrors; pub mod pylifecycle; pub mod pystate; diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs new file mode 100644 index 00000000000..859b9b95cba --- /dev/null +++ b/crates/capi/src/object.rs @@ -0,0 +1,83 @@ +use crate::PyObject; +use crate::pystate::with_vm; +use core::ffi::{c_int, c_uint, c_ulong}; +use rustpython_vm::builtins::PyType; +use rustpython_vm::{AsObject, Context, Py}; + +const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; +const PY_TPFLAGS_LIST_SUBCLASS: c_ulong = 1 << 25; +const PY_TPFLAGS_TUPLE_SUBCLASS: c_ulong = 1 << 26; +const PY_TPFLAGS_BYTES_SUBCLASS: c_ulong = 1 << 27; +const PY_TPFLAGS_UNICODE_SUBCLASS: c_ulong = 1 << 28; +const PY_TPFLAGS_DICT_SUBCLASS: c_ulong = 1 << 29; +const PY_TPFLAGS_BASE_EXC_SUBCLASS: c_ulong = 1 << 30; +const PY_TPFLAGS_TYPE_SUBCLASS: c_ulong = 1 << 31; + +pub type PyTypeObject = Py; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_TYPE(op: *mut PyObject) -> *const PyTypeObject { + unsafe { (*op).class() } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_IS_TYPE(op: *mut PyObject, ty: *mut PyTypeObject) -> c_int { + with_vm(|_vm| { + let obj = unsafe { &*op }; + let ty = unsafe { &*ty }; + obj.class().is(ty) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyType_GetFlags(ptr: *const PyTypeObject) -> c_ulong { + let ctx = Context::genesis(); + let zoo = &ctx.types; + let exp_zoo = &ctx.exceptions; + + let ty = unsafe { &*ptr }; + let mut flags = ty.slots.flags.bits(); + + if ty.is_subtype(zoo.int_type) { + flags |= PY_TPFLAGS_LONG_SUBCLASS; + } + if ty.is_subtype(zoo.list_type) { + flags |= PY_TPFLAGS_LIST_SUBCLASS + } + if ty.is_subtype(zoo.tuple_type) { + flags |= PY_TPFLAGS_TUPLE_SUBCLASS; + } + if ty.is_subtype(zoo.bytes_type) { + flags |= PY_TPFLAGS_BYTES_SUBCLASS; + } + if ty.is_subtype(zoo.str_type) { + flags |= PY_TPFLAGS_UNICODE_SUBCLASS; + } + if ty.is_subtype(zoo.dict_type) { + flags |= PY_TPFLAGS_DICT_SUBCLASS; + } + if ty.is_subtype(exp_zoo.base_exception_type) { + flags |= PY_TPFLAGS_BASE_EXC_SUBCLASS; + } + if ty.is_subtype(zoo.type_type) { + flags |= PY_TPFLAGS_TYPE_SUBCLASS; + } + + flags +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject { + with_vm(|vm| { + let ctx = &vm.ctx; + match constant_id { + 0 => ctx.none.as_object(), + 1 => ctx.false_value.as_object(), + 2 => ctx.true_value.as_object(), + 3 => ctx.ellipsis.as_object(), + 4 => ctx.not_implemented.as_object(), + _ => panic!("Invalid constant_id passed to Py_GetConstantBorrowed"), + } + .as_raw() + }) +} From 6b79cb5e06e56a5b4a5b6901f67fdbda60ee2e19 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 5 May 2026 21:02:48 +0200 Subject: [PATCH 095/135] Add `pyerrors` to dictionary --- .cspell.dict/cpython.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.cspell.dict/cpython.txt b/.cspell.dict/cpython.txt index 3bbe7426c74..1fb1c56e029 100644 --- a/.cspell.dict/cpython.txt +++ b/.cspell.dict/cpython.txt @@ -157,6 +157,7 @@ pybuilddir pycore pyinner pydecimal +pyerrors Pyfunc pylifecycle pymain From 71438c698f5c25303982c4a81c50bf3e3816fa51 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 5 May 2026 21:10:40 +0200 Subject: [PATCH 096/135] Return exception in `Py_GetConstantBorrowed` --- crates/capi/src/object.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 859b9b95cba..043eb16e468 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -70,14 +70,19 @@ pub unsafe extern "C" fn PyType_GetFlags(ptr: *const PyTypeObject) -> c_ulong { pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject { with_vm(|vm| { let ctx = &vm.ctx; - match constant_id { + let constant = match constant_id { 0 => ctx.none.as_object(), 1 => ctx.false_value.as_object(), 2 => ctx.true_value.as_object(), 3 => ctx.ellipsis.as_object(), 4 => ctx.not_implemented.as_object(), - _ => panic!("Invalid constant_id passed to Py_GetConstantBorrowed"), + _ => { + return Err( + vm.new_system_error("Invalid constant ID passed to Py_GetConstantBorrowed") + ); + } } - .as_raw() + .as_raw(); + Ok(constant) }) } From d708bfe52f35524610de622eb6ffd401311df399 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 5 May 2026 21:17:05 +0200 Subject: [PATCH 097/135] Remove `allow(dead_code)` --- crates/capi/src/pystate.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index ecbff21713d..00ad5c41e4a 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -7,7 +7,6 @@ use rustpython_vm::vm::thread::{ CurrentVmAttachState, attach_current_thread, release_current_thread, with_current_vm, }; -#[allow(dead_code)] pub(crate) fn with_vm, O>(f: impl FnOnce(&VirtualMachine) -> R) -> O { with_current_vm(|vm| f(vm).into_output(vm)) } From bbf365658b18722d8f5fca7235c51c6188b74508 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 5 May 2026 21:43:52 +0200 Subject: [PATCH 098/135] Fix windows --- crates/capi/src/object.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 043eb16e468..4b4fa70840d 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -4,14 +4,14 @@ use core::ffi::{c_int, c_uint, c_ulong}; use rustpython_vm::builtins::PyType; use rustpython_vm::{AsObject, Context, Py}; -const PY_TPFLAGS_LONG_SUBCLASS: c_ulong = 1 << 24; -const PY_TPFLAGS_LIST_SUBCLASS: c_ulong = 1 << 25; -const PY_TPFLAGS_TUPLE_SUBCLASS: c_ulong = 1 << 26; -const PY_TPFLAGS_BYTES_SUBCLASS: c_ulong = 1 << 27; -const PY_TPFLAGS_UNICODE_SUBCLASS: c_ulong = 1 << 28; -const PY_TPFLAGS_DICT_SUBCLASS: c_ulong = 1 << 29; -const PY_TPFLAGS_BASE_EXC_SUBCLASS: c_ulong = 1 << 30; -const PY_TPFLAGS_TYPE_SUBCLASS: c_ulong = 1 << 31; +const PY_TPFLAGS_LONG_SUBCLASS: u64 = 1 << 24; +const PY_TPFLAGS_LIST_SUBCLASS: u64 = 1 << 25; +const PY_TPFLAGS_TUPLE_SUBCLASS: u64 = 1 << 26; +const PY_TPFLAGS_BYTES_SUBCLASS: u64 = 1 << 27; +const PY_TPFLAGS_UNICODE_SUBCLASS: u64 = 1 << 28; +const PY_TPFLAGS_DICT_SUBCLASS: u64 = 1 << 29; +const PY_TPFLAGS_BASE_EXC_SUBCLASS: u64 = 1 << 30; +const PY_TPFLAGS_TYPE_SUBCLASS: u64 = 1 << 31; pub type PyTypeObject = Py; @@ -63,7 +63,7 @@ pub unsafe extern "C" fn PyType_GetFlags(ptr: *const PyTypeObject) -> c_ulong { flags |= PY_TPFLAGS_TYPE_SUBCLASS; } - flags + flags as c_ulong } #[unsafe(no_mangle)] From bccd38e9816d4e4a8181b1bfc2d29fc90861044a Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 5 May 2026 22:21:18 +0200 Subject: [PATCH 099/135] Load stdlib when calling `Py_InitializeEx` --- Cargo.lock | 1 + crates/capi/Cargo.toml | 5 +---- crates/capi/src/pylifecycle.rs | 22 ++++++++++++++++++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1e7a3204d4..8fd9cc02fde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3157,6 +3157,7 @@ name = "rustpython-capi" version = "0.5.0" dependencies = [ "pyo3", + "rustpython-pylib", "rustpython-stdlib", "rustpython-vm", ] diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index a090aaedaf3..c6c08ccbc05 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -17,10 +17,7 @@ rustpython-stdlib = {workspace = true, features = ["threading"] } [dev-dependencies] pyo3 = { version = "0.28", features = ["auto-initialize", "abi3"] } +rustpython-pylib = { workspace = true, features = ["freeze-stdlib"] } [lints] workspace = true - -[package.metadata.cargo-shear] -# Not a direct dependency (yet), but we need to enable threading support in the stdlib. -ignored = ["rustpython-stdlib"] \ No newline at end of file diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 6760b2822a3..10c0df5fce9 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -3,7 +3,7 @@ use crate::pyerrors::init_exception_statics; use crate::pystate::ensure_thread_has_vm_attached; use core::ffi::c_int; use rustpython_vm::vm::thread::ThreadedVirtualMachine; -use rustpython_vm::{Context, Interpreter}; +use rustpython_vm::{Context, Interpreter, Settings}; use std::sync::Mutex; pub(crate) static MAIN_INTERP: Mutex> = Mutex::new(None); @@ -32,7 +32,25 @@ pub extern "C" fn Py_InitializeEx(_initsigs: c_int) { if interp.is_none() { // Safety: Interpreter was not initialized before, so we can safely assume the statics are not used unsafe { init_exception_statics(&Context::genesis().exceptions) }; - *interp = Interpreter::with_init(Default::default(), |_vm| {}).into(); + + let settings = Settings::default(); + let mut builder = Interpreter::builder(settings); + + let defs = rustpython_stdlib::stdlib_module_defs(&builder.ctx); + builder = builder.add_native_modules(&defs); + + #[cfg(test)] + { + use rustpython_vm::common::rc::PyRc; + builder = builder + .add_frozen_modules(rustpython_pylib::FROZEN_STDLIB) + .init_hook(|vm| { + let state = PyRc::get_mut(&mut vm.state).unwrap(); + state.config.paths.stdlib_dir = Some(rustpython_pylib::LIB_PATH.to_owned()); + }); + } + + *interp = Some(builder.build()); drop(interp); ensure_thread_has_vm_attached(); } From 9e20cf7be626f4bbc6b7c9478dc0e43aa8c65c2f Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 5 May 2026 22:49:11 +0200 Subject: [PATCH 100/135] Debug tests --- .github/workflows/ci.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2db52bb04e3..f0ed29f9d35 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -112,10 +112,14 @@ jobs: uses: ./.github/actions/install-macos-deps - name: run rust tests - run: cargo test --workspace ${{ env.WORKSPACE_EXCLUDES }} --features threading ${{ env.CARGO_ARGS }} + run: cargo test --workspace --exclude rustpython-capi ${{ env.WORKSPACE_EXCLUDES }} --features threading ${{ env.CARGO_ARGS }} env: INSTA_WORKSPACE_ROOT: ${{ github.workspace }} + - name: run c-api tests + working-directory: crates/capi + run: cargo test + - run: cargo doc --locked if: runner.os == 'Linux' From 268856b7fab6ae804aa03c7d35802029a6cdc981 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 6 May 2026 09:08:56 +0200 Subject: [PATCH 101/135] Revert "Load stdlib when calling `Py_InitializeEx`" This reverts commit bccd38e9816d4e4a8181b1bfc2d29fc90861044a. --- Cargo.lock | 1 - crates/capi/Cargo.toml | 5 ++++- crates/capi/src/pylifecycle.rs | 22 ++-------------------- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8fd9cc02fde..b1e7a3204d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3157,7 +3157,6 @@ name = "rustpython-capi" version = "0.5.0" dependencies = [ "pyo3", - "rustpython-pylib", "rustpython-stdlib", "rustpython-vm", ] diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index c6c08ccbc05..a090aaedaf3 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -17,7 +17,10 @@ rustpython-stdlib = {workspace = true, features = ["threading"] } [dev-dependencies] pyo3 = { version = "0.28", features = ["auto-initialize", "abi3"] } -rustpython-pylib = { workspace = true, features = ["freeze-stdlib"] } [lints] workspace = true + +[package.metadata.cargo-shear] +# Not a direct dependency (yet), but we need to enable threading support in the stdlib. +ignored = ["rustpython-stdlib"] \ No newline at end of file diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 10c0df5fce9..6760b2822a3 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -3,7 +3,7 @@ use crate::pyerrors::init_exception_statics; use crate::pystate::ensure_thread_has_vm_attached; use core::ffi::c_int; use rustpython_vm::vm::thread::ThreadedVirtualMachine; -use rustpython_vm::{Context, Interpreter, Settings}; +use rustpython_vm::{Context, Interpreter}; use std::sync::Mutex; pub(crate) static MAIN_INTERP: Mutex> = Mutex::new(None); @@ -32,25 +32,7 @@ pub extern "C" fn Py_InitializeEx(_initsigs: c_int) { if interp.is_none() { // Safety: Interpreter was not initialized before, so we can safely assume the statics are not used unsafe { init_exception_statics(&Context::genesis().exceptions) }; - - let settings = Settings::default(); - let mut builder = Interpreter::builder(settings); - - let defs = rustpython_stdlib::stdlib_module_defs(&builder.ctx); - builder = builder.add_native_modules(&defs); - - #[cfg(test)] - { - use rustpython_vm::common::rc::PyRc; - builder = builder - .add_frozen_modules(rustpython_pylib::FROZEN_STDLIB) - .init_hook(|vm| { - let state = PyRc::get_mut(&mut vm.state).unwrap(); - state.config.paths.stdlib_dir = Some(rustpython_pylib::LIB_PATH.to_owned()); - }); - } - - *interp = Some(builder.build()); + *interp = Interpreter::with_init(Default::default(), |_vm| {}).into(); drop(interp); ensure_thread_has_vm_attached(); } From 8432c1783af996b75974303f9f916c76945d51a5 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 6 May 2026 09:30:47 +0200 Subject: [PATCH 102/135] Disable tests on windows for now --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f0ed29f9d35..24a1a4a86a7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -119,6 +119,7 @@ jobs: - name: run c-api tests working-directory: crates/capi run: cargo test + if: runner.os != 'Windows' # Requires pyo3 0.29+ on Windows - run: cargo doc --locked if: runner.os == 'Linux' From 130321e36fb27c867929bd23685f7e3c53e619db Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 6 May 2026 11:44:31 +0200 Subject: [PATCH 103/135] Truncate `PyType_GetFlags` to be always 32 bits --- crates/capi/src/object.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 4b4fa70840d..878b31bdba6 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -4,14 +4,14 @@ use core::ffi::{c_int, c_uint, c_ulong}; use rustpython_vm::builtins::PyType; use rustpython_vm::{AsObject, Context, Py}; -const PY_TPFLAGS_LONG_SUBCLASS: u64 = 1 << 24; -const PY_TPFLAGS_LIST_SUBCLASS: u64 = 1 << 25; -const PY_TPFLAGS_TUPLE_SUBCLASS: u64 = 1 << 26; -const PY_TPFLAGS_BYTES_SUBCLASS: u64 = 1 << 27; -const PY_TPFLAGS_UNICODE_SUBCLASS: u64 = 1 << 28; -const PY_TPFLAGS_DICT_SUBCLASS: u64 = 1 << 29; -const PY_TPFLAGS_BASE_EXC_SUBCLASS: u64 = 1 << 30; -const PY_TPFLAGS_TYPE_SUBCLASS: u64 = 1 << 31; +const PY_TPFLAGS_LONG_SUBCLASS: u32 = 1 << 24; +const PY_TPFLAGS_LIST_SUBCLASS: u32 = 1 << 25; +const PY_TPFLAGS_TUPLE_SUBCLASS: u32 = 1 << 26; +const PY_TPFLAGS_BYTES_SUBCLASS: u32 = 1 << 27; +const PY_TPFLAGS_UNICODE_SUBCLASS: u32 = 1 << 28; +const PY_TPFLAGS_DICT_SUBCLASS: u32 = 1 << 29; +const PY_TPFLAGS_BASE_EXC_SUBCLASS: u32 = 1 << 30; +const PY_TPFLAGS_TYPE_SUBCLASS: u32 = 1 << 31; pub type PyTypeObject = Py; @@ -36,7 +36,7 @@ pub unsafe extern "C" fn PyType_GetFlags(ptr: *const PyTypeObject) -> c_ulong { let exp_zoo = &ctx.exceptions; let ty = unsafe { &*ptr }; - let mut flags = ty.slots.flags.bits(); + let mut flags = ty.slots.flags.bits() as u32; if ty.is_subtype(zoo.int_type) { flags |= PY_TPFLAGS_LONG_SUBCLASS; From 30ac0610b1ffe627c9d8cbf2480d2ab4246ee539 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 6 May 2026 12:02:58 +0200 Subject: [PATCH 104/135] Add test for exception type checking --- crates/capi/src/pyerrors.rs | 8 ++++++++ crates/capi/src/pystate.rs | 3 +++ 2 files changed, 11 insertions(+) diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index ec5578e48fa..4554757c66e 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -266,4 +266,12 @@ mod tests { assert!(!PyErr::occurred(py)); }) } + + #[test] + fn test_error_is_instance() { + Python::attach(|py| { + let err = PyTypeError::new_err(py.None()); + assert!(err.is_instance_of::(py)); + }) + } } diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index 00ad5c41e4a..97b29bcebe1 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -47,6 +47,9 @@ pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState { ptr::null_mut() } +#[unsafe(no_mangle)] +pub extern "C" fn PyEval_RestoreThread(_state: *mut PyThreadState) {} + #[cfg(test)] mod tests { use crate::get_main_interpreter; From 4e4f7df6bd267bad857cc81e40d82173ec80573b Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 6 May 2026 16:35:21 +0200 Subject: [PATCH 105/135] Remove subclass type flags --- crates/capi/src/object.rs | 44 ++------------------------------------- 1 file changed, 2 insertions(+), 42 deletions(-) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 878b31bdba6..fb8fc3de54a 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -2,16 +2,7 @@ use crate::PyObject; use crate::pystate::with_vm; use core::ffi::{c_int, c_uint, c_ulong}; use rustpython_vm::builtins::PyType; -use rustpython_vm::{AsObject, Context, Py}; - -const PY_TPFLAGS_LONG_SUBCLASS: u32 = 1 << 24; -const PY_TPFLAGS_LIST_SUBCLASS: u32 = 1 << 25; -const PY_TPFLAGS_TUPLE_SUBCLASS: u32 = 1 << 26; -const PY_TPFLAGS_BYTES_SUBCLASS: u32 = 1 << 27; -const PY_TPFLAGS_UNICODE_SUBCLASS: u32 = 1 << 28; -const PY_TPFLAGS_DICT_SUBCLASS: u32 = 1 << 29; -const PY_TPFLAGS_BASE_EXC_SUBCLASS: u32 = 1 << 30; -const PY_TPFLAGS_TYPE_SUBCLASS: u32 = 1 << 31; +use rustpython_vm::{AsObject, Py}; pub type PyTypeObject = Py; @@ -31,39 +22,8 @@ pub unsafe extern "C" fn Py_IS_TYPE(op: *mut PyObject, ty: *mut PyTypeObject) -> #[unsafe(no_mangle)] pub unsafe extern "C" fn PyType_GetFlags(ptr: *const PyTypeObject) -> c_ulong { - let ctx = Context::genesis(); - let zoo = &ctx.types; - let exp_zoo = &ctx.exceptions; - let ty = unsafe { &*ptr }; - let mut flags = ty.slots.flags.bits() as u32; - - if ty.is_subtype(zoo.int_type) { - flags |= PY_TPFLAGS_LONG_SUBCLASS; - } - if ty.is_subtype(zoo.list_type) { - flags |= PY_TPFLAGS_LIST_SUBCLASS - } - if ty.is_subtype(zoo.tuple_type) { - flags |= PY_TPFLAGS_TUPLE_SUBCLASS; - } - if ty.is_subtype(zoo.bytes_type) { - flags |= PY_TPFLAGS_BYTES_SUBCLASS; - } - if ty.is_subtype(zoo.str_type) { - flags |= PY_TPFLAGS_UNICODE_SUBCLASS; - } - if ty.is_subtype(zoo.dict_type) { - flags |= PY_TPFLAGS_DICT_SUBCLASS; - } - if ty.is_subtype(exp_zoo.base_exception_type) { - flags |= PY_TPFLAGS_BASE_EXC_SUBCLASS; - } - if ty.is_subtype(zoo.type_type) { - flags |= PY_TPFLAGS_TYPE_SUBCLASS; - } - - flags as c_ulong + ty.slots.flags.bits() as u32 as c_ulong } #[unsafe(no_mangle)] From 9bf8c2529c4a8c228373832191a7e2a3ce75d20f Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 9 May 2026 14:40:42 +0200 Subject: [PATCH 106/135] Update PyO3 to main branch --- Cargo.lock | 10 +++++----- Cargo.toml | 4 ++-- crates/capi/Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c11a81ea571..8158c80aadf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2683,7 +2683,7 @@ dependencies = [ [[package]] name = "pyo3" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#68bfc26d86b9d3e9c0d4fcc6a43663774ede66d6" +source = "git+https://github.com/PyO3/pyo3#eef62776f3e5cf32505b77b69dacaecf2a77caf2" dependencies = [ "libc", "once_cell", @@ -2696,7 +2696,7 @@ dependencies = [ [[package]] name = "pyo3-build-config" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#68bfc26d86b9d3e9c0d4fcc6a43663774ede66d6" +source = "git+https://github.com/PyO3/pyo3#eef62776f3e5cf32505b77b69dacaecf2a77caf2" dependencies = [ "target-lexicon", ] @@ -2704,7 +2704,7 @@ dependencies = [ [[package]] name = "pyo3-ffi" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#68bfc26d86b9d3e9c0d4fcc6a43663774ede66d6" +source = "git+https://github.com/PyO3/pyo3#eef62776f3e5cf32505b77b69dacaecf2a77caf2" dependencies = [ "libc", "pyo3-build-config", @@ -2713,7 +2713,7 @@ dependencies = [ [[package]] name = "pyo3-macros" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#68bfc26d86b9d3e9c0d4fcc6a43663774ede66d6" +source = "git+https://github.com/PyO3/pyo3#eef62776f3e5cf32505b77b69dacaecf2a77caf2" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2724,7 +2724,7 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" version = "0.28.3" -source = "git+https://github.com/bschoenmaeckers/pyo3?branch=RustPython#68bfc26d86b9d3e9c0d4fcc6a43663774ede66d6" +source = "git+https://github.com/PyO3/pyo3#eef62776f3e5cf32505b77b69dacaecf2a77caf2" dependencies = [ "heck", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 2fe28b0fa83..b616d0485ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ rustyline = { workspace = true } [dev-dependencies] criterion = { workspace = true } -pyo3 = { git = "https://github.com/bschoenmaeckers/pyo3", branch = "RustPython", features = ["auto-initialize"] } +pyo3 = { git = "https://github.com/PyO3/pyo3", features = ["auto-initialize"] } rustpython-stdlib = { workspace = true } ruff_python_parser = { workspace = true } @@ -101,7 +101,7 @@ lto = "thin" [patch.crates-io] parking_lot_core = { git = "https://github.com/youknowone/parking_lot", branch = "rustpython" } -pyo3-ffi = { git = "https://github.com/bschoenmaeckers/pyo3", branch = "RustPython" } +pyo3-ffi = { git = "https://github.com/PyO3/pyo3" } # REDOX START, Uncomment when you want to compile/check with redoxer # REDOX END diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index 744a3abb525..e7d5bc6f7eb 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -21,7 +21,7 @@ rustpython-vm = { workspace = true, features = ["threading", "compiler"]} nightly = [] [dev-dependencies] -pyo3 = { git = "https://github.com/bschoenmaeckers/pyo3", branch = "RustPython", features = ["auto-initialize", "abi3"] } +pyo3 = { git = "https://github.com/PyO3/pyo3", features = ["auto-initialize", "abi3"] } [lints] workspace = true From a5b3e87dbf30fa2954a43aa599ec946262fa047b Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 9 May 2026 15:06:24 +0200 Subject: [PATCH 107/135] Add type check functions --- crates/capi/src/bytesobject.rs | 3 +++ crates/capi/src/complexobject.rs | 3 +++ crates/capi/src/dictobject.rs | 6 ++++++ crates/capi/src/floatobject.rs | 3 +++ crates/capi/src/listobject.rs | 3 +++ crates/capi/src/longobject.rs | 3 +++ crates/capi/src/methodobject.rs | 3 +++ crates/capi/src/moduleobject.rs | 3 +++ crates/capi/src/object.rs | 17 ++++++++--------- crates/capi/src/pyerrors.rs | 3 +++ crates/capi/src/traceback.rs | 3 +++ crates/capi/src/tupleobject.rs | 3 +++ crates/capi/src/unicodeobject.rs | 3 +++ 13 files changed, 47 insertions(+), 9 deletions(-) diff --git a/crates/capi/src/bytesobject.rs b/crates/capi/src/bytesobject.rs index d2028574de1..b1cb9420ea8 100644 --- a/crates/capi/src/bytesobject.rs +++ b/crates/capi/src/bytesobject.rs @@ -1,7 +1,10 @@ +use crate::object::define_py_check; use crate::{PyObject, with_vm}; use core::ffi::c_char; use rustpython_vm::builtins::PyBytes; +define_py_check!(PyBytes_Check, types.bytes_type); + #[unsafe(no_mangle)] pub extern "C" fn PyBytes_FromStringAndSize(bytes: *mut c_char, len: isize) -> *mut PyObject { with_vm(|vm| { diff --git a/crates/capi/src/complexobject.rs b/crates/capi/src/complexobject.rs index 663c7264e76..eb416033b26 100644 --- a/crates/capi/src/complexobject.rs +++ b/crates/capi/src/complexobject.rs @@ -1,9 +1,12 @@ +use crate::object::define_py_check; use crate::{PyObject, with_vm}; use core::ffi::c_double; use num_complex::{Complex, Complex64}; use rustpython_vm::builtins::PyComplex; use rustpython_vm::{PyResult, VirtualMachine}; +define_py_check!(PyComplex_Check, types.complex_type); + #[unsafe(no_mangle)] pub extern "C" fn PyComplex_FromDoubles(real: c_double, imag: c_double) -> *mut PyObject { with_vm(|vm| vm.ctx.new_complex(Complex::new(real, imag))) diff --git a/crates/capi/src/dictobject.rs b/crates/capi/src/dictobject.rs index b36d30516c7..90350fd06b1 100644 --- a/crates/capi/src/dictobject.rs +++ b/crates/capi/src/dictobject.rs @@ -1,9 +1,15 @@ use crate::PyObject; +use crate::object::define_py_check; use crate::pystate::with_vm; use core::ffi::c_int; use rustpython_vm::AsObject; use rustpython_vm::builtins::PyDict; +define_py_check!(PyDict_Check, types.dict_type); +define_py_check!(PyDictKeys_Check, types.dict_keys_type); +define_py_check!(PyDictValues_Check, types.dict_values_type); +define_py_check!(PyDictItems_Check, types.dict_items_type); + #[unsafe(no_mangle)] pub extern "C" fn PyDict_New() -> *mut PyObject { with_vm(|vm| vm.ctx.new_dict()) diff --git a/crates/capi/src/floatobject.rs b/crates/capi/src/floatobject.rs index 5c0284408f6..de1f46b23fe 100644 --- a/crates/capi/src/floatobject.rs +++ b/crates/capi/src/floatobject.rs @@ -1,7 +1,10 @@ +use crate::object::define_py_check; use crate::{PyObject, with_vm}; use core::ffi::c_double; use rustpython_vm::builtins::PyFloat; +define_py_check!(PyFloat_Check, types.float_type); + #[unsafe(no_mangle)] pub extern "C" fn PyFloat_FromDouble(value: c_double) -> *mut PyObject { with_vm(|vm| vm.ctx.new_float(value)) diff --git a/crates/capi/src/listobject.rs b/crates/capi/src/listobject.rs index 5e0ca5d18d7..f940f15cf02 100644 --- a/crates/capi/src/listobject.rs +++ b/crates/capi/src/listobject.rs @@ -1,10 +1,13 @@ use crate::PyObject; +use crate::object::define_py_check; use crate::pystate::with_vm; use core::ffi::c_int; use core::ptr::NonNull; use rustpython_vm::PyObjectRef; use rustpython_vm::builtins::PyList; +define_py_check!(PyList_Check, types.list_type); + #[unsafe(no_mangle)] pub extern "C" fn PyList_New(size: isize) -> *mut PyObject { with_vm(|vm| vm.ctx.new_list(Vec::with_capacity(size as usize))) diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index a8e7341fed1..e838265cb69 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -1,8 +1,11 @@ +use crate::object::define_py_check; use crate::{PyObject, with_vm}; use core::ffi::{c_long, c_longlong, c_ulong, c_ulonglong}; use rustpython_vm::PyResult; use rustpython_vm::builtins::PyInt; +define_py_check!(PyLong_Check, types.int_type); + #[unsafe(no_mangle)] pub extern "C" fn PyLong_FromLong(value: c_long) -> *mut PyObject { with_vm(|vm| vm.ctx.new_int(value)) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 33a3cb003f0..81c6afd8916 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -1,11 +1,14 @@ use crate::PyObject; use crate::object::PyTypeObject; +use crate::object::define_py_check; use crate::pystate::with_vm; use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; use rustpython_vm::function::{FuncArgs, HeapMethodDef, PyMethodFlags}; use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; +define_py_check!(PyCFunction_Check, types.builtin_function_or_method_type); + type PyCFunction = unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject; type PyCFunctionWithKeywords = unsafe extern "C" fn( slf: *mut PyObject, diff --git a/crates/capi/src/moduleobject.rs b/crates/capi/src/moduleobject.rs index 2a126de75ba..fee9868c7d8 100644 --- a/crates/capi/src/moduleobject.rs +++ b/crates/capi/src/moduleobject.rs @@ -1,10 +1,13 @@ use crate::PyObject; use crate::methodobject::PyMethodDef; +use crate::object::define_py_check; use crate::pystate::with_vm; use rustpython_vm::AsObject; use rustpython_vm::builtins::PyModule; use std::ffi::{CStr, c_char, c_int, c_void}; +define_py_check!(PyModule_Check, types.module_type); + const PY_MOD_CREATE: c_int = 1; const PY_MOD_EXEC: c_int = 2; const PY_MOD_GIL: c_int = 4; diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index c2bfaef5759..52b24306991 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -20,23 +20,22 @@ const PY_TPFLAGS_TYPE_SUBCLASS: c_ulong = 1 << 31; pub type PyTypeObject = Py; macro_rules! define_py_check { - ($name:ident, $type_name:ident) => { + ($name:ident, $($ctx_path:ident).+) => { #[unsafe(no_mangle)] - pub extern "C" fn $name(obj: *mut PyObject) -> c_int { - with_vm(|vm| unsafe { (*obj).is_instance(vm.ctx.types.$type_name.as_object(), vm) }) + pub unsafe extern "C" fn $name(obj: *mut PyObject) -> core::ffi::c_int { + with_vm(|vm| unsafe { (&*obj).class().is_subtype(vm.ctx.$($ctx_path).+) }) } }; - (exact $name:ident, $type_name:ident) => { + (exact $name:ident, $($ctx_path:ident).+) => { #[unsafe(no_mangle)] - pub extern "C" fn $name(obj: *mut PyObject) -> c_int { - with_vm(|vm| unsafe { (*obj).class().is(vm.ctx.types.$type_name) }) + pub unsafe extern "C" fn $name(obj: *mut PyObject) -> core::ffi::c_int { + with_vm(|vm| unsafe { (&*obj).class().is(vm.ctx.$($ctx_path).+) }) } }; } -define_py_check!(PyFloat_Check, float_type); -define_py_check!(PyModule_Check, module_type); -define_py_check!(PyBool_Check, bool_type); +pub(crate) use define_py_check; +define_py_check!(PyType_Check, types.type_type); #[unsafe(no_mangle)] pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const PyTypeObject { diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index eb86559f846..23065b1987f 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -1,3 +1,4 @@ +use crate::object::define_py_check; use crate::{PyObject, with_vm}; use core::convert::Infallible; use core::ffi::{CStr, c_char, c_int}; @@ -95,6 +96,8 @@ define_exception_statics! { PyExc_EncodingWarning => encoding_warning, } +define_py_check!(PyExceptionInstance_Check, exceptions.base_exception_type); + #[unsafe(no_mangle)] pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject { with_vm(|vm| vm.take_raised_exception()) diff --git a/crates/capi/src/traceback.rs b/crates/capi/src/traceback.rs index a0741f99be0..71228f4aee6 100644 --- a/crates/capi/src/traceback.rs +++ b/crates/capi/src/traceback.rs @@ -1,8 +1,11 @@ use crate::PyObject; +use crate::object::define_py_check; use crate::pystate::with_vm; use core::ffi::c_int; use rustpython_vm::function::{FuncArgs, KwArgs}; +define_py_check!(exact PyTraceBack_Check, types.traceback_type); + #[unsafe(no_mangle)] pub extern "C" fn PyTraceBack_Print(tb: *mut PyObject, file: *mut PyObject) -> c_int { with_vm(|vm| { diff --git a/crates/capi/src/tupleobject.rs b/crates/capi/src/tupleobject.rs index 4c76d2cb00e..98305ba1dd1 100644 --- a/crates/capi/src/tupleobject.rs +++ b/crates/capi/src/tupleobject.rs @@ -1,8 +1,11 @@ use crate::PyObject; +use crate::object::define_py_check; use crate::pystate::with_vm; use rustpython_vm::PyResult; use rustpython_vm::builtins::PyTuple; +define_py_check!(PyTuple_Check, types.tuple_type); + #[unsafe(no_mangle)] pub extern "C" fn PyTuple_New(len: isize) -> *mut PyObject { with_vm(|vm| { diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index 8e1ba55f006..b6bcda7a47a 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -1,3 +1,4 @@ +use crate::object::define_py_check; use crate::{PyObject, with_vm}; use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; @@ -6,6 +7,8 @@ use core::str; use rustpython_vm::PyObjectRef; use rustpython_vm::builtins::PyStr; +define_py_check!(PyUnicode_Check, types.str_type); + #[unsafe(no_mangle)] pub extern "C" fn PyUnicode_FromStringAndSize(s: *const c_char, len: isize) -> *mut PyObject { let len = usize::try_from(len).expect("PyUnicode_FromStringAndSize called with negative len"); From 84f87d66fe4a147907ac56301d8254e24e28485c Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 9 May 2026 15:13:22 +0200 Subject: [PATCH 108/135] Add bool object functions --- crates/capi/src/boolobject.rs | 36 +++++++++++++++++++++++++++++++++++ crates/capi/src/lib.rs | 1 + 2 files changed, 37 insertions(+) create mode 100644 crates/capi/src/boolobject.rs diff --git a/crates/capi/src/boolobject.rs b/crates/capi/src/boolobject.rs new file mode 100644 index 00000000000..e5b01ec8a5c --- /dev/null +++ b/crates/capi/src/boolobject.rs @@ -0,0 +1,36 @@ +use crate::object::define_py_check; +use crate::{PyObject, pystate::with_vm}; +use core::ffi::{c_int, c_long}; +use rustpython_vm::AsObject; + +define_py_check!(PyBool_Check, types.bool_type); + +#[unsafe(no_mangle)] +pub extern "C" fn Py_IsTrue(obj: *mut PyObject) -> c_int { + with_vm(|vm| unsafe { + obj.as_ref() + .map(|obj| obj.is(&vm.ctx.true_value)) + .unwrap_or_default() + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_IsFalse(obj: *mut PyObject) -> c_int { + with_vm(|vm| unsafe { + obj.as_ref() + .map(|obj| obj.is(&vm.ctx.false_value)) + .unwrap_or_default() + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyBool_FromLong(value: c_long) -> *mut PyObject { + with_vm(|vm| { + if value == 0 { + &vm.ctx.false_value + } else { + &vm.ctx.true_value + } + .to_owned() + }) +} diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 68c1a5ad28a..39d374a3210 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -5,6 +5,7 @@ pub use rustpython_vm::PyObject; extern crate alloc; pub(crate) mod abstract_; +pub(crate) mod boolobject; pub(crate) mod bytesobject; pub(crate) mod capsule; pub(crate) mod ceval; From 9306c6613c857ae5a387f9a68e06911a42d528f4 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 9 May 2026 15:39:28 +0200 Subject: [PATCH 109/135] Add exact type check macros --- crates/capi/src/bytesobject.rs | 1 + crates/capi/src/complexobject.rs | 1 + crates/capi/src/dictobject.rs | 1 + crates/capi/src/floatobject.rs | 1 + crates/capi/src/listobject.rs | 1 + crates/capi/src/longobject.rs | 1 + crates/capi/src/methodobject.rs | 1 + crates/capi/src/moduleobject.rs | 1 + crates/capi/src/object.rs | 16 ++++++++++++++-- crates/capi/src/tupleobject.rs | 1 + crates/capi/src/unicodeobject.rs | 1 + 11 files changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/capi/src/bytesobject.rs b/crates/capi/src/bytesobject.rs index b1cb9420ea8..5d58ae36566 100644 --- a/crates/capi/src/bytesobject.rs +++ b/crates/capi/src/bytesobject.rs @@ -4,6 +4,7 @@ use core::ffi::c_char; use rustpython_vm::builtins::PyBytes; define_py_check!(PyBytes_Check, types.bytes_type); +define_py_check!(exact PyBytes_CheckExact, types.bytes_type); #[unsafe(no_mangle)] pub extern "C" fn PyBytes_FromStringAndSize(bytes: *mut c_char, len: isize) -> *mut PyObject { diff --git a/crates/capi/src/complexobject.rs b/crates/capi/src/complexobject.rs index eb416033b26..45225b72a8a 100644 --- a/crates/capi/src/complexobject.rs +++ b/crates/capi/src/complexobject.rs @@ -6,6 +6,7 @@ use rustpython_vm::builtins::PyComplex; use rustpython_vm::{PyResult, VirtualMachine}; define_py_check!(PyComplex_Check, types.complex_type); +define_py_check!(exact PyComplex_CheckExact, types.complex_type); #[unsafe(no_mangle)] pub extern "C" fn PyComplex_FromDoubles(real: c_double, imag: c_double) -> *mut PyObject { diff --git a/crates/capi/src/dictobject.rs b/crates/capi/src/dictobject.rs index 90350fd06b1..76849476120 100644 --- a/crates/capi/src/dictobject.rs +++ b/crates/capi/src/dictobject.rs @@ -6,6 +6,7 @@ use rustpython_vm::AsObject; use rustpython_vm::builtins::PyDict; define_py_check!(PyDict_Check, types.dict_type); +define_py_check!(exact PyDict_CheckExact, types.dict_type); define_py_check!(PyDictKeys_Check, types.dict_keys_type); define_py_check!(PyDictValues_Check, types.dict_values_type); define_py_check!(PyDictItems_Check, types.dict_items_type); diff --git a/crates/capi/src/floatobject.rs b/crates/capi/src/floatobject.rs index de1f46b23fe..e37124df2fb 100644 --- a/crates/capi/src/floatobject.rs +++ b/crates/capi/src/floatobject.rs @@ -4,6 +4,7 @@ use core::ffi::c_double; use rustpython_vm::builtins::PyFloat; define_py_check!(PyFloat_Check, types.float_type); +define_py_check!(exact PyFloat_CheckExact, types.float_type); #[unsafe(no_mangle)] pub extern "C" fn PyFloat_FromDouble(value: c_double) -> *mut PyObject { diff --git a/crates/capi/src/listobject.rs b/crates/capi/src/listobject.rs index f940f15cf02..7cf4d15f413 100644 --- a/crates/capi/src/listobject.rs +++ b/crates/capi/src/listobject.rs @@ -7,6 +7,7 @@ use rustpython_vm::PyObjectRef; use rustpython_vm::builtins::PyList; define_py_check!(PyList_Check, types.list_type); +define_py_check!(exact PyList_CheckExact, types.list_type); #[unsafe(no_mangle)] pub extern "C" fn PyList_New(size: isize) -> *mut PyObject { diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index e838265cb69..da61cca706f 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -5,6 +5,7 @@ use rustpython_vm::PyResult; use rustpython_vm::builtins::PyInt; define_py_check!(PyLong_Check, types.int_type); +define_py_check!(exact PyLong_CheckExact, types.int_type); #[unsafe(no_mangle)] pub extern "C" fn PyLong_FromLong(value: c_long) -> *mut PyObject { diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 81c6afd8916..a9031dbaa88 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -8,6 +8,7 @@ use rustpython_vm::function::{FuncArgs, HeapMethodDef, PyMethodFlags}; use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; define_py_check!(PyCFunction_Check, types.builtin_function_or_method_type); +define_py_check!(exact PyCFunction_CheckExact, types.builtin_function_or_method_type); type PyCFunction = unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject; type PyCFunctionWithKeywords = unsafe extern "C" fn( diff --git a/crates/capi/src/moduleobject.rs b/crates/capi/src/moduleobject.rs index fee9868c7d8..fe80b82b1c9 100644 --- a/crates/capi/src/moduleobject.rs +++ b/crates/capi/src/moduleobject.rs @@ -7,6 +7,7 @@ use rustpython_vm::builtins::PyModule; use std::ffi::{CStr, c_char, c_int, c_void}; define_py_check!(PyModule_Check, types.module_type); +define_py_check!(exact PyModule_CheckExact, types.module_type); const PY_MOD_CREATE: c_int = 1; const PY_MOD_EXEC: c_int = 2; diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 52b24306991..9d742515aee 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -23,19 +23,31 @@ macro_rules! define_py_check { ($name:ident, $($ctx_path:ident).+) => { #[unsafe(no_mangle)] pub unsafe extern "C" fn $name(obj: *mut PyObject) -> core::ffi::c_int { - with_vm(|vm| unsafe { (&*obj).class().is_subtype(vm.ctx.$($ctx_path).+) }) + with_vm(|vm| unsafe { + obj + .as_ref() + .map(|obj| obj.class().is_subtype(vm.ctx.$($ctx_path).+)) + .unwrap_or_default() + }) } }; (exact $name:ident, $($ctx_path:ident).+) => { #[unsafe(no_mangle)] pub unsafe extern "C" fn $name(obj: *mut PyObject) -> core::ffi::c_int { - with_vm(|vm| unsafe { (&*obj).class().is(vm.ctx.$($ctx_path).+) }) + use rustpython_vm::AsObject; + with_vm(|vm| unsafe { + obj + .as_ref() + .map(|obj| obj.class().is(vm.ctx.$($ctx_path).+)) + .unwrap_or_default() + }) } }; } pub(crate) use define_py_check; define_py_check!(PyType_Check, types.type_type); +define_py_check!(exact PyType_CheckExact, types.type_type); #[unsafe(no_mangle)] pub extern "C" fn Py_TYPE(op: *mut PyObject) -> *const PyTypeObject { diff --git a/crates/capi/src/tupleobject.rs b/crates/capi/src/tupleobject.rs index 98305ba1dd1..1c6add82ca8 100644 --- a/crates/capi/src/tupleobject.rs +++ b/crates/capi/src/tupleobject.rs @@ -5,6 +5,7 @@ use rustpython_vm::PyResult; use rustpython_vm::builtins::PyTuple; define_py_check!(PyTuple_Check, types.tuple_type); +define_py_check!(exact PyTuple_CheckExact, types.tuple_type); #[unsafe(no_mangle)] pub extern "C" fn PyTuple_New(len: isize) -> *mut PyObject { diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index b6bcda7a47a..eb23ea356b9 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -8,6 +8,7 @@ use rustpython_vm::PyObjectRef; use rustpython_vm::builtins::PyStr; define_py_check!(PyUnicode_Check, types.str_type); +define_py_check!(exact PyUnicode_CheckExact, types.str_type); #[unsafe(no_mangle)] pub extern "C" fn PyUnicode_FromStringAndSize(s: *const c_char, len: isize) -> *mut PyObject { From b75b430dbbc9db5b69ac15aa76d94ad5cbc248e6 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 9 May 2026 15:48:14 +0200 Subject: [PATCH 110/135] Implement `PyExceptionClass_Check` --- crates/capi/src/pyerrors.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index 23065b1987f..112e3ace067 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -165,6 +165,16 @@ pub extern "C" fn PyErr_WriteUnraisable(obj: *mut PyObject) { }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyExceptionClass_Check(obj: *mut PyObject) -> c_int { + with_vm(|vm| unsafe { + obj.as_ref() + .and_then(|obj| obj.downcast_ref::()) + .map(|ty| ty.is_subtype(vm.ctx.exceptions.base_exception_type)) + .unwrap_or_default() + }) +} + #[unsafe(no_mangle)] pub extern "C" fn PyErr_NewException( name: *const c_char, From f8bd659f467b9006198e2f94dd1a01092eaf82ed Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 9 May 2026 16:58:44 +0200 Subject: [PATCH 111/135] Add self support to `PyNativeFunction` --- crates/capi/src/methodobject.rs | 30 ++++++++++++++---------------- crates/capi/src/object.rs | 2 +- crates/vm/src/function/method.rs | 3 ++- crates/vm/src/vm/vm_new.rs | 2 +- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index a9031dbaa88..0bde1289ffb 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -4,6 +4,7 @@ use crate::object::define_py_check; use crate::pystate::with_vm; use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; +use rustpython_vm::builtins::PyStr; use rustpython_vm::function::{FuncArgs, HeapMethodDef, PyMethodFlags}; use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; @@ -63,11 +64,9 @@ enum Binding { } impl Binding { - fn self_ptr(self, args: &mut FuncArgs, slf: Option<&PyObjectRef>) -> *mut PyObject { + fn self_ptr(self, args: &mut FuncArgs) -> *mut PyObject { if self == Binding::Static { core::ptr::null_mut() - } else if let Some(slf) = slf { - slf.as_object().as_raw().cast_mut() } else if !args.args.is_empty() { args.args.remove(0).as_object().as_raw().cast_mut() } else { @@ -123,7 +122,6 @@ impl FunctionType { name: &'static str, flags: PyMethodFlags, method: PyMethodPointer, - slf: Option, doc: Option<&'static str>, ) -> PyRef { let binding = self.binding; @@ -131,7 +129,7 @@ impl FunctionType { CallConvention::NoArgs => { let f = unsafe { method.function }; let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - let slf_ptr = binding.self_ptr(&mut args, slf.as_ref()); + let slf_ptr = binding.self_ptr(&mut args); let arg_tuple = vm.ctx.new_tuple(args.args); debug_assert!(arg_tuple.is_empty(), "Expected no arguments, but got some"); let ret_ptr = unsafe { f(slf_ptr, arg_tuple.as_object().as_raw().cast_mut()) }; @@ -142,7 +140,7 @@ impl FunctionType { CallConvention::VarArgs => { let f = unsafe { method.function }; let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - let slf_ptr = binding.self_ptr(&mut args, slf.as_ref()); + let slf_ptr = binding.self_ptr(&mut args); let arg_tuple = vm.ctx.new_tuple(args.args); let ret_ptr = unsafe { f(slf_ptr, arg_tuple.as_object().as_raw().cast_mut()) }; ret_ptr_to_pyresult(vm, ret_ptr) @@ -152,7 +150,7 @@ impl FunctionType { CallConvention::VarArgsKeywords => { let f = unsafe { method.function_with_keywords }; let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - let slf_ptr = binding.self_ptr(&mut args, slf.as_ref()); + let slf_ptr = binding.self_ptr(&mut args); let arg_tuple = vm.ctx.new_tuple(args.args); let kwargs = vm.ctx.new_dict(); for (k, v) in args.kwargs { @@ -172,7 +170,7 @@ impl FunctionType { CallConvention::FastCallKeywords => { let f = unsafe { method.function_fast_with_keywords }; let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - let slf_ptr = binding.self_ptr(&mut args, slf.as_ref()); + let slf_ptr = binding.self_ptr(&mut args); let nargs = args.args.len(); let mut fastcall_args = args.args; let mut kwnames_tuple = None; @@ -228,11 +226,7 @@ fn ret_ptr_to_pyresult(vm: &VirtualMachine, ret_ptr: *mut PyObject) -> PyResult Ok(unsafe { PyObjectRef::from_raw(ret_ptr) }) } -pub(crate) fn build_method_def( - vm: &VirtualMachine, - ml: &PyMethodDef, - slf: Option, -) -> PyRef { +pub(crate) fn build_method_def(vm: &VirtualMachine, ml: &PyMethodDef) -> PyRef { let name = unsafe { CStr::from_ptr(ml.ml_name) } .to_str() .expect("Method name was not valid UTF-8"); @@ -246,7 +240,7 @@ pub(crate) fn build_method_def( let flags = PyMethodFlags::from_bits(ml.ml_flags as u32).expect("PyMethodDef contains unknown flags"); - FunctionType::from_flags(flags).build_heap_method_def(vm, name, flags, ml.ml_meth, slf, doc) + FunctionType::from_flags(flags).build_heap_method_def(vm, name, flags, ml.ml_meth, doc) } #[unsafe(no_mangle)] @@ -257,9 +251,13 @@ pub extern "C" fn PyCMethod_New( _cls: *mut PyTypeObject, ) -> *mut PyObject { with_vm(|vm| -> PyResult { + assert!( + _cls.is_null(), + "PyCMethod_New does not support METH_METHOD yet" + ); let ml = unsafe { &*ml }; - let slf = NonNull::new(slf).map(|ptr| unsafe { ptr.as_ref().to_owned() }); - Ok(build_method_def(vm, ml, slf).build_function(vm).into()) + let zelf = unsafe { slf.as_ref().map(|obj| obj.to_owned()) }; + Ok(build_method_def(vm, ml).build_function(vm, zelf).into()) }) } diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 9d742515aee..a8050d71dd9 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -372,7 +372,7 @@ pub extern "C" fn PyType_FromSpec(spec: *mut PyType_Spec) -> *mut PyObject { .to_str() .expect("method name must be valid UTF-8") }; - let method = build_method_def(vm, method, None).build_method(class_static, vm); + let method = build_method_def(vm, method).build_method(class_static, vm); class .attributes .write() diff --git a/crates/vm/src/function/method.rs b/crates/vm/src/function/method.rs index 295e4d89adf..66e75da7c92 100644 --- a/crates/vm/src/function/method.rs +++ b/crates/vm/src/function/method.rs @@ -296,9 +296,10 @@ impl Py { unsafe { &*(&self.method as *const _) } } - pub fn build_function(&self, vm: &VirtualMachine) -> PyRef { + pub fn build_function(&self, vm: &VirtualMachine, zelf: Option) -> PyRef { let mut function = unsafe { self.method() }.to_function(); function._method_def_owner = Some(self.to_owned().into()); + function.zelf = zelf; PyRef::new_ref( function, vm.ctx.types.builtin_function_or_method_type.to_owned(), diff --git a/crates/vm/src/vm/vm_new.rs b/crates/vm/src/vm/vm_new.rs index 63de652ac7a..ebbffddd4b7 100644 --- a/crates/vm/src/vm/vm_new.rs +++ b/crates/vm/src/vm/vm_new.rs @@ -83,7 +83,7 @@ impl VirtualMachine { let def = self .ctx .new_method_def(name, f, PyMethodFlags::empty(), None); - def.build_function(self) + def.build_function(self, None) } pub fn new_method( From b2c2f6913fc1c2dbacc607c1f1b2e5f37a4768eb Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 9 May 2026 19:59:33 +0200 Subject: [PATCH 112/135] Use latest pyo3 to make test work on windows --- .github/workflows/ci.yaml | 1 - Cargo.lock | 16 +++++----------- Cargo.toml | 3 ++- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 61e81a8e61d..8ff92f4d12b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -121,7 +121,6 @@ jobs: - name: run c-api tests working-directory: crates/capi run: cargo test - if: runner.os != 'Windows' # Requires pyo3 0.29+ on Windows - run: cargo doc --locked if: runner.os == 'Linux' diff --git a/Cargo.lock b/Cargo.lock index 508dc691457..fb4c24a60d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2682,8 +2682,7 @@ dependencies = [ [[package]] name = "pyo3" version = "0.28.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" +source = "git+https://github.com/PyO3/pyo3#901b032bfb8ec8eb05b39be3b98ef880427cf15a" dependencies = [ "libc", "once_cell", @@ -2696,8 +2695,7 @@ dependencies = [ [[package]] name = "pyo3-build-config" version = "0.28.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" +source = "git+https://github.com/PyO3/pyo3#901b032bfb8ec8eb05b39be3b98ef880427cf15a" dependencies = [ "target-lexicon", ] @@ -2705,8 +2703,7 @@ dependencies = [ [[package]] name = "pyo3-ffi" version = "0.28.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" +source = "git+https://github.com/PyO3/pyo3#901b032bfb8ec8eb05b39be3b98ef880427cf15a" dependencies = [ "libc", "pyo3-build-config", @@ -2715,8 +2712,7 @@ dependencies = [ [[package]] name = "pyo3-macros" version = "0.28.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" +source = "git+https://github.com/PyO3/pyo3#901b032bfb8ec8eb05b39be3b98ef880427cf15a" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2727,12 +2723,10 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" version = "0.28.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" +source = "git+https://github.com/PyO3/pyo3#901b032bfb8ec8eb05b39be3b98ef880427cf15a" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn", ] diff --git a/Cargo.toml b/Cargo.toml index d926b5f5e2e..40c7efacbe9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,6 +103,7 @@ lto = "thin" [patch.crates-io] parking_lot_core = { git = "https://github.com/youknowone/parking_lot", branch = "rustpython" } +pyo3-ffi = { git = "https://github.com/PyO3/pyo3" } # REDOX START, Uncomment when you want to compile/check with redoxer # REDOX END @@ -256,7 +257,7 @@ pkcs8 = "0.10" proc-macro2 = "1.0.105" psm = "0.1" pymath = { version = "0.2.0", features = ["mul_add", "malachite-bigint", "complex"] } -pyo3 = "0.28" +pyo3 = { git = "https://github.com/PyO3/pyo3" } quote = "1.0.45" radium = "1.1.1" rand = "0.9" From d18d259aa62c77714f8e2504e6d9931e36fe5744 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 10 May 2026 12:52:11 +0200 Subject: [PATCH 113/135] Revert "Use latest pyo3 to make test work on windows" This reverts commit b2c2f6913fc1c2dbacc607c1f1b2e5f37a4768eb. --- .github/workflows/ci.yaml | 1 + Cargo.lock | 16 +++++++++++----- Cargo.toml | 3 +-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8ff92f4d12b..61e81a8e61d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -121,6 +121,7 @@ jobs: - name: run c-api tests working-directory: crates/capi run: cargo test + if: runner.os != 'Windows' # Requires pyo3 0.29+ on Windows - run: cargo doc --locked if: runner.os == 'Linux' diff --git a/Cargo.lock b/Cargo.lock index fb4c24a60d1..508dc691457 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2682,7 +2682,8 @@ dependencies = [ [[package]] name = "pyo3" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#901b032bfb8ec8eb05b39be3b98ef880427cf15a" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" dependencies = [ "libc", "once_cell", @@ -2695,7 +2696,8 @@ dependencies = [ [[package]] name = "pyo3-build-config" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#901b032bfb8ec8eb05b39be3b98ef880427cf15a" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" dependencies = [ "target-lexicon", ] @@ -2703,7 +2705,8 @@ dependencies = [ [[package]] name = "pyo3-ffi" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#901b032bfb8ec8eb05b39be3b98ef880427cf15a" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" dependencies = [ "libc", "pyo3-build-config", @@ -2712,7 +2715,8 @@ dependencies = [ [[package]] name = "pyo3-macros" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#901b032bfb8ec8eb05b39be3b98ef880427cf15a" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2723,10 +2727,12 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#901b032bfb8ec8eb05b39be3b98ef880427cf15a" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" dependencies = [ "heck", "proc-macro2", + "pyo3-build-config", "quote", "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 40c7efacbe9..d926b5f5e2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,6 @@ lto = "thin" [patch.crates-io] parking_lot_core = { git = "https://github.com/youknowone/parking_lot", branch = "rustpython" } -pyo3-ffi = { git = "https://github.com/PyO3/pyo3" } # REDOX START, Uncomment when you want to compile/check with redoxer # REDOX END @@ -257,7 +256,7 @@ pkcs8 = "0.10" proc-macro2 = "1.0.105" psm = "0.1" pymath = { version = "0.2.0", features = ["mul_add", "malachite-bigint", "complex"] } -pyo3 = { git = "https://github.com/PyO3/pyo3" } +pyo3 = "0.28" quote = "1.0.45" radium = "1.1.1" rand = "0.9" From 0f60694798e219063095f98ab5eb6ece53f1b12c Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sun, 10 May 2026 17:04:08 +0200 Subject: [PATCH 114/135] Add todo list --- crates/capi/capi_todo.md | 215 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 crates/capi/capi_todo.md diff --git a/crates/capi/capi_todo.md b/crates/capi/capi_todo.md new file mode 100644 index 00000000000..d6e4960ac71 --- /dev/null +++ b/crates/capi/capi_todo.md @@ -0,0 +1,215 @@ +# Unimplemented C API functions + +Mapping source: `pyo3-ffi/src/*.rs`, which mirrors the CPython header split used by the C API. + +## `abstract.h` + +RustPython C API target: `crates/capi/src/abstract_.rs` + +- `PyIter_Check` +- `PyIter_NextItem` +- `PyIter_Send` +- `PyMapping_Items` +- `PyMapping_Keys` +- `PyMapping_Size` +- `PyMapping_Values` +- `PyNumber_Add` +- `PyNumber_Lshift` +- `PyNumber_Or` +- `PyNumber_Rshift` +- `PyNumber_Subtract` +- `PyObject_GetIter` +- `PyObject_IsInstance` +- `PyObject_IsSubclass` +- `PyObject_Size` +- `PySequence_Check` +- `PySequence_Concat` +- `PySequence_Count` +- `PySequence_DelItem` +- `PySequence_DelSlice` +- `PySequence_GetItem` +- `PySequence_GetSlice` +- `PySequence_InPlaceConcat` +- `PySequence_InPlaceRepeat` +- `PySequence_Index` +- `PySequence_List` +- `PySequence_Repeat` +- `PySequence_SetItem` +- `PySequence_SetSlice` +- `PySequence_Size` +- `PySequence_Tuple` + +## `bytearrayobject.h` + +RustPython C API target: `crates/capi/src/bytearrayobject.rs` (not present yet) + +- `PyByteArray_AsString` +- `PyByteArray_Check` +- `PyByteArray_FromObject` +- `PyByteArray_FromStringAndSize` +- `PyByteArray_Resize` +- `PyByteArray_Size` + +## `descrobject.h` + +RustPython C API target: `crates/capi/src/descrobject.rs` (not present yet) + +- `PyDictProxy_New` + +## `dictobject.h` + +RustPython C API target: `crates/capi/src/dictobject.rs` + +- `PyDict_Contains` +- `PyDict_Copy` +- `PyDict_DelItem` +- `PyDict_Items` +- `PyDict_Keys` +- `PyDict_Merge` +- `PyDict_MergeFromSeq2` +- `PyDict_Update` +- `PyDict_Values` + +## `genericaliasobject.h` + +RustPython C API target: `crates/capi/src/genericaliasobject.rs` (not present yet) + +- `Py_GenericAlias` + +## `import.h` + +RustPython C API target: `crates/capi/src/import.rs` + +- `PyImport_ExecCodeModuleEx` + +## `listobject.h` + +RustPython C API target: `crates/capi/src/listobject.rs` + +- `PyList_AsTuple` +- `PyList_GetSlice` +- `PyList_SetSlice` +- `PyList_Sort` + +## `longobject.h` + +RustPython C API target: `crates/capi/src/longobject.rs` + +- `PyLong_AsUnsignedLongLongMask` + +## `modsupport.h` + +RustPython C API target: `crates/capi/src/modsupport.rs` (not present yet) + +- `PyModule_ExecDef` +- `PyModule_FromDefAndSpec2` + +## `moduleobject.h` + +RustPython C API target: `crates/capi/src/moduleobject.rs` + +- `PyModule_GetFilenameObject` +- `PyModule_NewObject` + +## `object.h` + +RustPython C API target: `crates/capi/src/object.rs` + +- `PyCallable_Check` +- `PyObject_ClearWeakRefs` +- `PyObject_Dir` +- `PyObject_GenericGetAttr` +- `PyObject_GetOptionalAttr` +- `PyObject_RichCompare` +- `PyType_GetModuleName` + +## `osmodule.h` + +RustPython C API target: `crates/capi/src/osmodule.rs` (not present yet) + +- `PyOS_FSPath` + +## `pybuffer.h` + +RustPython C API target: `crates/capi/src/pybuffer.rs` (not present yet) + +- `PyBuffer_FromContiguous` +- `PyBuffer_GetPointer` +- `PyBuffer_IsContiguous` +- `PyBuffer_Release` +- `PyBuffer_ToContiguous` +- `PyObject_GetBuffer` + +## `pycapsule.h` + +RustPython C API target: `crates/capi/src/capsule.rs` + +- `PyCapsule_GetContext` +- `PyCapsule_Import` +- `PyCapsule_SetContext` +- `PyCapsule_SetPointer` + +## `pyerrors.h` + +RustPython C API target: `crates/capi/src/pyerrors.rs` + +- `PyException_GetCause` +- `PyException_GetContext` +- `PyException_SetContext` +- `PyUnicodeDecodeError_Create` + +## `pystate.h` + +RustPython C API target: `crates/capi/src/pystate.rs` + +- `PyInterpreterState_Get` +- `PyInterpreterState_GetID` + +## `setobject.h` + +RustPython C API target: `crates/capi/src/setobject.rs` (not present yet) + +- `PyFrozenSet_Check` +- `PyFrozenSet_New` +- `PySet_Add` +- `PySet_Check` +- `PySet_Clear` +- `PySet_Contains` +- `PySet_Discard` +- `PySet_New` +- `PySet_Pop` +- `PySet_Size` + +## `sliceobject.h` + +RustPython C API target: `crates/capi/src/sliceobject.rs` (not present yet) + +- `PySlice_AdjustIndices` +- `PySlice_New` +- `PySlice_Unpack` + +## `unicodeobject.h` + +RustPython C API target: `crates/capi/src/unicodeobject.rs` + +- `PyUnicode_AsUTF8String` +- `PyUnicode_DecodeFSDefaultAndSize` +- `PyUnicode_EncodeFSDefault` +- `PyUnicode_FromEncodedObject` + +## `warnings.h` + +RustPython C API target: `crates/capi/src/warnings.rs` (not present yet) + +- `PyErr_WarnEx` +- `PyErr_WarnExplicit` + +## `weakrefobject.h` + +RustPython C API target: `crates/capi/src/weakrefobject.rs` (not present yet) + +- `PyWeakref_CheckProxy` +- `PyWeakref_CheckRef` +- `PyWeakref_GetRef` +- `PyWeakref_NewProxy` +- `PyWeakref_NewRef` From 2efb3b30a36f2b2ddc2d54d80edc22502c8ca0a3 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 11 May 2026 11:04:32 +0200 Subject: [PATCH 115/135] Update var_arg functions --- crates/capi/src/abstract_.rs | 14 +++++--------- crates/capi/src/tupleobject.rs | 10 ++++------ crates/vm/src/function/method.rs | 6 +++++- crates/vm/src/types/slot_defs.rs | 2 +- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index faf8992aa43..631102b9214 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -61,17 +61,13 @@ pub unsafe extern "C" fn PyObject_CallMethodObjArgs( mut args: ... ) -> *mut PyObject { with_vm(|vm| { - let mut arguments: Vec = vec![]; - loop { - if let Some(arg) = core::ptr::NonNull::new(unsafe { args.arg::<*mut PyObject>() }) { - arguments.push(unsafe { arg.as_ref() }.to_owned()); - } else { - break; - } - } - let method_name = unsafe { (&*name).try_downcast_ref::(vm)? }; let callable = unsafe { (&*receiver).get_attr(method_name, vm)? }; + let arguments = core::iter::from_fn(|| unsafe { + core::ptr::NonNull::new(args.next_arg::<*mut PyObject>()) + .map(|obj| obj.as_ref().to_owned()) + }) + .collect::>(); callable.call(arguments, vm) }) } diff --git a/crates/capi/src/tupleobject.rs b/crates/capi/src/tupleobject.rs index 1c6add82ca8..4bd86dc513f 100644 --- a/crates/capi/src/tupleobject.rs +++ b/crates/capi/src/tupleobject.rs @@ -34,12 +34,10 @@ pub extern "C" fn PyTuple_FromArray(array: *const *mut PyObject, size: isize) -> #[cfg(feature = "nightly")] pub unsafe extern "C" fn PyTuple_Pack(len: isize, mut args: ...) -> *mut PyObject { with_vm(|vm| { - let mut items = vec![]; - - for _ in 0..len { - let item = unsafe { &*args.arg::<*mut PyObject>() }; - items.push(item.to_owned()); - } + let items = + core::iter::repeat_with(|| unsafe { (&*args.next_arg::<*mut PyObject>()).to_owned() }) + .take(len as usize) + .collect::>(); vm.new_tuple(items) }) diff --git a/crates/vm/src/function/method.rs b/crates/vm/src/function/method.rs index f9340be935b..8fd8aa94764 100644 --- a/crates/vm/src/function/method.rs +++ b/crates/vm/src/function/method.rs @@ -299,7 +299,11 @@ impl Py { unsafe { &*(&self.method as *const _) } } - pub fn build_function(&self, vm: &VirtualMachine, zelf: Option) -> PyRef { + pub fn build_function( + &self, + vm: &VirtualMachine, + zelf: Option, + ) -> PyRef { let mut function = unsafe { self.method() }.to_function(); function._method_def_owner = Some(self.to_owned().into()); function.zelf = zelf; diff --git a/crates/vm/src/types/slot_defs.rs b/crates/vm/src/types/slot_defs.rs index 1cae6ec5796..a27c681df02 100644 --- a/crates/vm/src/types/slot_defs.rs +++ b/crates/vm/src/types/slot_defs.rs @@ -2,9 +2,9 @@ //! //! This module provides a centralized array of all slot definitions, -use num_enum::TryFromPrimitive; use super::{PyComparisonOp, PyTypeSlots}; use crate::builtins::descriptor::SlotFunc; +use num_enum::TryFromPrimitive; /// Slot operation type /// From 365eb110ead15804dc7b2e137e4862eb1ebfaa79 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 11 May 2026 17:08:26 +0200 Subject: [PATCH 116/135] Fix `ctypes.py_object` handling --- Lib/test/test_ctypes/test_python_api.py | 2 -- crates/vm/src/stdlib/_ctypes/base.rs | 12 ++++++++++++ crates/vm/src/stdlib/_ctypes/simple.rs | 11 +++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_ctypes/test_python_api.py b/Lib/test/test_ctypes/test_python_api.py index a1ee8a0de1e..388ec796af0 100644 --- a/Lib/test/test_ctypes/test_python_api.py +++ b/Lib/test/test_ctypes/test_python_api.py @@ -7,8 +7,6 @@ class PythonAPITestCase(unittest.TestCase): - # TODO: RUSTPYTHON - requires pythonapi (Python C API) - @unittest.expectedFailure def test_PyBytes_FromStringAndSize(self): PyBytes_FromStringAndSize = pythonapi.PyBytes_FromStringAndSize diff --git a/crates/vm/src/stdlib/_ctypes/base.rs b/crates/vm/src/stdlib/_ctypes/base.rs index 7d611c27b0f..afc03213b26 100644 --- a/crates/vm/src/stdlib/_ctypes/base.rs +++ b/crates/vm/src/stdlib/_ctypes/base.rs @@ -2342,6 +2342,18 @@ pub(super) fn bytes_to_pyobject( } Ok(vm.ctx.new_int(val).into()) } + "O" => { + // py_object: return Python object from pointer + let ptr = read_ptr_from_buffer(bytes); + if ptr == 0 { + return Err(vm.new_value_error("PyObject is NULL")); + } + unsafe { + let obj = + PyObjectRef::from_raw(core::ptr::NonNull::new_unchecked(ptr as *mut _)); + Ok(obj) + } + } "u" => { let val = if bytes.len() >= mem::size_of::() { let wc = if mem::size_of::() == 2 { diff --git a/crates/vm/src/stdlib/_ctypes/simple.rs b/crates/vm/src/stdlib/_ctypes/simple.rs index 1dd2001335f..f5e07ebc2ea 100644 --- a/crates/vm/src/stdlib/_ctypes/simple.rs +++ b/crates/vm/src/stdlib/_ctypes/simple.rs @@ -466,6 +466,17 @@ impl PyCSimpleType { } } } + // py_object: pass any Python object as PyObject* + Some("O") => { + return Ok(CArgObject { + tag: b'O', + value: FfiArgValue::Pointer(value.get_id()), + obj: value, + size: 0, + offset: 0, + } + .to_pyobject(vm)); + } // c_bool Some("?") => { let bool_val = value.is_true(vm)?; From 397f6f5ef4ecda422213c9e2f04d4c407265cff3 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 12 May 2026 12:54:30 +0200 Subject: [PATCH 117/135] Refactor `build_method_def` --- crates/capi/src/methodobject.rs | 354 ++++++++++++++------------------ 1 file changed, 153 insertions(+), 201 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 0bde1289ffb..0b9c81e37ab 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -4,34 +4,12 @@ use crate::object::define_py_check; use crate::pystate::with_vm; use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; -use rustpython_vm::builtins::PyStr; use rustpython_vm::function::{FuncArgs, HeapMethodDef, PyMethodFlags}; use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; define_py_check!(PyCFunction_Check, types.builtin_function_or_method_type); define_py_check!(exact PyCFunction_CheckExact, types.builtin_function_or_method_type); -type PyCFunction = unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject; -type PyCFunctionWithKeywords = unsafe extern "C" fn( - slf: *mut PyObject, - args: *mut PyObject, - kwargs: *mut PyObject, -) -> *mut PyObject; -type PyCFunctionFastWithKeywords = unsafe extern "C" fn( - slf: *mut PyObject, - args: *const *mut PyObject, - nargs: isize, - kwnames: *mut PyObject, -) -> *mut PyObject; - -#[repr(C)] -#[derive(Copy, Clone)] -pub union PyMethodPointer { - function: PyCFunction, - function_with_keywords: PyCFunctionWithKeywords, - function_fast_with_keywords: PyCFunctionFastWithKeywords, -} - #[repr(C)] pub struct PyMethodDef { pub(crate) ml_name: *const c_char, @@ -40,182 +18,165 @@ pub struct PyMethodDef { pub(crate) ml_doc: *const c_char, } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -struct FunctionType { - call: CallConvention, - binding: Binding, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum CallConvention { - NoArgs, - OneArg, - VarArgs, - VarArgsKeywords, - FastCall, - FastCallKeywords, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum Binding { - Instance, - Class, - Static, -} - -impl Binding { - fn self_ptr(self, args: &mut FuncArgs) -> *mut PyObject { - if self == Binding::Static { - core::ptr::null_mut() - } else if !args.args.is_empty() { - args.args.remove(0).as_object().as_raw().cast_mut() - } else { - core::ptr::null_mut() - } - } +#[repr(C)] +#[derive(Copy, Clone)] +#[allow(non_snake_case)] +pub union PyMethodPointer { + PyCFunction: unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject, + PyCFunctionWithKeywords: unsafe extern "C" fn( + slf: *mut PyObject, + args: *mut PyObject, + kwargs: *mut PyObject, + ) -> *mut PyObject, + PyCFunctionFast: unsafe extern "C" fn( + slf: *mut PyObject, + args: *mut *mut PyObject, + nargs: isize, + ) -> *mut PyObject, + PyCFunctionFastWithKeywords: unsafe extern "C" fn( + slf: *mut PyObject, + args: *const *mut PyObject, + nargs: isize, + kwnames: *mut PyObject, + ) -> *mut PyObject, } -impl FunctionType { - fn from_flags(flags: PyMethodFlags) -> Self { - if flags.contains(PyMethodFlags::METHOD) { - todo!("METH_METHOD is not supported yet") - } +pub(crate) fn build_method_def(vm: &VirtualMachine, ml: &PyMethodDef) -> PyRef { + let name = unsafe { CStr::from_ptr(ml.ml_name) } + .to_str() + .expect("Method name was not valid UTF-8"); - let binding = if flags.contains(PyMethodFlags::STATIC) { - debug_assert!(!flags.contains(PyMethodFlags::CLASS)); - Binding::Static - } else if flags.contains(PyMethodFlags::CLASS) { - Binding::Class - } else { - Binding::Instance - }; + let doc = NonNull::new(ml.ml_doc.cast_mut()).map(|doc| { + unsafe { CStr::from_ptr(doc.as_ptr()) } + .to_str() + .expect("Method doc was not valid UTF-8") + }); - let call_flags = flags - & (PyMethodFlags::VARARGS - | PyMethodFlags::KEYWORDS - | PyMethodFlags::NOARGS - | PyMethodFlags::O - | PyMethodFlags::FASTCALL); + let flags = + PyMethodFlags::from_bits(ml.ml_flags as u32).expect("PyMethodDef contains unknown flags"); - let call = if call_flags == PyMethodFlags::NOARGS { - CallConvention::NoArgs - } else if call_flags == PyMethodFlags::O { - CallConvention::OneArg - } else if call_flags == PyMethodFlags::VARARGS { - CallConvention::VarArgs - } else if call_flags == (PyMethodFlags::VARARGS | PyMethodFlags::KEYWORDS) { - CallConvention::VarArgsKeywords - } else if call_flags == PyMethodFlags::FASTCALL { - CallConvention::FastCall - } else if call_flags == (PyMethodFlags::FASTCALL | PyMethodFlags::KEYWORDS) { - CallConvention::FastCallKeywords - } else { - todo!("unsupported or invalid calling-convention flags"); - }; + let method = ml.ml_meth; - Self { call, binding } + if flags.contains(PyMethodFlags::METHOD) { + panic!("METH_METHOD is not supported on abi3") } - fn build_heap_method_def( - self, - vm: &VirtualMachine, - name: &'static str, - flags: PyMethodFlags, - method: PyMethodPointer, - doc: Option<&'static str>, - ) -> PyRef { - let binding = self.binding; - match self.call { - CallConvention::NoArgs => { - let f = unsafe { method.function }; - let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - let slf_ptr = binding.self_ptr(&mut args); - let arg_tuple = vm.ctx.new_tuple(args.args); - debug_assert!(arg_tuple.is_empty(), "Expected no arguments, but got some"); - let ret_ptr = unsafe { f(slf_ptr, arg_tuple.as_object().as_raw().cast_mut()) }; - ret_ptr_to_pyresult(vm, ret_ptr) - }; - vm.ctx.new_method_def(name, callable, flags, doc) - } - CallConvention::VarArgs => { - let f = unsafe { method.function }; - let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - let slf_ptr = binding.self_ptr(&mut args); - let arg_tuple = vm.ctx.new_tuple(args.args); - let ret_ptr = unsafe { f(slf_ptr, arg_tuple.as_object().as_raw().cast_mut()) }; - ret_ptr_to_pyresult(vm, ret_ptr) + let call_flags = flags + & (PyMethodFlags::VARARGS + | PyMethodFlags::KEYWORDS + | PyMethodFlags::NOARGS + | PyMethodFlags::O + | PyMethodFlags::FASTCALL); + + bitflags::bitflags_match!(call_flags, { + PyMethodFlags::NOARGS => { + let f = unsafe { method.PyCFunction }; + let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { + let slf = take_self_arg(&mut args, flags); + let slf_ptr = slf + .as_ref() + .map(|obj| obj.as_object().as_raw().cast_mut()) + .unwrap_or_default(); + let arg_tuple = vm.ctx.new_tuple(args.args); + debug_assert!(arg_tuple.is_empty(), "Expected no arguments, but got some"); + let ret_ptr = unsafe { f(slf_ptr, arg_tuple.as_object().as_raw().cast_mut()) }; + ret_ptr_to_pyresult(vm, ret_ptr) + }; + vm.ctx.new_method_def(name, callable, flags, doc) + }, + PyMethodFlags::VARARGS => { + let f = unsafe { method.PyCFunction }; + let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { + let slf = take_self_arg(&mut args, flags); + let slf_ptr = slf + .as_ref() + .map(|obj| obj.as_object().as_raw().cast_mut()) + .unwrap_or_default(); + let arg_tuple = vm.ctx.new_tuple(args.args); + let ret_ptr = unsafe { f(slf_ptr, arg_tuple.as_object().as_raw().cast_mut()) }; + ret_ptr_to_pyresult(vm, ret_ptr) + }; + vm.ctx.new_method_def(name, callable, flags, doc) + }, + PyMethodFlags::VARARGS | PyMethodFlags::KEYWORDS => { + let f = unsafe { method.PyCFunctionWithKeywords }; + let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { + let slf = take_self_arg(&mut args, flags); + let slf_ptr = slf + .as_ref() + .map(|obj| obj.as_object().as_raw().cast_mut()) + .unwrap_or_default(); + let arg_tuple = vm.ctx.new_tuple(args.args); + let kwargs = vm.ctx.new_dict(); + for (k, v) in args.kwargs { + kwargs.set_item(&*k, v, vm)?; + } + let ret_ptr = unsafe { + f( + slf_ptr, + arg_tuple.as_object().as_raw().cast_mut(), + kwargs.as_object().as_raw().cast_mut(), + ) }; - vm.ctx.new_method_def(name, callable, flags, doc) - } - CallConvention::VarArgsKeywords => { - let f = unsafe { method.function_with_keywords }; - let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - let slf_ptr = binding.self_ptr(&mut args); - let arg_tuple = vm.ctx.new_tuple(args.args); - let kwargs = vm.ctx.new_dict(); + ret_ptr_to_pyresult(vm, ret_ptr) + }; + vm.ctx.new_method_def(name, callable, flags, doc) + }, + PyMethodFlags::FASTCALL | PyMethodFlags::KEYWORDS => { + let f = unsafe { method.PyCFunctionFastWithKeywords }; + let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { + let slf = take_self_arg(&mut args, flags); + let slf_ptr = slf + .as_ref() + .map(|obj| obj.as_object().as_raw().cast_mut()) + .unwrap_or_default(); + let nargs = args.args.len(); + let mut fastcall_args = args.args; + let mut kwnames_tuple = None; + if !args.kwargs.is_empty() { + let mut kwnames = Vec::with_capacity(args.kwargs.len()); for (k, v) in args.kwargs { - kwargs.set_item(&*k, v, vm)?; + kwnames.push(vm.ctx.new_str(k).into()); + fastcall_args.push(v); } - let ret_ptr = unsafe { - f( - slf_ptr, - arg_tuple.as_object().as_raw().cast_mut(), - kwargs.as_object().as_raw().cast_mut(), - ) - }; - ret_ptr_to_pyresult(vm, ret_ptr) + kwnames_tuple = Some(vm.ctx.new_tuple(kwnames)); + } + let fastcall_arg_ptrs = fastcall_args + .iter() + .map(|obj| obj.as_object().as_raw().cast_mut()) + .collect::>(); + let kwnames_ptr = kwnames_tuple + .as_ref() + .map(|tuple| tuple.as_object().as_raw().cast_mut()) + .unwrap_or(core::ptr::null_mut()); + let ret_ptr = unsafe { + f( + slf_ptr, + fastcall_arg_ptrs.as_ptr(), + nargs as isize, + kwnames_ptr, + ) }; - vm.ctx.new_method_def(name, callable, flags, doc) - } - CallConvention::FastCallKeywords => { - let f = unsafe { method.function_fast_with_keywords }; - let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - let slf_ptr = binding.self_ptr(&mut args); - let nargs = args.args.len(); - let mut fastcall_args = args.args; - let mut kwnames_tuple = None; - if !args.kwargs.is_empty() { - let mut kwnames = Vec::with_capacity(args.kwargs.len()); - for (k, v) in args.kwargs { - kwnames.push(vm.ctx.new_str(k).into()); - fastcall_args.push(v); - } - kwnames_tuple = Some(vm.ctx.new_tuple(kwnames)); - } - let fastcall_arg_ptrs = fastcall_args - .iter() - .map(|obj| obj.as_object().as_raw().cast_mut()) - .collect::>(); - let kwnames_ptr = kwnames_tuple - .as_ref() - .map(|tuple| tuple.as_object().as_raw().cast_mut()) - .unwrap_or(core::ptr::null_mut()); - let ret_ptr = unsafe { - f( - slf_ptr, - fastcall_arg_ptrs.as_ptr(), - nargs as isize, - kwnames_ptr, - ) - }; - ret_ptr_to_pyresult(vm, ret_ptr) - }; - vm.ctx.new_method_def(name, callable, flags, doc) - } - CallConvention::FastCall => { - let callable = move |_args: FuncArgs, _vm: &VirtualMachine| -> PyResult { - todo!("METH_FASTCALL without METH_KEYWORDS is not supported yet") - }; - vm.ctx.new_method_def(name, callable, flags, doc) - } - CallConvention::OneArg => { - let callable = move |_args: FuncArgs, _vm: &VirtualMachine| -> PyResult { - todo!("METH_O is not supported yet") - }; - vm.ctx.new_method_def(name, callable, flags, doc) - } - } - } + ret_ptr_to_pyresult(vm, ret_ptr) + }; + vm.ctx.new_method_def(name, callable, flags, doc) + }, + PyMethodFlags::FASTCALL => { + let callable = move |_args: FuncArgs, _vm: &VirtualMachine| -> PyResult { + todo!("METH_FASTCALL without METH_KEYWORDS is not supported yet") + }; + vm.ctx.new_method_def(name, callable, flags, doc) + }, + PyMethodFlags::O => { + let callable = move |_args: FuncArgs, _vm: &VirtualMachine| -> PyResult { + todo!("METH_O is not supported yet") + }; + vm.ctx.new_method_def(name, callable, flags, doc) + }, + _ => { + todo!("unsupported or invalid calling-convention flags") + }, + }) } fn ret_ptr_to_pyresult(vm: &VirtualMachine, ret_ptr: *mut PyObject) -> PyResult { @@ -226,21 +187,12 @@ fn ret_ptr_to_pyresult(vm: &VirtualMachine, ret_ptr: *mut PyObject) -> PyResult Ok(unsafe { PyObjectRef::from_raw(ret_ptr) }) } -pub(crate) fn build_method_def(vm: &VirtualMachine, ml: &PyMethodDef) -> PyRef { - let name = unsafe { CStr::from_ptr(ml.ml_name) } - .to_str() - .expect("Method name was not valid UTF-8"); - - let doc = NonNull::new(ml.ml_doc.cast_mut()).map(|doc| { - unsafe { CStr::from_ptr(doc.as_ptr()) } - .to_str() - .expect("Method doc was not valid UTF-8") - }); - - let flags = - PyMethodFlags::from_bits(ml.ml_flags as u32).expect("PyMethodDef contains unknown flags"); - - FunctionType::from_flags(flags).build_heap_method_def(vm, name, flags, ml.ml_meth, doc) +fn take_self_arg(args: &mut FuncArgs, flags: PyMethodFlags) -> Option { + if flags.contains(PyMethodFlags::STATIC) | args.args.is_empty() { + None + } else { + Some(args.args.remove(0)) + } } #[unsafe(no_mangle)] From a15972b2c278bbbf2a78cf0d376efe76299526b7 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 12 May 2026 12:59:49 +0200 Subject: [PATCH 118/135] Implement `build_method_def` for `METH_O` --- crates/capi/src/methodobject.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 0b9c81e37ab..dd4f8878f6f 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -168,8 +168,10 @@ pub(crate) fn build_method_def(vm: &VirtualMachine, ml: &PyMethodDef) -> PyRef { - let callable = move |_args: FuncArgs, _vm: &VirtualMachine| -> PyResult { - todo!("METH_O is not supported yet") + let f = unsafe { method.PyCFunction }; + let callable = move |zelf: PyObjectRef, arg: PyObjectRef, vm: &VirtualMachine| -> PyResult { + let ret_ptr = unsafe { f(zelf.as_raw().cast_mut(), arg.as_raw().cast_mut()) }; + ret_ptr_to_pyresult(vm, ret_ptr) }; vm.ctx.new_method_def(name, callable, flags, doc) }, From aff5c7f3b832a9a4740dcbfec741102fdee659a2 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 12 May 2026 13:25:03 +0200 Subject: [PATCH 119/135] Extract callable convertions from `build_method_def` --- crates/capi/src/methodobject.rs | 173 ++++++++++++++++++-------------- 1 file changed, 96 insertions(+), 77 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index dd4f8878f6f..bb325debd94 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -70,94 +70,27 @@ pub(crate) fn build_method_def(vm: &VirtualMachine, ml: &PyMethodDef) -> PyRef { - let f = unsafe { method.PyCFunction }; - let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - let slf = take_self_arg(&mut args, flags); - let slf_ptr = slf - .as_ref() - .map(|obj| obj.as_object().as_raw().cast_mut()) - .unwrap_or_default(); - let arg_tuple = vm.ctx.new_tuple(args.args); - debug_assert!(arg_tuple.is_empty(), "Expected no arguments, but got some"); - let ret_ptr = unsafe { f(slf_ptr, arg_tuple.as_object().as_raw().cast_mut()) }; - ret_ptr_to_pyresult(vm, ret_ptr) + let callable = move |args: FuncArgs, vm: &VirtualMachine| unsafe { + debug_assert!(args.args.len() <= 1 && args.kwargs.is_empty(), "Expected no arguments, but got some"); + call_function(vm, method, flags, args) }; vm.ctx.new_method_def(name, callable, flags, doc) }, PyMethodFlags::VARARGS => { - let f = unsafe { method.PyCFunction }; - let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - let slf = take_self_arg(&mut args, flags); - let slf_ptr = slf - .as_ref() - .map(|obj| obj.as_object().as_raw().cast_mut()) - .unwrap_or_default(); - let arg_tuple = vm.ctx.new_tuple(args.args); - let ret_ptr = unsafe { f(slf_ptr, arg_tuple.as_object().as_raw().cast_mut()) }; - ret_ptr_to_pyresult(vm, ret_ptr) + let callable = move |args: FuncArgs, vm: &VirtualMachine| unsafe { + call_function(vm, method, flags, args) }; vm.ctx.new_method_def(name, callable, flags, doc) }, PyMethodFlags::VARARGS | PyMethodFlags::KEYWORDS => { - let f = unsafe { method.PyCFunctionWithKeywords }; - let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - let slf = take_self_arg(&mut args, flags); - let slf_ptr = slf - .as_ref() - .map(|obj| obj.as_object().as_raw().cast_mut()) - .unwrap_or_default(); - let arg_tuple = vm.ctx.new_tuple(args.args); - let kwargs = vm.ctx.new_dict(); - for (k, v) in args.kwargs { - kwargs.set_item(&*k, v, vm)?; - } - let ret_ptr = unsafe { - f( - slf_ptr, - arg_tuple.as_object().as_raw().cast_mut(), - kwargs.as_object().as_raw().cast_mut(), - ) - }; - ret_ptr_to_pyresult(vm, ret_ptr) + let callable = move | args: FuncArgs, vm: &VirtualMachine| unsafe { + call_function_with_keywords(vm, method, flags, args) }; vm.ctx.new_method_def(name, callable, flags, doc) }, PyMethodFlags::FASTCALL | PyMethodFlags::KEYWORDS => { - let f = unsafe { method.PyCFunctionFastWithKeywords }; - let callable = move |mut args: FuncArgs, vm: &VirtualMachine| { - let slf = take_self_arg(&mut args, flags); - let slf_ptr = slf - .as_ref() - .map(|obj| obj.as_object().as_raw().cast_mut()) - .unwrap_or_default(); - let nargs = args.args.len(); - let mut fastcall_args = args.args; - let mut kwnames_tuple = None; - if !args.kwargs.is_empty() { - let mut kwnames = Vec::with_capacity(args.kwargs.len()); - for (k, v) in args.kwargs { - kwnames.push(vm.ctx.new_str(k).into()); - fastcall_args.push(v); - } - kwnames_tuple = Some(vm.ctx.new_tuple(kwnames)); - } - let fastcall_arg_ptrs = fastcall_args - .iter() - .map(|obj| obj.as_object().as_raw().cast_mut()) - .collect::>(); - let kwnames_ptr = kwnames_tuple - .as_ref() - .map(|tuple| tuple.as_object().as_raw().cast_mut()) - .unwrap_or(core::ptr::null_mut()); - let ret_ptr = unsafe { - f( - slf_ptr, - fastcall_arg_ptrs.as_ptr(), - nargs as isize, - kwnames_ptr, - ) - }; - ret_ptr_to_pyresult(vm, ret_ptr) + let callable = move |args: FuncArgs, vm: &VirtualMachine| unsafe { + call_fast_function_with_keywords(vm, method, flags, args) }; vm.ctx.new_method_def(name, callable, flags, doc) }, @@ -176,11 +109,97 @@ pub(crate) fn build_method_def(vm: &VirtualMachine, ml: &PyMethodDef) -> PyRef { - todo!("unsupported or invalid calling-convention flags") + todo!("function {name} has unsupported or invalid calling-convention flags: {flags:?}") }, }) } +unsafe fn call_function( + vm: &VirtualMachine, + method: PyMethodPointer, + flags: PyMethodFlags, + mut args: FuncArgs, +) -> PyResult { + let f = unsafe { method.PyCFunction }; + let slf = take_self_arg(&mut args, flags); + let slf_ptr = slf + .as_ref() + .map(|obj| obj.as_object().as_raw().cast_mut()) + .unwrap_or_default(); + let arg_tuple = vm.ctx.new_tuple(args.args); + let ret_ptr = unsafe { f(slf_ptr, arg_tuple.as_object().as_raw().cast_mut()) }; + ret_ptr_to_pyresult(vm, ret_ptr) +} + +unsafe fn call_function_with_keywords( + vm: &VirtualMachine, + method: PyMethodPointer, + flags: PyMethodFlags, + mut args: FuncArgs, +) -> PyResult { + let f = unsafe { method.PyCFunctionWithKeywords }; + let slf = take_self_arg(&mut args, flags); + let slf_ptr = slf + .as_ref() + .map(|obj| obj.as_object().as_raw().cast_mut()) + .unwrap_or_default(); + let arg_tuple = vm.ctx.new_tuple(args.args); + let kwargs = vm.ctx.new_dict(); + for (k, v) in args.kwargs { + kwargs.set_item(&*k, v, vm)?; + } + let ret_ptr = unsafe { + f( + slf_ptr, + arg_tuple.as_object().as_raw().cast_mut(), + kwargs.as_object().as_raw().cast_mut(), + ) + }; + ret_ptr_to_pyresult(vm, ret_ptr) +} + +unsafe fn call_fast_function_with_keywords( + vm: &VirtualMachine, + method: PyMethodPointer, + flags: PyMethodFlags, + mut args: FuncArgs, +) -> PyResult { + let f = unsafe { method.PyCFunctionFastWithKeywords }; + let slf = take_self_arg(&mut args, flags); + let slf_ptr = slf + .as_ref() + .map(|obj| obj.as_object().as_raw().cast_mut()) + .unwrap_or_default(); + let nargs = args.args.len(); + let mut fastcall_args = args.args; + let mut kwnames_tuple = None; + if !args.kwargs.is_empty() { + let mut kwnames = Vec::with_capacity(args.kwargs.len()); + for (k, v) in args.kwargs { + kwnames.push(vm.ctx.new_str(k).into()); + fastcall_args.push(v); + } + kwnames_tuple = Some(vm.ctx.new_tuple(kwnames)); + } + let fastcall_arg_ptrs = fastcall_args + .iter() + .map(|obj| obj.as_object().as_raw().cast_mut()) + .collect::>(); + let kwnames_ptr = kwnames_tuple + .as_ref() + .map(|tuple| tuple.as_object().as_raw().cast_mut()) + .unwrap_or(core::ptr::null_mut()); + let ret_ptr = unsafe { + f( + slf_ptr, + fastcall_arg_ptrs.as_ptr(), + nargs as isize, + kwnames_ptr, + ) + }; + ret_ptr_to_pyresult(vm, ret_ptr) +} + fn ret_ptr_to_pyresult(vm: &VirtualMachine, ret_ptr: *mut PyObject) -> PyResult { let ret_ptr = NonNull::new(ret_ptr).ok_or_else(|| { vm.take_raised_exception() From f2b889dd770beee216e1860bd4700c29ecdc7dd8 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 12 May 2026 13:40:52 +0200 Subject: [PATCH 120/135] Make PyMethodDef members public --- crates/capi/src/methodobject.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index bb325debd94..f12ffb18872 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -12,28 +12,28 @@ define_py_check!(exact PyCFunction_CheckExact, types.builtin_function_or_method_ #[repr(C)] pub struct PyMethodDef { - pub(crate) ml_name: *const c_char, - pub(crate) ml_meth: PyMethodPointer, - pub(crate) ml_flags: c_int, - pub(crate) ml_doc: *const c_char, + pub ml_name: *const c_char, + pub ml_meth: PyMethodPointer, + pub ml_flags: c_int, + pub ml_doc: *const c_char, } #[repr(C)] #[derive(Copy, Clone)] #[allow(non_snake_case)] pub union PyMethodPointer { - PyCFunction: unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject, - PyCFunctionWithKeywords: unsafe extern "C" fn( + pub PyCFunction: unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject, + pub PyCFunctionWithKeywords: unsafe extern "C" fn( slf: *mut PyObject, args: *mut PyObject, kwargs: *mut PyObject, ) -> *mut PyObject, - PyCFunctionFast: unsafe extern "C" fn( + pub PyCFunctionFast: unsafe extern "C" fn( slf: *mut PyObject, args: *mut *mut PyObject, nargs: isize, ) -> *mut PyObject, - PyCFunctionFastWithKeywords: unsafe extern "C" fn( + pub PyCFunctionFastWithKeywords: unsafe extern "C" fn( slf: *mut PyObject, args: *const *mut PyObject, nargs: isize, From 83ca45e94519cf37438bf53ba4cfb065cdd0311d Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 12 May 2026 13:55:57 +0200 Subject: [PATCH 121/135] Add `PyCFunction_New` & `PyCFunction_NewEx` --- crates/capi/src/methodobject.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index f12ffb18872..b9946cf591c 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -226,7 +226,7 @@ pub extern "C" fn PyCMethod_New( with_vm(|vm| -> PyResult { assert!( _cls.is_null(), - "PyCMethod_New does not support METH_METHOD yet" + "PyCMethod_New does not support METH_METHOD on abi3" ); let ml = unsafe { &*ml }; let zelf = unsafe { slf.as_ref().map(|obj| obj.to_owned()) }; @@ -234,6 +234,20 @@ pub extern "C" fn PyCMethod_New( }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyCFunction_New(ml: *mut PyMethodDef, slf: *mut PyObject) -> *mut PyObject { + PyCMethod_New(ml, slf, core::ptr::null_mut(), core::ptr::null_mut()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCFunction_NewEx( + ml: *mut PyMethodDef, + slf: *mut PyObject, + module: *mut PyObject, +) -> *mut PyObject { + PyCMethod_New(ml, slf, module, core::ptr::null_mut()) +} + #[cfg(test)] mod tests { use pyo3::exceptions::PyException; From 265991fe120924fed0e89f58d41242bb61539008 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 12 May 2026 15:32:50 +0200 Subject: [PATCH 122/135] Fix self handling in `METH_NOARGS` functions --- crates/capi/src/methodobject.rs | 60 ++++++++++++++++++++++++--------- crates/capi/src/object.rs | 2 +- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index b9946cf591c..ac544c566df 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -4,7 +4,7 @@ use crate::object::define_py_check; use crate::pystate::with_vm; use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; -use rustpython_vm::function::{FuncArgs, HeapMethodDef, PyMethodFlags}; +use rustpython_vm::function::{FuncArgs, HeapMethodDef, PosArgs, PyMethodFlags}; use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; define_py_check!(PyCFunction_Check, types.builtin_function_or_method_type); @@ -41,7 +41,11 @@ pub union PyMethodPointer { ) -> *mut PyObject, } -pub(crate) fn build_method_def(vm: &VirtualMachine, ml: &PyMethodDef) -> PyRef { +pub(crate) fn build_method_def( + vm: &VirtualMachine, + ml: &PyMethodDef, + has_self: bool, +) -> PyRef { let name = unsafe { CStr::from_ptr(ml.ml_name) } .to_str() .expect("Method name was not valid UTF-8"); @@ -70,15 +74,21 @@ pub(crate) fn build_method_def(vm: &VirtualMachine, ml: &PyMethodDef) -> PyRef { - let callable = move |args: FuncArgs, vm: &VirtualMachine| unsafe { - debug_assert!(args.args.len() <= 1 && args.kwargs.is_empty(), "Expected no arguments, but got some"); - call_function(vm, method, flags, args) - }; - vm.ctx.new_method_def(name, callable, flags, doc) + if has_self { + let callable = move |zelf: PyObjectRef, vm: &VirtualMachine| unsafe { + call_function(vm, method, flags, Some(vec![zelf])) + }; + vm.ctx.new_method_def(name, callable, flags, doc) + } else { + let callable = move |vm: &VirtualMachine| unsafe { + call_function::(vm, method, flags, None) + }; + vm.ctx.new_method_def(name, callable, flags, doc) + } }, PyMethodFlags::VARARGS => { - let callable = move |args: FuncArgs, vm: &VirtualMachine| unsafe { - call_function(vm, method, flags, args) + let callable = move |args: PosArgs, vm: &VirtualMachine| unsafe { + call_function(vm, method, flags, Some(args)) }; vm.ctx.new_method_def(name, callable, flags, doc) }, @@ -95,7 +105,7 @@ pub(crate) fn build_method_def(vm: &VirtualMachine, ml: &PyMethodDef) -> PyRef { - let callable = move |_args: FuncArgs, _vm: &VirtualMachine| -> PyResult { + let callable = move |_args: PosArgs, _vm: &VirtualMachine| -> PyResult { todo!("METH_FASTCALL without METH_KEYWORDS is not supported yet") }; vm.ctx.new_method_def(name, callable, flags, doc) @@ -114,20 +124,32 @@ pub(crate) fn build_method_def(vm: &VirtualMachine, ml: &PyMethodDef) -> PyRef>( vm: &VirtualMachine, method: PyMethodPointer, flags: PyMethodFlags, - mut args: FuncArgs, + args: Option, ) -> PyResult { let f = unsafe { method.PyCFunction }; - let slf = take_self_arg(&mut args, flags); + let (slf, arg_tuple) = if let Some(mut args) = args.map(Into::into) { + let slf = take_self_arg(&mut args, flags); + let arg_tuple = vm.ctx.new_tuple(args.args); + (slf, Some(arg_tuple)) + } else { + (None, None) + }; + let slf_ptr = slf .as_ref() .map(|obj| obj.as_object().as_raw().cast_mut()) .unwrap_or_default(); - let arg_tuple = vm.ctx.new_tuple(args.args); - let ret_ptr = unsafe { f(slf_ptr, arg_tuple.as_object().as_raw().cast_mut()) }; + + let arg_ptr = arg_tuple + .as_ref() + .map(|tuple| tuple.as_object().as_raw().cast_mut()) + .unwrap_or_default(); + + let ret_ptr = unsafe { f(slf_ptr, arg_ptr) }; ret_ptr_to_pyresult(vm, ret_ptr) } @@ -230,7 +252,9 @@ pub extern "C" fn PyCMethod_New( ); let ml = unsafe { &*ml }; let zelf = unsafe { slf.as_ref().map(|obj| obj.to_owned()) }; - Ok(build_method_def(vm, ml).build_function(vm, zelf).into()) + Ok(build_method_def(vm, ml, zelf.is_some()) + .build_function(vm, zelf) + .into()) }) } @@ -273,6 +297,7 @@ mod tests { Python::attach(|py| { unsafe extern "C" fn c_fn(_self: *mut PyObject, _args: *mut PyObject) -> *mut PyObject { assert!(_self.is_null()); + assert!(_args.is_null()); unsafe { PyLong_FromLong(4200) } } @@ -286,6 +311,9 @@ mod tests { .extract::() .unwrap(); assert_eq!(result, 4200); + + assert!(py_fn.call((1,), None).is_err()); + assert!(py_fn.call((1, 2), None).is_err()); }) } diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 24831d5983c..cbeab00fed3 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -330,7 +330,7 @@ pub extern "C" fn PyType_FromSpec(spec: *mut PyType_Spec) -> *mut PyObject { .to_str() .expect("method name must be valid UTF-8") }; - let method = build_method_def(vm, method).build_method(class_static, vm); + let method = build_method_def(vm, method, true).build_method(class_static, vm); class .attributes .write() From 93b40121cd99cfa0c169856eb5da340b35bd7fa9 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 12 May 2026 15:41:01 +0200 Subject: [PATCH 123/135] Fix handling of self in `METH_O` functions --- crates/capi/src/methodobject.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index ac544c566df..094142ab874 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -112,11 +112,19 @@ pub(crate) fn build_method_def( }, PyMethodFlags::O => { let f = unsafe { method.PyCFunction }; - let callable = move |zelf: PyObjectRef, arg: PyObjectRef, vm: &VirtualMachine| -> PyResult { - let ret_ptr = unsafe { f(zelf.as_raw().cast_mut(), arg.as_raw().cast_mut()) }; - ret_ptr_to_pyresult(vm, ret_ptr) - }; - vm.ctx.new_method_def(name, callable, flags, doc) + if has_self { + let callable = move |zelf: PyObjectRef, arg: PyObjectRef, vm: &VirtualMachine| -> PyResult { + let ret_ptr = unsafe { f(zelf.as_raw().cast_mut(), arg.as_raw().cast_mut()) }; + ret_ptr_to_pyresult(vm, ret_ptr) + }; + vm.ctx.new_method_def(name, callable, flags, doc) + } else { + let callable = move |arg: PyObjectRef, vm: &VirtualMachine| -> PyResult { + let ret_ptr = unsafe { f(std::ptr::null_mut(), arg.as_raw().cast_mut()) }; + ret_ptr_to_pyresult(vm, ret_ptr) + }; + vm.ctx.new_method_def(name, callable, flags, doc) + } }, _ => { todo!("function {name} has unsupported or invalid calling-convention flags: {flags:?}") @@ -231,10 +239,10 @@ fn ret_ptr_to_pyresult(vm: &VirtualMachine, ret_ptr: *mut PyObject) -> PyResult } fn take_self_arg(args: &mut FuncArgs, flags: PyMethodFlags) -> Option { - if flags.contains(PyMethodFlags::STATIC) | args.args.is_empty() { + if flags.contains(PyMethodFlags::STATIC) { None } else { - Some(args.args.remove(0)) + args.take_positional() } } From b3a85117ec8e70554f2605a8cc756a3464ceef29 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 12 May 2026 16:13:21 +0200 Subject: [PATCH 124/135] Implement `PyObject_IsSubclass` & `PyObject_IsInstance` --- crates/capi/capi_todo.md | 2 -- crates/capi/src/abstract_.rs | 18 ++++++++++++++++++ crates/capi/src/object.rs | 2 +- crates/capi/src/test.rs | 1 + 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 crates/capi/src/test.rs diff --git a/crates/capi/capi_todo.md b/crates/capi/capi_todo.md index d6e4960ac71..3c024faaa33 100644 --- a/crates/capi/capi_todo.md +++ b/crates/capi/capi_todo.md @@ -19,8 +19,6 @@ RustPython C API target: `crates/capi/src/abstract_.rs` - `PyNumber_Rshift` - `PyNumber_Subtract` - `PyObject_GetIter` -- `PyObject_IsInstance` -- `PyObject_IsSubclass` - `PyObject_Size` - `PySequence_Check` - `PySequence_Concat` diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index 631102b9214..208370816a0 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -158,6 +158,24 @@ pub extern "C" fn PyObject_DelItem(obj: *mut PyObject, key: *mut PyObject) -> c_ }) } +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_IsSubclass(derived: *mut PyObject, cls: *mut PyObject) -> c_int { + with_vm(|vm| { + let derived = unsafe { &*derived }; + let cls = unsafe { &*cls }; + derived.is_subclass(cls, vm) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyObject_IsInstance(inst: *mut PyObject, cls: *mut PyObject) -> c_int { + with_vm(|vm| { + let inst = unsafe { &*inst }; + let cls = unsafe { &*cls }; + inst.is_instance(cls, vm) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn PySequence_Contains(obj: *mut PyObject, value: *mut PyObject) -> c_int { with_vm(|vm| { diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index cbeab00fed3..c047298e42e 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -596,7 +596,7 @@ mod tests { #[classmethod] fn cls_method(cls: &Bound<'_, PyType>) -> PyResult { - // assert!(cls.is_subclass_of::()?); + assert!(cls.is_subclass_of::()?); Ok(10) } } diff --git a/crates/capi/src/test.rs b/crates/capi/src/test.rs new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/crates/capi/src/test.rs @@ -0,0 +1 @@ + From cf7a8ef32a2584317a864cd8c4f862071599bc3a Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 13 May 2026 09:54:28 +0200 Subject: [PATCH 125/135] Discard `self` on static functions --- crates/capi/src/object.rs | 35 +++++++++++++++++++++++++++++++---- crates/capi/src/test.rs | 1 - 2 files changed, 31 insertions(+), 5 deletions(-) delete mode 100644 crates/capi/src/test.rs diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index c047298e42e..edf01c7cfae 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -4,7 +4,7 @@ use core::ffi::{CStr, c_char, c_int, c_uint, c_ulong, c_void}; use core::ptr::NonNull; use rustpython_vm::builtins::{PyDict, PyStr, PyTuple, PyType}; use rustpython_vm::convert::IntoObject; -use rustpython_vm::function::FuncArgs; +use rustpython_vm::function::{FuncArgs, PyMethodFlags}; use rustpython_vm::types::{PyTypeFlags, PyTypeSlots, SlotAccessor}; use rustpython_vm::{AsObject, Context, Py, PyObjectRef, PyResult, VirtualMachine}; @@ -330,7 +330,9 @@ pub extern "C" fn PyType_FromSpec(spec: *mut PyType_Spec) -> *mut PyObject { .to_str() .expect("method name must be valid UTF-8") }; - let method = build_method_def(vm, method, true).build_method(class_static, vm); + let is_static = PyMethodFlags::from_bits_retain(method.ml_flags as _) + .contains(PyMethodFlags::STATIC); + let method = build_method_def(vm, method, !is_static).build_method(class_static, vm); class .attributes .write() @@ -589,11 +591,20 @@ mod tests { Ok(self.num + 10) } + fn method2(&self, a: i32) -> PyResult { + Ok(self.num + a) + } + #[staticmethod] - fn static_method(a: i32, b: i32) -> PyResult { + fn static_method1(a: i32, b: i32) -> PyResult { Ok(a + b) } + #[staticmethod] + fn static_method2() -> PyResult { + Ok(0) + } + #[classmethod] fn cls_method(cls: &Bound<'_, PyType>) -> PyResult { assert!(cls.is_subclass_of::()?); @@ -618,13 +629,29 @@ mod tests { ); assert_eq!( - obj.call_method1("static_method", (5, 8)) + obj.call_method1("method2", (5,)) + .unwrap() + .extract::() + .unwrap(), + 8 + ); + + assert_eq!( + obj.call_method1("static_method1", (5, 8)) .unwrap() .extract::() .unwrap(), 13 ); + assert_eq!( + obj.call_method1("static_method2", ()) + .unwrap() + .extract::() + .unwrap(), + 0 + ); + assert_eq!( obj.call_method1("cls_method", ()) .unwrap() diff --git a/crates/capi/src/test.rs b/crates/capi/src/test.rs deleted file mode 100644 index 8b137891791..00000000000 --- a/crates/capi/src/test.rs +++ /dev/null @@ -1 +0,0 @@ - From e7af38ea2848e51db021503b0692001853c86879 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 20 May 2026 11:26:21 +0200 Subject: [PATCH 126/135] Fix `Py_GetVersion` --- crates/capi/src/pylifecycle.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index fe61cee283f..52feaf2c719 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -2,9 +2,10 @@ use crate::get_main_interpreter; use crate::pyerrors::init_exception_statics; use crate::pystate::ensure_thread_has_vm_attached; use core::ffi::{c_char, c_int}; -use rustpython_vm::version::get_version; +use rustpython_vm::version::{MAJOR, MICRO, MINOR, VERSION_HEX}; use rustpython_vm::vm::thread::ThreadedVirtualMachine; use rustpython_vm::{Context, Interpreter}; +use std::ffi::c_ulong; use std::sync::{LazyLock, Mutex}; pub(crate) static MAIN_INTERP: Mutex> = Mutex::new(None); @@ -17,6 +18,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 @@ -56,7 +60,7 @@ pub extern "C" fn Py_IsFinalizing() -> c_int { #[unsafe(no_mangle)] pub extern "C" fn Py_GetVersion() -> *const c_char { - static VERSION: LazyLock = LazyLock::new(get_version); + static VERSION: LazyLock = LazyLock::new(|| format!("{MAJOR}.{MINOR}.{MICRO}")); VERSION.as_str().as_ptr() as *const c_char } @@ -70,5 +74,7 @@ mod tests { let version = py.version_info(); assert!(version >= (3, 14)); }); + + assert!(unsafe { pyo3::ffi::Py_Version } >= 0x030d0000); } } From a8acad3288a0069ee1980f68460c29659d0dfb10 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 20 May 2026 16:51:32 +0200 Subject: [PATCH 127/135] Rename `pycapsule` --- crates/capi/src/lib.rs | 2 +- crates/capi/src/{capsule.rs => pycapsule.rs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename crates/capi/src/{capsule.rs => pycapsule.rs} (100%) diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 9f8aebeef9c..507ec30f422 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -12,7 +12,7 @@ extern crate alloc; pub mod abstract_; pub mod boolobject; pub mod bytesobject; -pub mod capsule; +pub mod pycapsule; pub mod ceval; pub mod complexobject; pub mod dictobject; diff --git a/crates/capi/src/capsule.rs b/crates/capi/src/pycapsule.rs similarity index 100% rename from crates/capi/src/capsule.rs rename to crates/capi/src/pycapsule.rs From 80ea41848ca88eba84484fb83442b199e51cc94e Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 20 May 2026 17:24:27 +0200 Subject: [PATCH 128/135] Implement remaining capsule functions --- crates/capi/src/lib.rs | 2 +- crates/capi/src/pycapsule.rs | 123 +++++++++++++++++++++++++----- crates/capi/src/util.rs | 8 ++ crates/vm/src/builtins/capsule.rs | 25 +++++- crates/vm/src/vm/context.rs | 6 +- 5 files changed, 142 insertions(+), 22 deletions(-) diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 507ec30f422..b707eacf5fd 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -12,7 +12,6 @@ extern crate alloc; pub mod abstract_; pub mod boolobject; pub mod bytesobject; -pub mod pycapsule; pub mod ceval; pub mod complexobject; pub mod dictobject; @@ -24,6 +23,7 @@ pub mod methodobject; pub mod moduleobject; pub mod object; pub mod objimpl; +pub mod pycapsule; pub mod pyerrors; pub mod pylifecycle; pub mod pystate; diff --git a/crates/capi/src/pycapsule.rs b/crates/capi/src/pycapsule.rs index 5b3a6b103e6..0eedf2c5f14 100644 --- a/crates/capi/src/pycapsule.rs +++ b/crates/capi/src/pycapsule.rs @@ -1,7 +1,9 @@ use crate::PyObject; use crate::pystate::with_vm; -use core::ffi::{c_char, c_int, c_void}; +use core::ffi::{CStr, c_char, c_int, c_void}; +use core::ptr::NonNull; use rustpython_vm::builtins::PyCapsule; +use rustpython_vm::{AsObject, Py, PyObjectRef, PyResult, VirtualMachine}; #[allow(non_camel_case_types)] pub type PyCapsule_Destructor = unsafe extern "C" fn(capsule: *mut PyObject); @@ -9,39 +11,124 @@ pub type PyCapsule_Destructor = unsafe extern "C" fn(capsule: *mut PyObject); #[unsafe(no_mangle)] pub extern "C" fn PyCapsule_New( pointer: *mut c_void, - _name: *const c_char, + name: *const c_char, destructor: Option, ) -> *mut PyObject { - with_vm(|vm| vm.ctx.new_capsule(pointer, destructor)) + with_vm(|vm| { + if pointer.is_null() { + return Err(vm.new_value_error("PyCapsule_New called with null pointer")); + } + let name = NonNull::new(name.cast_mut()).map(|ptr| unsafe { CStr::from_ptr(ptr.as_ptr()) }); + Ok(vm.ctx.new_capsule(pointer, name, destructor)) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCapsule_GetPointer(capsule: *mut PyObject, name: *const c_char) -> *mut c_void { + with_vm(|vm| Ok(checked_capsule(vm, unsafe { &*capsule }, name)?.pointer())) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCapsule_GetName(capsule: *mut PyObject) -> *const c_char { + with_vm(|vm| { + let capsule = unsafe { &*capsule } + .downcast_ref_if_exact::(vm) + .ok_or_else(|| vm.new_value_error("Invalid capsule"))?; + Ok(capsule.name().map(CStr::as_ptr).unwrap_or_default()) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCapsule_GetContext(capsule: *mut PyObject) -> *mut c_void { + with_vm(|vm| { + let capsule = unsafe { &*capsule } + .downcast_ref_if_exact::(vm) + .ok_or_else(|| vm.new_value_error("Invalid capsule"))?; + Ok(capsule.context()) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCapsule_SetContext(capsule: *mut PyObject, context: *mut c_void) -> c_int { + with_vm(|vm| { + let capsule = unsafe { &*capsule } + .downcast_ref_if_exact::(vm) + .ok_or_else(|| vm.new_value_error("Invalid capsule"))?; + Ok(capsule.set_context(context)) + }) } #[unsafe(no_mangle)] -pub extern "C" fn PyCapsule_GetPointer( - capsule: *mut PyObject, - _name: *const c_char, -) -> *mut c_void { +pub extern "C" fn PyCapsule_SetPointer(capsule: *mut PyObject, pointer: *mut c_void) -> c_int { with_vm(|vm| { - let capsule = unsafe { &*capsule }.try_downcast_ref::(vm)?; - Ok(capsule.pointer()) + let capsule = unsafe { &*capsule } + .downcast_ref_if_exact::(vm) + .ok_or_else(|| vm.new_value_error("Invalid capsule"))?; + Ok(capsule.set_pointer(pointer)) }) } #[unsafe(no_mangle)] -pub extern "C" fn PyCapsule_IsValid(capsule: *mut PyObject, _name: *const c_char) -> c_int { +pub extern "C" fn PyCapsule_IsValid(capsule: *mut PyObject, name: *const c_char) -> c_int { with_vm(|vm| { - let Some(capsule) = - unsafe { capsule.as_ref() }.and_then(|obj| obj.downcast_ref_if_exact::(vm)) - else { - return false; - }; + if capsule.is_null() { + return false + } - !capsule.pointer().is_null() + checked_capsule(vm, unsafe { &*capsule }, name).is_ok() }) } #[unsafe(no_mangle)] -pub extern "C" fn PyCapsule_GetName(_capsule: *mut PyObject) -> *const c_char { - core::ptr::null_mut() +pub extern "C" fn PyCapsule_Import(name: *const c_char, _no_block: c_int) -> *mut c_void { + with_vm(|vm| { + let capsule_name = unsafe { CStr::from_ptr(name) } + .to_str() + .map_err(|_| vm.new_system_error("capsule name is not valid UTF-8"))?; + let (module_name, attrs_path) = capsule_name.split_once('.').ok_or_else(|| { + vm.new_import_error( + "capsule name is missing attribute path", + vm.ctx.new_str(capsule_name), + ) + })?; + let mut obj: PyObjectRef = vm.import(module_name, 0)?.into(); + + for attr in attrs_path.split('.') { + obj = obj.get_attr(attr, vm)?; + } + + Ok(checked_capsule(vm, &obj, name)?.pointer()) + }) +} + +#[inline] +fn names_match(stored_name: *const c_char, expected_name: *const c_char) -> bool { + if stored_name.is_null() || expected_name.is_null() { + stored_name.is_null() && expected_name.is_null() + } else { + unsafe { CStr::from_ptr(stored_name) == CStr::from_ptr(expected_name) } + } +} + +#[inline] +fn checked_capsule<'a>( + vm: &VirtualMachine, + obj: &'a PyObject, + name: *const c_char, +) -> PyResult<&'a PyCapsule> { + let capsule = obj + .downcast_ref_if_exact::(vm) + .ok_or_else(|| vm.new_value_error("Invalid capsule"))?; + + if !names_match(capsule.name().map(CStr::as_ptr).unwrap_or_default(), name) { + return Err(vm.new_value_error("Capsule name does not match")); + } + + if capsule.pointer().is_null() { + return Err(vm.new_value_error("Capsule has null pointer")); + } + + Ok(capsule) } #[cfg(test)] diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 9119581902a..6eef9163a5c 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -76,6 +76,14 @@ impl FfiResult<*mut c_char> for *const u8 { } } +impl FfiResult for *const c_char { + const ERR_VALUE: *const c_char = core::ptr::null_mut(); + + fn into_output(self, _vm: &VirtualMachine) -> *const c_char { + self + } +} + impl FfiResult for usize { const ERR_VALUE: isize = -1; diff --git a/crates/vm/src/builtins/capsule.rs b/crates/vm/src/builtins/capsule.rs index 33680fb1973..9f54146813d 100644 --- a/crates/vm/src/builtins/capsule.rs +++ b/crates/vm/src/builtins/capsule.rs @@ -4,7 +4,7 @@ use crate::{ class::PyClassImpl, types::{Destructor, Representable}, }; -use core::ffi::c_void; +use core::ffi::{CStr, c_void}; use core::sync::atomic::AtomicPtr; /// PyCapsule - a container for C pointers. @@ -13,6 +13,8 @@ use core::sync::atomic::AtomicPtr; #[derive(Debug)] pub struct PyCapsule { ptr: AtomicPtr, + context: AtomicPtr, + name: Option<&'static CStr>, destructor: Option, } @@ -27,10 +29,13 @@ impl PyPayload for PyCapsule { impl PyCapsule { pub fn new( ptr: *mut c_void, + name: Option<&'static CStr>, destructor: Option, ) -> Self { Self { ptr: ptr.into(), + context: core::ptr::null_mut::().into(), + name, destructor, } } @@ -39,6 +44,24 @@ impl PyCapsule { self.ptr.load(core::sync::atomic::Ordering::Relaxed) } + pub fn set_pointer(&self, pointer: *mut c_void) { + self.ptr + .store(pointer, core::sync::atomic::Ordering::Relaxed); + } + + pub fn context(&self) -> *mut c_void { + self.context.load(core::sync::atomic::Ordering::Relaxed) + } + + pub fn set_context(&self, context: *mut c_void) { + self.context + .store(context, core::sync::atomic::Ordering::Relaxed); + } + + pub fn name(&self) -> Option<&'static CStr> { + self.name + } + fn destructor(&self) -> Option { self.destructor } diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index 8d20e750706..4d16c5d8075 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -26,6 +26,7 @@ use crate::{ object::{Py, PyObjectPayload, PyObjectRef, PyPayload, PyRef}, types::{PyTypeFlags, PyTypeSlots, TypeZoo}, }; +use core::ffi::{CStr, c_void}; use malachite_bigint::BigInt; use num_complex::Complex64; use num_traits::ToPrimitive; @@ -754,10 +755,11 @@ impl Context { pub fn new_capsule( &self, - ptr: *mut core::ffi::c_void, + ptr: *mut c_void, + name: Option<&'static CStr>, destructor: Option, ) -> PyRef { - PyCapsule::new(ptr, destructor).into_ref(self) + PyCapsule::new(ptr, name, destructor).into_ref(self) } } From 961bfc482c552955534b069bfb2b9e6fe9ff8d80 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 21 May 2026 08:53:40 +0200 Subject: [PATCH 129/135] Update todo list --- crates/capi/capi_todo.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/capi/capi_todo.md b/crates/capi/capi_todo.md index 927c52bdcec..876c271cfe3 100644 --- a/crates/capi/capi_todo.md +++ b/crates/capi/capi_todo.md @@ -138,15 +138,6 @@ RustPython C API target: `crates/capi/src/pybuffer.rs` (not present yet) - `PyBuffer_ToContiguous` - `PyObject_GetBuffer` -## `pycapsule.h` - -RustPython C API target: `crates/capi/src/capsule.rs` - -- `PyCapsule_GetContext` -- `PyCapsule_Import` -- `PyCapsule_SetContext` -- `PyCapsule_SetPointer` - ## `pyerrors.h` RustPython C API target: `crates/capi/src/pyerrors.rs` From dcb5973b67843354b413e17ff4728fade8684a5b Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Tue, 26 May 2026 16:33:26 +0200 Subject: [PATCH 130/135] Update pyo3 --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a7c361b417a..98433da8db0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2828,7 +2828,7 @@ dependencies = [ [[package]] name = "pyo3" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#eef62776f3e5cf32505b77b69dacaecf2a77caf2" +source = "git+https://github.com/PyO3/pyo3#08dde9fcc0deb58685858bbc2cfca6e51ffab337" dependencies = [ "libc", "once_cell", @@ -2841,7 +2841,7 @@ dependencies = [ [[package]] name = "pyo3-build-config" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#eef62776f3e5cf32505b77b69dacaecf2a77caf2" +source = "git+https://github.com/PyO3/pyo3#08dde9fcc0deb58685858bbc2cfca6e51ffab337" dependencies = [ "target-lexicon", ] @@ -2849,7 +2849,7 @@ dependencies = [ [[package]] name = "pyo3-ffi" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#eef62776f3e5cf32505b77b69dacaecf2a77caf2" +source = "git+https://github.com/PyO3/pyo3#08dde9fcc0deb58685858bbc2cfca6e51ffab337" dependencies = [ "libc", "pyo3-build-config", @@ -2858,7 +2858,7 @@ dependencies = [ [[package]] name = "pyo3-macros" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#eef62776f3e5cf32505b77b69dacaecf2a77caf2" +source = "git+https://github.com/PyO3/pyo3#08dde9fcc0deb58685858bbc2cfca6e51ffab337" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2869,7 +2869,7 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" version = "0.28.3" -source = "git+https://github.com/PyO3/pyo3#eef62776f3e5cf32505b77b69dacaecf2a77caf2" +source = "git+https://github.com/PyO3/pyo3#08dde9fcc0deb58685858bbc2cfca6e51ffab337" dependencies = [ "heck", "proc-macro2", From 16a34b9d5c274a0dba2281c14f65b62ff4771663 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 27 May 2026 15:23:08 +0200 Subject: [PATCH 131/135] Implement `PyDict_SetDefaultRef` --- crates/capi/src/dictobject.rs | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/capi/src/dictobject.rs b/crates/capi/src/dictobject.rs index ffdb1eb62df..7ac18194074 100644 --- a/crates/capi/src/dictobject.rs +++ b/crates/capi/src/dictobject.rs @@ -56,6 +56,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| { From ea5efa7e3417a46af933cac6d9e713eb695d440e Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 1 Jun 2026 14:58:08 +0200 Subject: [PATCH 132/135] Add agent skill --- .../skills/rustpython-capi-expansion/SKILL.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .agents/skills/rustpython-capi-expansion/SKILL.md diff --git a/.agents/skills/rustpython-capi-expansion/SKILL.md b/.agents/skills/rustpython-capi-expansion/SKILL.md new file mode 100644 index 00000000000..fb74b69eb2c --- /dev/null +++ b/.agents/skills/rustpython-capi-expansion/SKILL.md @@ -0,0 +1,59 @@ +--- +name: rustpython-capi-expansion +description: Implement missing RustPython C-API functions in crates/capi using the pyo3-ffi header split mapping (`pyo3-ffi/src/*.rs`, mirroring CPython C API headers). Use this whenever the user asks to add or port C-API functions (for example from setobject.h, dictobject.h, unicodeobject.h) or add capi tests. +--- + +# RustPython C-API Expansion + +Use this workflow for adding missing C-API functions to RustPython. + +## Source of truth for target files + +- Use this mapping source: `pyo3-ffi/src/*.rs`, which mirrors the CPython header split used by the C API. +- Map requested header APIs to `crates/capi/src/.rs` using that split. Examples: + - `setobject.h` -> `crates/capi/src/setobject.rs` + - `dictobject.h` -> `crates/capi/src/dictobject.rs` + - `unicodeobject.h` -> `crates/capi/src/unicodeobject.rs` +- Do not invent alternate target modules when the header split implies a direct target. +- If the target file is not present yet, create it and wire it in `crates/capi/src/lib.rs`. + +## Implementation workflow + +1. Identify requested missing APIs from the user request and their originating C API header. +2. Open nearby capi modules in `crates/capi/src/` and follow existing style and patterns. +3. Implement only the requested functions in the mapped target file. +4. Keep behavior aligned with CPython C-API contracts. +5. Prefer using existing `rustpython-vm` functionality as much as possible instead of re-implementing behavior in capi. +6. If a needed `rustpython-vm` helper exists but is private, make it public with a minimal, focused visibility change. +7. Prefer direct contract assumptions over defensive null checks unless required by the established local style. +8. Add basic tests only; do not overfit with very specific edge-case clutter. +9. In tests, use `pyo3` only as a safe wrapper over the API. Avoid raw pointer-heavy direct FFI-style tests. +10. Run tests from `crates/capi`. + +## Testing rules + +- Run test commands with working directory set to `crates/capi`. +- Prefer targeted tests first (module/function filter), then broader capi tests if needed. +- Keep test names concise (no required `test_` prefix). + +## Style rules + +- Follow existing RustPython capi coding style in neighboring files. +- Reuse `rustpython-vm` methods and types first; avoid duplicating VM logic in capi wrappers. +- When exposing previously private VM helpers, keep the API surface minimal and avoid unrelated refactors. +- Add comments only when they explain non-obvious behavior. +- Keep edits minimal and focused on requested API expansion. + +## Completion checklist + +- [ ] All requested functions implemented in mapped target file. +- [ ] New module exported in `crates/capi/src/lib.rs` when applicable. +- [ ] Basic safe-wrapper `pyo3` tests added/updated. +- [ ] Tests executed from `crates/capi` and passing for changed area. +- [ ] Final response includes changed file paths and test command summary. + +## Example prompts this skill should handle + +- "Implement these missing functions from `dictobject.h`." +- "Add `setobject.h` C-API functions in RustPython and include basic tests." +- "Port the listed `unicodeobject.h` APIs in capi, follow existing style, and run tests from `crates/capi`." From 6bf855f58372f5114e2f67c37135e5392fdb34f5 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 1 Jun 2026 15:13:10 +0200 Subject: [PATCH 133/135] Add `set` object functions to c-api --- Cargo.lock | 1 + crates/capi/Cargo.toml | 1 + crates/capi/capi_todo.md | 13 +-- crates/capi/src/lib.rs | 1 + crates/capi/src/setobject.rs | 162 ++++++++++++++++++++++++++++++++++ crates/vm/src/builtins/set.rs | 10 +-- 6 files changed, 171 insertions(+), 17 deletions(-) create mode 100644 crates/capi/src/setobject.rs diff --git a/Cargo.lock b/Cargo.lock index 5aaa8d533bd..a0cbb7465e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3473,6 +3473,7 @@ name = "rustpython-capi" version = "0.5.0" dependencies = [ "bitflags 2.11.1", + "itertools 0.14.0", "num-complex", "pyo3", "rustpython-stdlib", diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index ef9161e4d46..34179c7fd19 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] bitflags = { workspace = true } +itertools = { workspace = true } num-complex = { workspace = true } rustpython-vm = { workspace = true, features = ["threading", "compiler"] } rustpython-stdlib = {workspace = true, features = ["threading"] } diff --git a/crates/capi/capi_todo.md b/crates/capi/capi_todo.md index 876c271cfe3..48c86929c76 100644 --- a/crates/capi/capi_todo.md +++ b/crates/capi/capi_todo.md @@ -154,18 +154,7 @@ RustPython C API target: `crates/capi/src/pystate.rs` ## `setobject.h` -RustPython C API target: `crates/capi/src/setobject.rs` (not present yet) - -- `PyFrozenSet_Check` -- `PyFrozenSet_New` -- `PySet_Add` -- `PySet_Check` -- `PySet_Clear` -- `PySet_Contains` -- `PySet_Discard` -- `PySet_New` -- `PySet_Pop` -- `PySet_Size` +RustPython C API target: `crates/capi/src/setobject.rs` ## `sliceobject.h` diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index b707eacf5fd..aed3786d82f 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -28,6 +28,7 @@ pub mod pyerrors; pub mod pylifecycle; pub mod pystate; pub mod refcount; +pub mod setobject; pub mod traceback; pub mod tupleobject; pub mod unicodeobject; diff --git a/crates/capi/src/setobject.rs b/crates/capi/src/setobject.rs new file mode 100644 index 00000000000..1036bb1473a --- /dev/null +++ b/crates/capi/src/setobject.rs @@ -0,0 +1,162 @@ +use crate::PyObject; +use crate::object::define_py_check; +use crate::pystate::with_vm; +use core::ffi::c_int; +use itertools::process_results; +use rustpython_vm::AsObject; +use rustpython_vm::PyPayload; +use rustpython_vm::TryFromObject; +use rustpython_vm::builtins::{PyFrozenSet, PySet}; +use rustpython_vm::function::ArgIterable; + +define_py_check!(fn PySet_Check, types.set_type); +define_py_check!(fn PyFrozenSet_Check, types.frozenset_type); + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PySet_New(iterable: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + if iterable.is_null() { + return Ok(PySet::default().into_ref(&vm.ctx)); + } + + let iterable = ArgIterable::try_from_object(vm, unsafe { &*iterable }.to_owned())?; + let set = PySet::default().into_ref(&vm.ctx); + for item in iterable.iter(vm)? { + set.add(item?, vm)?; + } + Ok(set) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyFrozenSet_New(iterable: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + if iterable.is_null() { + return Ok(vm.ctx.empty_frozenset.to_owned()); + } + + let iterable = ArgIterable::try_from_object(vm, unsafe { &*iterable }.to_owned())?; + let set = process_results(iterable.iter(vm)?, |it| PyFrozenSet::from_iter(vm, it))??; + Ok(set.into_ref(&vm.ctx)) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PySet_Add(set: *mut PyObject, key: *mut PyObject) -> c_int { + with_vm(|vm| { + let set = unsafe { &*set }.try_downcast_ref::(vm)?; + let key = unsafe { &*key }.to_owned(); + set.add(key, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PySet_Clear(set: *mut PyObject) -> c_int { + with_vm(|vm| { + let set = unsafe { &*set }.try_downcast_ref::(vm)?; + set.clear(); + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PySet_Contains(anyset: *mut PyObject, key: *mut PyObject) -> c_int { + with_vm(|vm| { + let anyset = unsafe { &*anyset }; + let key = unsafe { &*key }; + + if let Some(set) = anyset.downcast_ref::() { + set.__contains__(key, vm) + } else if let Some(frozenset) = anyset.downcast_ref::() { + frozenset.__contains__(key, vm) + } else { + Err(vm.new_type_error(format!( + "expected set or frozenset, got '{}'", + anyset.class().name() + ))) + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PySet_Discard(set: *mut PyObject, key: *mut PyObject) -> c_int { + with_vm(|vm| { + let set = unsafe { &*set }.try_downcast_ref::(vm)?; + let key = unsafe { &*key }; + let had_item = set.__contains__(key, vm)?; + if had_item { + set.discard(key.to_owned(), vm)?; + } + Ok(had_item) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PySet_Pop(set: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let set = unsafe { &*set }.try_downcast_ref::(vm)?; + set.pop(vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PySet_Size(anyset: *mut PyObject) -> isize { + with_vm(|vm| { + let anyset = unsafe { &*anyset }; + if let Some(set) = anyset.downcast_ref::() { + set.as_object().length(vm) + } else if let Some(frozenset) = anyset.downcast_ref::() { + frozenset.as_object().length(vm) + } else { + Err(vm.new_type_error(format!( + "expected set or frozenset, got '{}'", + anyset.class().name() + ))) + } + }) +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::{PyFrozenSet, PyInt, PySet}; + + #[test] + fn new_and_size() { + Python::attach(|py| { + let set = PySet::empty(py).unwrap(); + assert!(set.is_instance_of::()); + assert_eq!(set.len(), 0); + + let frozen = PyFrozenSet::empty(py).unwrap(); + assert!(frozen.is_instance_of::()); + assert_eq!(frozen.len(), 0); + }) + } + + #[test] + fn add_contains_discard() { + Python::attach(|py| { + let set = PySet::empty(py).unwrap(); + let item = PyInt::new(py, 42); + + set.add(&item).unwrap(); + assert!(set.contains(&item).unwrap()); + set.discard(&item).unwrap(); + assert!(!set.contains(&item).unwrap()); + }) + } + + #[test] + fn pop_reduces_size() { + Python::attach(|py| { + let set = PySet::empty(py).unwrap(); + set.add(7).unwrap(); + assert_eq!(set.len(), 1); + + let popped = set.pop().unwrap(); + assert_eq!(popped.extract::().unwrap(), 7); + assert_eq!(set.len(), 0); + }) + } +} diff --git a/crates/vm/src/builtins/set.rs b/crates/vm/src/builtins/set.rs index f730049ff01..136470c1813 100644 --- a/crates/vm/src/builtins/set.rs +++ b/crates/vm/src/builtins/set.rs @@ -537,7 +537,7 @@ impl PySet { self.inner.len() } - fn __contains__(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult { + pub fn __contains__(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult { self.inner.contains(needle, vm) } @@ -679,17 +679,17 @@ impl PySet { } #[pymethod] - fn discard(&self, item: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + pub fn discard(&self, item: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { self.inner.discard(&item, vm).map(|_| ()) } #[pymethod] - fn clear(&self) { + pub fn clear(&self) { self.inner.clear() } #[pymethod] - fn pop(&self, vm: &VirtualMachine) -> PyResult { + pub fn pop(&self, vm: &VirtualMachine) -> PyResult { self.inner.pop(vm) } @@ -995,7 +995,7 @@ impl PyFrozenSet { self.inner.len() } - fn __contains__(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult { + pub fn __contains__(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult { self.inner.contains(needle, vm) } From 7f6c466b308d74d358a06e4f9b6a89bd3d54f88e Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 1 Jun 2026 15:48:26 +0200 Subject: [PATCH 134/135] Add mapping c-api functions --- crates/capi/capi_todo.md | 4 -- crates/capi/src/abstract_.rs | 3 ++ crates/capi/src/abstract_/mapping.rs | 66 ++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 crates/capi/src/abstract_/mapping.rs diff --git a/crates/capi/capi_todo.md b/crates/capi/capi_todo.md index 48c86929c76..777d9a585cf 100644 --- a/crates/capi/capi_todo.md +++ b/crates/capi/capi_todo.md @@ -9,10 +9,6 @@ RustPython C API target: `crates/capi/src/abstract_.rs` - `PyIter_Check` - `PyIter_NextItem` - `PyIter_Send` -- `PyMapping_Items` -- `PyMapping_Keys` -- `PyMapping_Size` -- `PyMapping_Values` - `PyNumber_Add` - `PyNumber_Lshift` - `PyNumber_Or` diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index 6cea270ba99..f25f3f16c01 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -5,6 +5,9 @@ use rustpython_vm::builtins::{PyDict, PyStr, PyTuple}; use rustpython_vm::function::{FuncArgs, KwArgs, PosArgs}; use rustpython_vm::{AsObject, Py, PyObjectRef, PyResult, VirtualMachine}; +mod mapping; +pub use mapping::*; + const PY_VECTORCALL_ARGUMENTS_OFFSET: usize = 1usize << (usize::BITS as usize - 1); fn tuple_to_args(tuple: &Py) -> PosArgs { diff --git a/crates/capi/src/abstract_/mapping.rs b/crates/capi/src/abstract_/mapping.rs new file mode 100644 index 00000000000..54cf175ff7b --- /dev/null +++ b/crates/capi/src/abstract_/mapping.rs @@ -0,0 +1,66 @@ +use crate::{PyObject, pystate::with_vm}; +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_Size(obj: *mut PyObject) -> isize { + with_vm(|vm| { + let obj = unsafe { &*obj }; + obj.try_mapping(vm)?.length(vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_Keys(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let keys = obj.try_mapping(vm)?.keys(vm)?; + let iter = keys.get_iter(vm)?; + Ok(vm.ctx.new_list(iter.try_to_value(vm)?)) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_Values(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let values = obj.try_mapping(vm)?.values(vm)?; + let iter = values.get_iter(vm)?; + Ok(vm.ctx.new_list(iter.try_to_value(vm)?)) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_Items(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let items = obj.try_mapping(vm)?.items(vm)?; + let iter = items.get_iter(vm)?; + Ok(vm.ctx.new_list(iter.try_to_value(vm)?)) + }) +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::{PyDict, PyMapping, PyMappingMethods, PyTuple}; + + #[test] + fn size_keys_values_items() { + Python::attach(|py| { + let dict = PyDict::new(py); + dict.set_item("a", 1).unwrap(); + dict.set_item("b", 2).unwrap(); + let mapping = dict.cast_into::().unwrap(); + + assert_eq!(mapping.len().unwrap(), 2); + + let keys = mapping.keys().unwrap(); + assert_eq!(keys.len(), 2); + + let values = mapping.values().unwrap(); + assert_eq!(values.len(), 2); + + let items = mapping.items().unwrap(); + assert_eq!(items.len(), 2); + assert!(items.iter().all(|item| item.cast_into::().is_ok())); + }) + } +} From 4d819d3293a3f8572f6fb691c6f26d462a3aee31 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 1 Jun 2026 17:01:20 +0200 Subject: [PATCH 135/135] Add number protocol functions --- crates/capi/capi_todo.md | 5 -- crates/capi/src/abstract_.rs | 7 +-- crates/capi/src/abstract_/number.rs | 86 +++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 crates/capi/src/abstract_/number.rs diff --git a/crates/capi/capi_todo.md b/crates/capi/capi_todo.md index 777d9a585cf..533839e6c7d 100644 --- a/crates/capi/capi_todo.md +++ b/crates/capi/capi_todo.md @@ -9,11 +9,6 @@ RustPython C API target: `crates/capi/src/abstract_.rs` - `PyIter_Check` - `PyIter_NextItem` - `PyIter_Send` -- `PyNumber_Add` -- `PyNumber_Lshift` -- `PyNumber_Or` -- `PyNumber_Rshift` -- `PyNumber_Subtract` - `PyObject_GetIter` - `PyObject_Size` - `PySequence_Check` diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index f25f3f16c01..bc7ec7f9e9c 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -6,7 +6,9 @@ use rustpython_vm::function::{FuncArgs, KwArgs, PosArgs}; use rustpython_vm::{AsObject, Py, PyObjectRef, PyResult, VirtualMachine}; mod mapping; +mod number; pub use mapping::*; +pub use number::*; const PY_VECTORCALL_ARGUMENTS_OFFSET: usize = 1usize << (usize::BITS as usize - 1); @@ -205,11 +207,6 @@ pub extern "C" fn PySequence_Contains(obj: *mut PyObject, value: *mut PyObject) }) } -#[unsafe(no_mangle)] -pub extern "C" fn PyNumber_Index(obj: *mut PyObject) -> *mut PyObject { - with_vm(|vm| unsafe { &*obj }.try_index(vm)) -} - #[cfg(test)] mod tests { use pyo3::prelude::*; diff --git a/crates/capi/src/abstract_/number.rs b/crates/capi/src/abstract_/number.rs new file mode 100644 index 00000000000..98309779f2b --- /dev/null +++ b/crates/capi/src/abstract_/number.rs @@ -0,0 +1,86 @@ +use crate::{PyObject, pystate::with_vm}; + +#[unsafe(no_mangle)] +pub extern "C" fn PyNumber_Add(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._add(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyNumber_Index(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| unsafe { &*obj }.try_index(vm)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyNumber_Lshift(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._lshift(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyNumber_Or(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._or(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyNumber_Rshift(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._rshift(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyNumber_Subtract(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._sub(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + + #[test] + fn add() { + Python::attach(|py| { + let lhs = 40i64.into_pyobject(py).unwrap(); + let rhs = 2i64.into_pyobject(py).unwrap(); + let out = lhs.call_method1("__add__", (&rhs,)).unwrap(); + assert_eq!(out.extract::().unwrap(), 42); + }) + } + + #[test] + fn lshift() { + Python::attach(|py| { + let lhs = 1i64.into_pyobject(py).unwrap(); + let rhs = 5i64.into_pyobject(py).unwrap(); + let out = lhs.call_method1("__lshift__", (&rhs,)).unwrap(); + assert_eq!(out.extract::().unwrap(), 32); + }) + } + + #[test] + fn bit_or() { + Python::attach(|py| { + let lhs = 0b1010i64.into_pyobject(py).unwrap(); + let rhs = 0b0110i64.into_pyobject(py).unwrap(); + let out = lhs.call_method1("__or__", (&rhs,)).unwrap(); + assert_eq!(out.extract::().unwrap(), 0b1110); + }) + } + + #[test] + fn rshift() { + Python::attach(|py| { + let lhs = 128i64.into_pyobject(py).unwrap(); + let rhs = 3i64.into_pyobject(py).unwrap(); + let out = lhs.call_method1("__rshift__", (&rhs,)).unwrap(); + assert_eq!(out.extract::().unwrap(), 16); + }) + } + + #[test] + fn subtract() { + Python::attach(|py| { + let lhs = 50i64.into_pyobject(py).unwrap(); + let rhs = 8i64.into_pyobject(py).unwrap(); + let out = lhs.call_method1("__sub__", (&rhs,)).unwrap(); + assert_eq!(out.extract::().unwrap(), 42); + }) + } +}