diff --git a/Lib/test/test_ctypes/test_python_api.py b/Lib/test/test_ctypes/test_python_api.py index 28abf2ac031..e35cd8917ff 100644 --- a/Lib/test/test_ctypes/test_python_api.py +++ b/Lib/test/test_ctypes/test_python_api.py @@ -7,7 +7,6 @@ class PythonAPITestCase(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON; - requires pythonapi (Python C API) def test_PyBytes_FromStringAndSize(self): PyBytes_FromStringAndSize = pythonapi.PyBytes_FromStringAndSize diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index 671c8d1640c..615724e9d91 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -24,5 +24,9 @@ rustpython-pylib = { workspace = true } [dev-dependencies] pyo3 = { workspace = true, features = ["auto-initialize", "abi3t"] } +[features] +# Enable PyObject_CallMethodObjArgs variadic function, which is only available on nightly Rust. +nightly = [] + [lints] workspace = true diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index fd6e966bdae..36fca57110f 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -17,11 +17,11 @@ mod sequence; const PY_VECTORCALL_ARGUMENTS_OFFSET: usize = 1usize << (usize::BITS as usize - 1); -fn tuple_to_args(tuple: &Py) -> PosArgs { +pub(crate) fn tuple_to_args(tuple: &Py) -> PosArgs { tuple.iter().cloned().collect::>().into() } -fn dict_to_kwargs(vm: &VirtualMachine, dict: &Py) -> PyResult { +pub(crate) fn dict_to_kwargs(vm: &VirtualMachine, dict: &Py) -> PyResult { dict.items_vec() .into_iter() .map(|(key, value)| { @@ -75,6 +75,25 @@ pub unsafe extern "C" fn PyObject_CallObject( }) } +#[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 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) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyObject_Vectorcall( callable: *mut PyObject, @@ -272,6 +291,18 @@ mod tests { use pyo3::prelude::*; use pyo3::types::{PyDict, 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 call_method1() { Python::attach(|py| { diff --git a/crates/capi/src/descrobject.rs b/crates/capi/src/descrobject.rs index 863f286e13a..02f95017ef2 100644 --- a/crates/capi/src/descrobject.rs +++ b/crates/capi/src/descrobject.rs @@ -30,6 +30,17 @@ pub struct PyGetSetDef { } impl PyGetSetDef { + pub(crate) fn iter<'a>(mut defs: *const Self) -> impl Iterator { + core::iter::from_fn(move || { + let def = unsafe { &*defs }; + if def.name.is_null() { + None + } else { + defs = unsafe { defs.add(1) }; + Some(def) + } + }) + } pub(crate) fn build( &self, ty: &'static Py, @@ -136,6 +147,18 @@ impl PyMemberDef { const PY_READONLY: c_int = 1; const PY_RELATIVE_OFFSET: c_int = 8; + pub(crate) fn iter<'a>(mut defs: *const Self) -> impl Iterator { + core::iter::from_fn(move || { + let def = unsafe { &*defs }; + if def.name.is_null() { + None + } else { + defs = unsafe { defs.add(1) }; + Some(def) + } + }) + } + pub(crate) fn build( &self, ty: &Py, diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index 2aff75fe15b..1b5c6888cdf 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -1,3 +1,4 @@ +#![cfg_attr(feature = "nightly", feature(c_variadic))] #![allow(clippy::missing_safety_doc)] use crate::pyerrors::init_exception_statics; @@ -40,6 +41,7 @@ pub mod pystrcmp; pub mod refcount; pub mod setobject; pub mod sliceobject; +pub mod slots; pub mod traceback; pub mod tupleobject; pub mod unicodeobject; diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 2be54ca8939..4ba71e7fc25 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 crate::util::CStrExt; use core::ffi::{c_char, c_int}; +use core::fmt::Debug; use core::ptr::NonNull; use rustpython_vm::function::{FuncArgs, HeapMethodDef, PosArgs, PyMethodFlags}; use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; @@ -12,6 +13,7 @@ define_py_check!(fn PyCFunction_Check, types.builtin_function_or_method_type); define_py_check!(exact fn PyCFunction_CheckExact, types.builtin_function_or_method_type); #[repr(C)] +#[derive(Debug)] pub struct PyMethodDef { pub ml_name: *const c_char, pub ml_meth: PyMethodPointer, @@ -19,6 +21,20 @@ pub struct PyMethodDef { pub ml_doc: *const c_char, } +impl PyMethodDef { + pub(crate) fn iter<'a>(mut methods: *const Self) -> impl Iterator { + core::iter::from_fn(move || { + let def = unsafe { &*methods }; + if def.ml_name.is_null() { + None + } else { + methods = unsafe { methods.add(1) }; + Some(def) + } + }) + } +} + #[repr(C)] #[derive(Copy, Clone)] #[allow(non_snake_case)] @@ -42,6 +58,12 @@ pub union PyMethodPointer { ) -> *mut PyObject, } +impl Debug for PyMethodPointer { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + unsafe { self.PyCFunction.fmt(f) } + } +} + pub(crate) fn build_method_def( vm: &VirtualMachine, ml: &PyMethodDef, diff --git a/crates/capi/src/moduleobject.rs b/crates/capi/src/moduleobject.rs index dd753455406..c25458973ff 100644 --- a/crates/capi/src/moduleobject.rs +++ b/crates/capi/src/moduleobject.rs @@ -1,11 +1,71 @@ use crate::PyObject; use crate::object::define_py_check; use crate::pystate::with_vm; -use rustpython_vm::builtins::{PyModule, PyStr}; +use crate::slots::{PySlot, PySlotKind, PySlotModule}; +use core::ffi::c_int; +use rustpython_vm::builtins::{PyModule, PyModuleDef, PyStr}; define_py_check!(fn PyModule_Check, types.module_type); define_py_check!(exact fn PyModule_CheckExact, types.module_type); +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyModule_FromSlotsAndSpec( + slots: *const PySlot, + spec: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let name = unsafe { &*spec } + .get_attr("name", vm)? + .downcast_exact::(vm) + .unwrap(); + + let mut exec = None; + let mut create = None; + + for slot in PySlot::iter(slots) { + match slot.as_kind(vm)? { + PySlotKind::Module(module) => match module { + PySlotModule::Create(mod_create) => create = Some(mod_create), + PySlotModule::Exec(mod_exec) => { + if exec.replace(mod_exec).is_some() { + return Err(vm.new_system_error("Multiple module exec slots found")); + } + } + PySlotModule::Name { .. } + | PySlotModule::Doc { .. } + | PySlotModule::Methods(_) + | PySlotModule::Abi { .. } + | PySlotModule::MultipleInterpreters { .. } + | PySlotModule::Gil { .. } => {} + }, + kind @ PySlotKind::Type(_) => { + return Err(vm.new_system_error(format!( + "Got type slot while module slots are expected: {kind:?}" + ))); + } + PySlotKind::Unknown { .. } => {} + } + } + + let def = PyModuleDef::from_slots(vm.ctx.intern_str(name), None, create, exec); + + def.create_module_owned(vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyModule_Exec(module: *mut PyObject) -> c_int { + with_vm(|vm| { + let module = unsafe { &*module }.try_downcast_ref::(vm)?; + let def = module + .def + .as_deref() + .ok_or_else(|| vm.new_system_error("Empty module"))?; + def.exec_module(vm, module)?; + Ok(()) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyModule_GetNameObject(module: *mut PyObject) -> *mut PyObject { with_vm(|vm| { @@ -40,3 +100,58 @@ pub unsafe extern "C" fn PyModule_NewObject(name: *mut PyObject) -> *mut PyObjec Ok(vm.new_module(name, vm.ctx.new_dict(), None)) }) } + +#[cfg(test)] +mod tests { + use pyo3::ffi; + use pyo3::prelude::*; + + #[test] + fn create_module() { + #[pymodule] + mod my_extension { + use pyo3::prelude::*; + + #[pymodule_export] + const PI: f64 = core::f64::consts::PI; + + #[pyfunction] // Inline definition of a pyfunction, also made available to Python + fn triple(x: usize) -> usize { + x * 3 + } + } + + fn create_module(py: Python<'_>) -> PyResult> { + let spec = py.import("types")?.getattr("SimpleNamespace")?.call0()?; + spec.setattr("name", "my_extension")?; + let slots = unsafe { my_extension::__pyo3_export() }; + let module = unsafe { + Bound::from_owned_ptr_or_err( + py, + ffi::PyModule_FromSlotsAndSpec(slots, spec.as_ptr()), + )? + .cast_into_unchecked::() + }; + unsafe { ffi::PyModule_Exec(module.as_ptr()) }; + Ok(module) + } + + Python::attach(|py| { + let module = create_module(py).unwrap(); + 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 + ); + }) + } +} diff --git a/crates/capi/src/object/pytype.rs b/crates/capi/src/object/pytype.rs index daad7b3133b..8be74e10031 100644 --- a/crates/capi/src/object/pytype.rs +++ b/crates/capi/src/object/pytype.rs @@ -1,7 +1,14 @@ +use crate::abstract_::{dict_to_kwargs, tuple_to_args}; +use crate::descrobject::{PyGetSetDef, PyMemberDef}; +use crate::methodobject::{PyMethodDef, build_method_def}; use crate::object::define_py_check; use crate::pystate::with_vm; -use core::ffi::{c_int, c_ulong}; -use rustpython_vm::builtins::{PyStr, PyType}; +use crate::slots::{PySlot, PySlotKind, PySlotType}; +use crate::util::CStrExt; +use core::ffi::{c_char, c_int, c_ulong, c_void}; +use rustpython_vm::builtins::{PyDict, PyStr, PyTuple, PyType}; +use rustpython_vm::function::{FuncArgs, PyMethodFlags}; +use rustpython_vm::types::{PyTypeFlags, PyTypeSlots, SlotAccessor}; use rustpython_vm::{AsObject, Py, PyObject}; pub type PyTypeObject = Py; @@ -9,6 +16,26 @@ pub type PyTypeObject = Py; define_py_check!(fn PyType_Check, types.type_type); define_py_check!(exact fn PyType_CheckExact, types.type_type); +#[repr(C)] +pub struct PyType_Slot { + pub slot: c_int, + pub pfunc: *mut c_void, +} + +impl PyType_Slot { + pub(crate) fn iter<'a>(mut slots: *const Self) -> impl Iterator { + core::iter::from_fn(move || { + let slot = unsafe { &*slots }; + if slot.slot == 0 { + None + } else { + slots = unsafe { slots.add(1) }; + Some(slot) + } + }) + } +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn Py_TYPE(op: *mut PyObject) -> *const PyTypeObject { unsafe { (*op).class() } @@ -70,10 +97,216 @@ pub unsafe extern "C" fn PyType_GetFullyQualifiedName(ptr: *const PyTypeObject) }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyType_GetSlot(ty: *const PyTypeObject, slot: c_int) -> *mut c_void { + with_vm(|_vm| { + 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 args = if let Some(args_obj) = unsafe { args.as_ref() } { + tuple_to_args(args_obj.try_downcast_ref::(vm)?) + } else { + ().into() + }; + + let kwargs = unsafe { kwargs.as_ref() } + .map(|obj| dict_to_kwargs(vm, obj.try_downcast_ref::(vm)?)) + .transpose()? + .unwrap_or_default(); + + subtype + .slots + .new + .load() + .expect("tp_new slot function pointer is null")( + subtype.to_owned(), + FuncArgs::new(args, kwargs), + vm, + ) + }) + } + + ty.slots.new.load().map(|_| newfunc_wrapper as *mut c_void) + } + _ => { + todo!("Slot {slot_accessor:?} for {ty:?} is not yet implemented in PyType_GetSlot") + } + } + .unwrap_or_default() + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyType_FromSlots(slots: *const PySlot) -> *mut PyObject { + with_vm(|vm| { + let mut name = None; + let mut base = None; + let mut methods = Vec::new(); + let mut type_slots: PyTypeSlots = Default::default(); + let attrs = Default::default(); + let mut getsets = Vec::new(); + let mut members = Vec::new(); + + for slot in PySlot::iter(slots) { + match slot.as_kind(vm)? { + kind @ PySlotKind::Type(type_slot) => { + match type_slot { + PySlotType::Name(value) => name = Some(value), + PySlotType::Flags(value) => { + type_slots.flags = PyTypeFlags::from_bits(value).ok_or_else(|| { + vm.new_value_error(format!( + "Invalid type flags: {value:#x} for PyType_FromSlots" + )) + })?; + } + PySlotType::BasicSize(size) | PySlotType::ExtraBasicSize(size) => { + if size != 0 { + return Err(vm.new_not_implemented_error( + "PyType_FromSlots with non-zero size is not yet supported", + )); + } + } + PySlotType::Slots { value, .. } => { + for slot in PyType_Slot::iter(value) { + let slot_id: u8 = slot.slot.try_into().unwrap(); + match slot_id.try_into().unwrap() { + SlotAccessor::TpDoc => { + let doc = unsafe { + slot.pfunc.cast::().try_as_str_opt(vm)? + }; + type_slots.doc = doc; + } + SlotAccessor::TpNew => { + type_slots.new.store(Some(|ty, _args, vm| { + Err(vm.new_not_implemented_error(format!("tp_new is not yet implemented in PyType_FromSlots for {ty:?}"))) + })); + } + SlotAccessor::TpBase => { + base = unsafe { Some(&*slot.pfunc.cast::()) } + } + SlotAccessor::TpDealloc => { + type_slots.del.store(Some(|_ty, _vm| { + // TODO + Ok(()) + })); + } + SlotAccessor::TpMethods => { + for def in PyMethodDef::iter(slot.pfunc.cast()) { + let name = unsafe { def.ml_name.try_as_str(vm)? }; + let is_static = + PyMethodFlags::from_bits_retain(def.ml_flags as _) + .contains(PyMethodFlags::STATIC); + let method = build_method_def(vm, def, !is_static)?; + methods.push((name, method)); + } + } + SlotAccessor::TpGetset => { + getsets.extend(PyGetSetDef::iter(slot.pfunc.cast())); + } + SlotAccessor::TpMembers => { + members.extend(PyMemberDef::iter(slot.pfunc.cast())); + } + slot => { + return Err(vm.new_not_implemented_error(format!( + "PyType_FromSlots with PyType_Slot {slot:?} not implemented yet" + ))); + } + } + } + } + _ => { + return Err(vm.new_not_implemented_error(format!( + "PyType_FromSlots with slot {kind:?} not implemented yet" + ))); + } + } + } + PySlotKind::Module(_) => { + return Err( + vm.new_system_error("Got module slot while type slots are expected") + ); + } + PySlotKind::Unknown { .. } => {} + } + } + + let bases = if let Some(base) = base { + vec![base.to_owned()] + } else { + vec![vm.ctx.types.object_type.to_owned()] + }; + + let metaclass = vm.ctx.types.type_type.to_owned(); + let class = PyType::new_heap(name.unwrap(), bases, attrs, type_slots, metaclass, &vm.ctx) + .map_err(|msg| { + vm.new_system_error(format!("Failed to create type from slots: {msg}")) + })?; + + let mut attrs = class.attributes.write(); + let class_static = unsafe { &*((&*class) as *const _) }; + for (name, method) in methods { + attrs.insert( + vm.ctx.intern_str(name), + method.build_method(class_static, vm).into(), + ); + } + for getset in getsets { + let name = unsafe { getset.name.try_as_str(vm)? }; + attrs.insert( + vm.ctx.intern_str(name), + getset.build(class_static, vm)?.into(), + ); + } + for member in members { + let name = unsafe { member.name.try_as_str(vm)? }; + attrs.insert( + vm.ctx.intern_str(name), + member.build(class_static, vm)?.into(), + ); + } + drop(attrs); + + Ok(class) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_GetTypeData( + obj: *mut PyObject, + cls: *mut PyTypeObject, +) -> *mut c_void { + if unsafe { &*cls }.slots.basicsize == 0 { + obj.cast() + } else { + todo!("PyObject_GetTypeData for non-zero sized types is not yet implemented") + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyType_Freeze(_ty: *mut PyTypeObject) -> c_int { + 0 +} + #[cfg(test)] mod tests { + use pyo3::IntoPyObjectExt; use pyo3::prelude::*; - use pyo3::types::{PyInt, PyString, PyTypeMethods}; + use pyo3::types::{PyDict, PyInt, PyString, PyType, PyTypeMethods}; #[test] fn type_name() { @@ -92,4 +325,138 @@ mod tests { ); }) } + + #[test] + #[ignore] + fn rust_class() { + #[pyclass] + struct MyClass { + #[pyo3(get)] + num: i32, + } + + #[pymethods] + impl MyClass { + #[new] + fn new(value: i32) -> Self { + Self { num: value } + } + + fn method1(&self) -> i32 { + self.num + 10 + } + + fn method2(&self, a: i32) -> i32 { + self.num + a + } + } + + 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(); + + assert_eq!( + obj.call_method1("method1", ()) + .unwrap() + .extract::() + .unwrap(), + 13 + ); + + assert_eq!( + obj.call_method1("method2", (5,)) + .unwrap() + .extract::() + .unwrap(), + 8 + ); + }); + } + + #[test] + #[ignore] + fn rust_class_with_member() { + #[pyclass(frozen)] + struct MyClass { + #[pyo3(get)] + value: Py, + } + + Python::attach(|py| { + let obj = Bound::new( + py, + MyClass { + value: 1.into_bound_py_any(py).unwrap().unbind(), + }, + ) + .unwrap(); + + let globals = PyDict::new(py); + globals.set_item("instance", &obj).unwrap(); + py.run(c"assert instance.value is None", Some(&globals), None) + .unwrap(); + }); + } + + #[test] + fn zero_sized_class() { + #[pyclass(frozen)] + struct MyEmptyClass {} + + #[pymethods] + impl MyEmptyClass { + #[new] + fn new() -> Self { + Self {} + } + + #[staticmethod] + fn static_method1(a: i32, b: i32) -> i32 { + a + b + } + + #[staticmethod] + fn static_method2() -> i32 { + 0 + } + + #[classmethod] + fn cls_method(cls: &Bound<'_, PyType>) -> PyResult { + assert!(cls.is_subclass_of::()?); + Ok(10) + } + } + + Python::attach(|py| { + let obj = Bound::new(py, MyEmptyClass {}).unwrap(); + + 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() + .extract::() + .unwrap(), + 10 + ); + }); + } } diff --git a/crates/capi/src/slots.rs b/crates/capi/src/slots.rs new file mode 100644 index 00000000000..73161c16869 --- /dev/null +++ b/crates/capi/src/slots.rs @@ -0,0 +1,202 @@ +use crate::PyObject; +use crate::methodobject::PyMethodDef; +use crate::object::PyType_Slot; +use crate::util::CStrExt; +use core::ffi::{c_char, c_int, c_void}; +use core::slice::from_ref; +use rustpython_vm::builtins::{PyModule, PyType}; +use rustpython_vm::{Py, PyResult, VirtualMachine}; + +#[repr(C)] +pub struct PySlot { + pub sl_id: u16, + pub sl_flags: u16, + _reserved: u32, + pub value: PySlotValue, +} + +impl PySlot { + const SLOT_OPTIONAL: u16 = 0x0001; + const SLOT_STATIC: u16 = 0x0002; + const SLOT_INTPTR: u16 = 0x0004; + + pub(crate) fn iter<'a>(mut slots: *const Self) -> impl Iterator { + core::iter::from_fn(move || { + let slot = unsafe { &*slots }; + if slot.sl_id == 0 { + None + } else { + slots = unsafe { slots.add(1) }; + Some(slot) + } + }) + } + + #[must_use] + pub fn is_optional(&self) -> bool { + self.sl_flags & Self::SLOT_OPTIONAL != 0 + } + + #[must_use] + pub fn is_static(&self) -> bool { + self.sl_flags & Self::SLOT_STATIC != 0 + } + + #[must_use] + pub fn is_intptr(&self) -> bool { + self.sl_flags & Self::SLOT_INTPTR != 0 + } + + pub(crate) fn as_kind(&self, vm: &VirtualMachine) -> PyResult> { + let value_ptr = unsafe { self.value.sl_ptr }; + let is_static = self.is_static(); + let kind = match self.sl_id { + 48 => PySlotKind::Type(PySlotType::Base(unsafe { &*value_ptr.cast() })), + 49 => PySlotKind::Type(PySlotType::Bases(unsafe { from_ref(&*value_ptr.cast()) })), + 84 => { + let create = unsafe { + core::mem::transmute::< + unsafe extern "C" fn(), + unsafe extern "C" fn( + spec: *mut PyObject, + def: *mut c_void, + ) -> *mut PyObject, + >(self.value.sl_func) + }; + PySlotKind::Module(PySlotModule::Create(create)) + } + 85 => { + let exec = unsafe { + core::mem::transmute::< + unsafe extern "C" fn(), + unsafe extern "C" fn(*mut PyObject) -> i32, + >(self.value.sl_func) + }; + PySlotKind::Module(PySlotModule::Exec(exec)) + } + 86 => PySlotKind::Module(PySlotModule::MultipleInterpreters(value_ptr)), + 87 => PySlotKind::Module(PySlotModule::Gil { + gil_used: !value_ptr.is_null(), + }), + // 92 => Py_slot_subslots + 93 => PySlotKind::Type(PySlotType::Slots { + value: value_ptr.cast(), + is_static, + }), + 95 => PySlotKind::Type(PySlotType::Name(unsafe { + value_ptr.cast::().try_as_str(vm)? + })), + 96 => PySlotKind::Type(PySlotType::BasicSize(unsafe { self.value.sl_size })), + 97 => PySlotKind::Type(PySlotType::ExtraBasicSize(unsafe { self.value.sl_size })), + 99 => PySlotKind::Type(PySlotType::Flags(unsafe { self.value.sl_uint64 })), + 100 => PySlotKind::Module(PySlotModule::Name(unsafe { + value_ptr.cast::().try_as_str(vm)? + })), + 101 => PySlotKind::Module(PySlotModule::Doc(unsafe { + value_ptr.cast::().try_as_str(vm)? + })), + // 102 => Py_mod_state_size + 103 => PySlotKind::Module(PySlotModule::Methods(unsafe { &*value_ptr.cast() })), + // 104 => Py_mod_state_traverse + // 105 => Py_mod_state_clear + // 106 => Py_mod_state_free + 107 => PySlotKind::Type(PySlotType::Metaclass(unsafe { &*value_ptr.cast() })), + 108 => PySlotKind::Type(PySlotType::Module(unsafe { &*value_ptr.cast() })), + 109 => PySlotKind::Module(PySlotModule::Abi(unsafe { *value_ptr.cast() })), + // 110 => Py_mod_token + id => { + if self.is_optional() { + PySlotKind::Unknown { + id, + value: value_ptr, + is_static, + } + } else { + return Err(vm.new_system_error(format!("unsupported required slot: {id}"))); + } + } + }; + Ok(kind) + } +} + +#[repr(C)] +pub union PySlotValue { + pub sl_ptr: *mut c_void, + pub sl_func: unsafe extern "C" fn(), + pub sl_size: isize, + pub sl_int64: i64, + pub sl_uint64: u64, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct PyABIInfo { + pub abiinfo_major_version: u8, + pub abiinfo_minor_version: u8, + pub flags: u16, + pub build_version: u32, + pub abi_version: u32, +} + +impl PyABIInfo { + #[allow(dead_code)] + #[must_use] + pub(crate) fn is_supported(&self) -> bool { + const PY_ABIINFO_STABLE: u16 = 0x0001; + const PY_ABIINFO_FREETHREADED: u16 = 0x0004; + + if self.abiinfo_major_version != 1 || self.abiinfo_minor_version != 0 { + return false; + } + + // Only accept abi3t + if self.flags & PY_ABIINFO_STABLE == 0 || self.flags & PY_ABIINFO_FREETHREADED == 0 { + return false; + } + + true + } +} + +#[allow(dead_code)] +#[derive(Debug, Copy, Clone)] +pub(crate) enum PySlotModule<'a> { + Create(unsafe extern "C" fn(spec: *mut PyObject, def: *mut c_void) -> *mut PyObject), + Exec(unsafe extern "C" fn(*mut PyObject) -> c_int), + Name(&'a str), + Doc(&'a str), + Methods(&'a PyMethodDef), + Abi(PyABIInfo), + MultipleInterpreters(*mut c_void), + Gil { gil_used: bool }, +} + +#[allow(dead_code)] +#[derive(Debug, Copy, Clone)] +pub(crate) enum PySlotType<'a> { + Base(&'a Py), + Bases(&'a [Py]), + Slots { + value: *mut PyType_Slot, + is_static: bool, + }, + Name(&'a str), + Flags(u64), + BasicSize(isize), + ExtraBasicSize(isize), + Metaclass(&'a Py), + Module(&'a Py), +} + +#[allow(dead_code)] +#[derive(Debug, Copy, Clone)] +pub(crate) enum PySlotKind<'a> { + Module(PySlotModule<'a>), + Type(PySlotType<'a>), + Unknown { + id: u16, + value: *mut c_void, + is_static: bool, + }, +} diff --git a/crates/capi/src/tupleobject.rs b/crates/capi/src/tupleobject.rs index 60c4b81b370..06386c02fb1 100644 --- a/crates/capi/src/tupleobject.rs +++ b/crates/capi/src/tupleobject.rs @@ -41,6 +41,19 @@ pub unsafe extern "C" fn PyTuple_FromArray( }) } +#[unsafe(no_mangle)] +#[cfg(feature = "nightly")] +pub unsafe extern "C" fn PyTuple_Pack(len: isize, mut args: ...) -> *mut PyObject { + with_vm(|vm| { + let items = + core::iter::repeat_with(|| unsafe { (&*args.next_arg::<*mut PyObject>()).to_owned() }) + .take(len as usize) + .collect::>(); + + vm.new_tuple(items) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn PyTuple_SetItem( _tuple: *mut PyObject, diff --git a/crates/derive-impl/src/pymodule.rs b/crates/derive-impl/src/pymodule.rs index 32d7a0fa6bf..03d2b6c09c0 100644 --- a/crates/derive-impl/src/pymodule.rs +++ b/crates/derive-impl/src/pymodule.rs @@ -277,7 +277,7 @@ pub(crate) fn impl_pymodule(args: PyModuleArgs, module_item: Item) -> Result PyResult>; -pub(crate) type ModuleExec = fn(&VirtualMachine, &Py) -> PyResult<()>; +#[derive(Clone)] +pub enum ModuleCreate { + Rust(fn(&VirtualMachine, &PyObject, &PyModuleDef) -> PyResult>), + C(unsafe extern "C" fn(spec: *mut PyObject, def: *mut c_void) -> *mut PyObject), +} + +impl From *mut PyObject> for ModuleCreate { + fn from(func: unsafe extern "C" fn(*mut PyObject, *mut c_void) -> *mut PyObject) -> Self { + Self::C(func) + } +} + +#[derive(Clone)] +pub enum ModuleExec { + Rust(fn(&VirtualMachine, &Py) -> PyResult<()>), + C(unsafe extern "C" fn(*mut PyObject) -> c_int), +} + +impl From c_int> for ModuleExec { + fn from(func: unsafe extern "C" fn(*mut PyObject) -> c_int) -> Self { + Self::C(func) + } +} -#[derive(Default)] +#[derive(Default, Clone)] pub struct PyModuleSlots { pub create: Option, pub exec: Option, @@ -55,13 +78,51 @@ impl PyModuleDef { use crate::PyPayload; // Create module (use create slot if provided, else default creation) - let module = if let Some(create) = self.slots.create { - // Custom module creation - let spec = vm.ctx.new_str(self.name.as_str()); - create(vm, spec.as_object(), self)? - } else { - // Default module creation - PyModule::from_def(self).into_ref(&vm.ctx) + let module = match self.slots.create { + Some(ModuleCreate::Rust(create)) => { + let spec = vm.ctx.new_str(self.name.as_str()); + create(vm, spec.as_object(), self)? + } + Some(ModuleCreate::C(_)) => { + return Err(vm.new_system_error("C module create slot is not supported here")); + } + None => PyModule::from_def(self).into_ref(&vm.ctx), + }; + + // Initialize module dict and methods + PyModule::__init_dict_from_def(vm, &module); + module.__init_methods(vm)?; + + Ok(module) + } + + pub fn create_module_owned(self, vm: &VirtualMachine) -> PyResult> { + use crate::PyPayload; + + // Create module (use create slot if provided, else default creation) + let module = match self.slots.create { + Some(ModuleCreate::Rust(create)) => { + let spec = vm.ctx.new_str(self.name.as_str()); + create(vm, spec.as_object(), &self)? + } + Some(ModuleCreate::C(create)) => { + let def = Box::leak(Box::new(self)); + let spec = PyNamespace::default().into_ref(&vm.ctx); + spec.as_object() + .set_attr("name", vm.ctx.new_str(def.name.as_str()), vm)?; + let module_ptr = + unsafe { create(spec.as_object().as_raw().cast_mut(), core::ptr::null_mut()) }; + let module_ptr = NonNull::new(module_ptr).ok_or_else(|| { + vm.take_raised_exception().unwrap_or_else(|| { + vm.new_system_error( + "module create slot failed without setting an exception", + ) + }) + })?; + let module_obj = unsafe { PyObjectRef::from_raw(module_ptr) }; + module_obj.try_downcast::(vm)? + } + None => PyModule::from_def_owned(self).into_ref(&vm.ctx), }; // Initialize module dict and methods @@ -74,19 +135,45 @@ impl PyModuleDef { /// Execute the module's exec slot (Phase 2 of multi-phase init). /// /// Calls the exec slot if present. Returns Ok(()) if no exec slot. - pub fn exec_module(&'static self, vm: &VirtualMachine, module: &Py) -> PyResult<()> { - if let Some(exec) = self.slots.exec { - exec(vm, module)?; + pub fn exec_module(&self, vm: &VirtualMachine, module: &Py) -> PyResult<()> { + if let Some(exec) = self.slots.exec.as_ref() { + match exec { + ModuleExec::Rust(exec) => exec(vm, module)?, + ModuleExec::C(exec) => unsafe { + if exec(module.as_object().as_raw().cast_mut()) != 0 { + return Err(vm.take_raised_exception().unwrap_or_else(|| { + vm.new_system_error("Unknown error in module exec slot") + })); + } + }, + }; } Ok(()) } + + pub fn from_slots, E: Into>( + name: &'static PyStrInterned, + doc: Option<&'static PyStrInterned>, + create: Option, + exec: Option, + ) -> Self { + Self { + name, + doc, + methods: &[], + slots: PyModuleSlots { + create: create.map(Into::into), + exec: exec.map(Into::into), + }, + } + } } #[pyclass(module = false, name = "module")] #[derive(Debug)] pub struct PyModule { // PyObject *md_dict; - pub def: Option<&'static PyModuleDef>, + pub def: Option>, // state: Any // weaklist // for logging purposes after md_dict is cleared @@ -123,13 +210,22 @@ impl PyModule { #[must_use] pub const fn from_def(def: &'static PyModuleDef) -> Self { Self { - def: Some(def), + def: Some(Cow::Borrowed(def)), name: Some(def.name), } } + #[must_use] + pub const fn from_def_owned(def: PyModuleDef) -> Self { + let name = def.name; + Self { + def: Some(Cow::Owned(def)), + name: Some(name), + } + } + pub fn __init_dict_from_def(vm: &VirtualMachine, module: &Py) { - let doc = module.def.unwrap().doc.map(|doc| doc.to_owned()); + let doc = module.def.as_ref().unwrap().doc.map(|doc| doc.to_owned()); module.init_dict(module.name.unwrap(), doc, vm); } } @@ -137,7 +233,7 @@ impl PyModule { impl Py { pub fn __init_methods(&self, vm: &VirtualMachine) -> PyResult<()> { debug_assert!(self.def.is_some()); - for method in self.def.unwrap().methods { + for method in self.def.as_ref().unwrap().methods { let func = method .to_function() .with_module(self.name.unwrap()) diff --git a/crates/vm/src/stdlib/_imp.rs b/crates/vm/src/stdlib/_imp.rs index 838012a1d0a..5086b0bc01b 100644 --- a/crates/vm/src/stdlib/_imp.rs +++ b/crates/vm/src/stdlib/_imp.rs @@ -221,12 +221,12 @@ mod _imp { let name_str = name.as_str(); if let Some(&def) = vm.state.module_defs.get(name_str) { // Phase 1: Create module (use create slot if provided, else default creation) - let module = if let Some(create) = def.slots.create { - // Custom module creation - create(vm, &spec, def)? - } else { - // Default module creation - PyModule::from_def(def).into_ref(&vm.ctx) + let module = match def.slots.create { + Some(ModuleCreate::Rust(create)) => create(vm, &spec, def)?, + Some(ModuleCreate::C(_)) => { + return Err(vm.new_system_error("C module create slot is not supported here")); + } + None => PyModule::from_def(def).into_ref(&vm.ctx), }; // Initialize module dict and methods @@ -238,9 +238,7 @@ mod _imp { sys_modules.set_item(name.as_pystr(), module.clone().into(), vm)?; // Phase 2: Call exec slot (can safely import other modules now) - if let Some(exec) = def.slots.exec { - exec(vm, &module)?; - } + def.exec_module(vm, &module)?; return Ok(module.into()); } diff --git a/crates/vm/src/types/slot_defs.rs b/crates/vm/src/types/slot_defs.rs index 300ee319907..82ba91ccc54 100644 --- a/crates/vm/src/types/slot_defs.rs +++ b/crates/vm/src/types/slot_defs.rs @@ -4,6 +4,7 @@ use super::{PyComparisonOp, PyTypeSlots, fn_addr}; use crate::builtins::descriptor::SlotFunc; +use num_enum::TryFromPrimitive; /// Slot operation type /// @@ -68,7 +69,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)