diff --git a/Cargo.lock b/Cargo.lock index 5fd981fd13..5d70087432 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,14 @@ dependencies = [ "winresource", ] +[[package]] +name = "rustpython-capi" +version = "0.5.0" +dependencies = [ + "pyo3", + "rustpython-vm", +] + [[package]] name = "rustpython-codegen" version = "0.5.0" diff --git a/crates/capi/.cargo/config.toml b/crates/capi/.cargo/config.toml new file mode 100644 index 0000000000..49ff5a492e --- /dev/null +++ b/crates/capi/.cargo/config.toml @@ -0,0 +1,2 @@ +[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 new file mode 100644 index 0000000000..b0106ae223 --- /dev/null +++ b/crates/capi/Cargo.toml @@ -0,0 +1,21 @@ +[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, 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 0000000000..e039bd6d6b --- /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 new file mode 100644 index 0000000000..915dc6236a --- /dev/null +++ b/crates/capi/src/abstract_.rs @@ -0,0 +1,91 @@ +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(), + ) + }) +} + +#[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 new file mode 100644 index 0000000000..8a9bb15d7d --- /dev/null +++ b/crates/capi/src/bytesobject.rs @@ -0,0 +1,56 @@ +use crate::PyObject; +use crate::pystate::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() + } 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 { + 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 { + with_vm(|_vm| { + let bytes = unsafe { &*bytes } + .downcast_ref::() + .expect("PyBytes_AsString argument must be a bytes object"); + 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!"); + }) + } +} \ No newline at end of file diff --git a/crates/capi/src/import.rs b/crates/capi/src/import.rs new file mode 100644 index 0000000000..9d546e1de7 --- /dev/null +++ b/crates/capi/src/import.rs @@ -0,0 +1,29 @@ +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 { + 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(), + ) + }) +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + + #[test] + fn test_import() { + Python::attach(|py| { + let _module = py.import("sys").unwrap(); + }) + } +} \ No newline at end of file diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs new file mode 100644 index 0000000000..35b3f276bd --- /dev/null +++ b/crates/capi/src/lib.rs @@ -0,0 +1,21 @@ +pub use rustpython_vm::PyObject; + +extern crate alloc; + +pub mod abstract_; +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; + +#[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 0000000000..b8d0ef740b --- /dev/null +++ b/crates/capi/src/longobject.rs @@ -0,0 +1,75 @@ +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::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()) +} + +#[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()) +} + +#[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()) +} + +#[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()) +} + +#[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()) +} + +#[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()) +} + +#[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"); + + 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 + }) + }) +} + +#[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 new file mode 100644 index 0000000000..4610e4b17a --- /dev/null +++ b/crates/capi/src/object.rs @@ -0,0 +1,172 @@ +use crate::PyObject; +use crate::pystate::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; + +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: MaybeUninit<&'static Py> = MaybeUninit::uninit(); + +#[unsafe(no_mangle)] +pub static mut PyLong_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); + +#[unsafe(no_mangle)] +pub static mut PyTuple_Type: MaybeUninit<&'static Py> = MaybeUninit::uninit(); + +#[unsafe(no_mangle)] +pub static mut PyUnicode_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`. + 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; + + // 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.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 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 { + let ty = unsafe { &*ptr }; + with_vm(move |vm| ty.__qualname__(vm).into_object().into_raw().as_ptr()) +} + +#[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: 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() + }) +} + +#[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(), + ) + }) +} + +#[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 new file mode 100644 index 0000000000..20c27cf6ce --- /dev/null +++ b/crates/capi/src/pyerrors.rs @@ -0,0 +1,151 @@ +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: 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 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(), + ) + }) +} + +#[unsafe(no_mangle)] +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) { + 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)] +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) { + 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) { + 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, + _doc: *const c_char, + base: *mut PyObject, + dict: *mut PyObject, +) -> *mut PyObject { + PyErr_NewException(name, base, dict) +} + +#[unsafe(no_mangle)] +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/pylifecycle.rs b/crates/capi/src/pylifecycle.rs new file mode 100644 index 0000000000..644639814c --- /dev/null +++ b/crates/capi/src/pylifecycle.rs @@ -0,0 +1,104 @@ +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::{AsObject, Context, Interpreter}; +use std::sync::{Once, OnceLock, mpsc}; + +static VM_REQUEST_TX: OnceLock>> = + OnceLock::new(); +pub(crate) static INITIALIZED: Once = Once::new(); + +/// 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") +} + +/// 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); + + 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()); + }; +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_IsInitialized() -> c_int { + INITIALIZED.is_completed() as _ +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_Initialize() { + Py_InitializeEx(0); +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_InitializeEx(_initsigs: c_int) { + if INITIALIZED.is_completed() { + panic!("Initialize called multiple times"); + } + + INITIALIZED.call_once(|| { + init_static_type_pointers(); + + let (tx, rx) = mpsc::channel(); + VM_REQUEST_TX + .set(tx) + .expect("VM request channel was already initialized"); + + 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"); + } + }) + }); + }); + + attach_vm_to_thread(); +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_Finalize() { + let _ = Py_FinalizeEx(); +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_FinalizeEx() -> c_int { + log_stub("Py_FinalizeEx"); + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" 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 0000000000..06aae21798 --- /dev/null +++ b/crates/capi/src/pystate.rs @@ -0,0 +1,75 @@ +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; + +#[repr(C)] +pub struct PyThreadState { + _interp: *mut std::ffi::c_void, +} + +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) {} + +#[unsafe(no_mangle)] +pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState { + ptr::null_mut() +} + +#[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 new file mode 100644 index 0000000000..abdcd5cd54 --- /dev/null +++ b/crates/capi/src/refcount.rs @@ -0,0 +1,56 @@ +use crate::PyObject; +use rustpython_vm::PyObjectRef; +use std::ptr::NonNull; +use crate::pystate::with_vm; + +#[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); +} + +#[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::prelude::*; + use pyo3::PyTypeInfo; + 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/traceback.rs b/crates/capi/src/traceback.rs new file mode 100644 index 0000000000..c73c952d5e --- /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 0000000000..996a835bb7 --- /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 0000000000..42a71ece01 --- /dev/null +++ b/crates/capi/src/unicodeobject.rs @@ -0,0 +1,81 @@ +use crate::PyObject; +use crate::pystate::with_vm; +use core::ffi::c_char; +use core::ptr; +use core::slice; +use core::str; +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 { + 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") + }; + + with_vm(|vm| { + let obj: PyObjectRef = vm.ctx.new_str(text).into(); + obj.into_raw().as_ptr() + }) +} + +#[unsafe(no_mangle)] +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)] +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"); +} + +#[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/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index 64801a1de5..652c4db1d8 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) } diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 231ba5cde3..e8f3b771a4 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)