diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 1e764176550..76b04426657 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -339,8 +339,7 @@ pub fn pymodule_module_impl( unsafe { impl_::ModuleDef::new( __PYO3_NAME, - #doc, - INITIALIZER + #doc ) } }}; @@ -385,6 +384,9 @@ pub fn pymodule_function_impl( let initialization = module_initialization(&name, ctx, quote! { MakeDef::make_def() }, false); + // Each generated `module_objects_init` function is exported as a separate symbol. + let module_objects_init_symbol = format!("__module_objects_init__{}", ident.unraw()); + // Module function called with optional Python<'_> marker as first arg, followed by the module. let mut module_args = Vec::new(); if function.sig.inputs.len() == 2 { @@ -396,6 +398,32 @@ pub fn pymodule_function_impl( Ok(quote! { #[doc(hidden)] #vis mod #ident { + /// Function used to add classes, functions, etc. to the module during + /// multi-phase initialization. + #[doc(hidden)] + #[export_name = #module_objects_init_symbol] + pub unsafe extern "C" fn __module_objects_init(module: *mut #pyo3_path::ffi::PyObject) -> ::std::ffi::c_int { + let module = unsafe { + let nonnull = ::std::ptr::NonNull::new(module).expect("'module' shouldn't be NULL"); + #pyo3_path::Py::<#pyo3_path::types::PyModule>::from_non_null(nonnull) + }; + + let res = unsafe { + #pyo3_path::Python::with_gil_unchecked(|py| { + let bound = module.bind(py); + MakeDef::do_init_multiphase(bound) + }) + }; + + // FIXME: Better error handling + let _ = res.unwrap(); + + 0 + } + + #[doc(hidden)] + pub const __PYO3_INIT: *mut ::std::ffi::c_void = __module_objects_init as *mut ::std::ffi::c_void; + #initialization } @@ -405,17 +433,22 @@ pub fn pymodule_function_impl( // inside a function body) #[allow(unknown_lints, non_local_definitions)] impl #ident::MakeDef { + /// Helper function for `__module_objects_init`. Should probably be put + /// somewhere else. + #[doc(hidden)] + pub fn do_init_multiphase(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { + #ident(#(#module_args),*) + } + const fn make_def() -> #pyo3_path::impl_::pymodule::ModuleDef { fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { #ident(#(#module_args),*) } - const INITIALIZER: #pyo3_path::impl_::pymodule::ModuleInitializer = #pyo3_path::impl_::pymodule::ModuleInitializer(__pyo3_pymodule); unsafe { #pyo3_path::impl_::pymodule::ModuleDef::new( #ident::__PYO3_NAME, #doc, - INITIALIZER ) } } @@ -442,14 +475,31 @@ fn module_initialization( #[doc(hidden)] pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = #module_def; }; + if !is_submodule { result.extend(quote! { + #[doc(hidden)] + pub static _PYO3_SLOTS: &[#pyo3_path::impl_::pymodule_state::ModuleDefSlot] = &[ + #pyo3_path::impl_::pymodule_state::ModuleDefSlot::start(), + #pyo3_path::impl_::pymodule_state::ModuleDefSlot::new( + #pyo3_path::ffi::Py_mod_exec, + __PYO3_INIT, + ), + #[cfg(Py_3_12)] + #pyo3_path::impl_::pymodule_state::ModuleDefSlot::per_interpreter_gil(), + #pyo3_path::impl_::pymodule_state::ModuleDefSlot::end(), + ]; + /// This autogenerated function is called by the python interpreter when importing /// the module. #[doc(hidden)] #[export_name = #pyinit_symbol] pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject { - #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py)) + #pyo3_path::impl_::trampoline::module_init(|py| { + let slots = #pyo3_path::impl_::pymodule_state::ModuleDefSlots::new_from_static(_PYO3_SLOTS); + _PYO3_DEF.set_multiphase_items(slots); + _PYO3_DEF.make_module(py); + }) } }); } diff --git a/src/impl_.rs b/src/impl_.rs index 5bfeda39f65..a183f6a8301 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -19,6 +19,7 @@ pub mod pyclass; pub mod pyfunction; pub mod pymethods; pub mod pymodule; +pub mod pymodule_state; #[doc(hidden)] pub mod trampoline; pub mod wrap; diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index b05652bced8..f8548555140 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -26,11 +26,15 @@ use crate::{ Bound, Py, PyClass, PyMethodDef, PyResult, PyTypeInfo, Python, }; +use crate::impl_::pymodule_state as state; + +// TODO: replace other usages (if this passes review :^) ) +pub use state::ModuleDefSlot; + /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { // wrapped in UnsafeCell so that Rust compiler treats this as interior mutability ffi_def: UnsafeCell, - initializer: ModuleInitializer, /// Interpreter ID where module was initialized (not applicable on PyPy). #[cfg(all( not(any(PyPy, GraalPy)), @@ -38,22 +42,17 @@ pub struct ModuleDef { not(all(windows, Py_LIMITED_API, not(Py_3_10))) ))] interpreter: AtomicI64, + // TODO: `module` could probably go..? /// Initialized module object, cached to avoid reinitialization. + #[allow(unused)] module: GILOnceCell>, } -/// Wrapper to enable initializer to be used in const fns. -pub struct ModuleInitializer(pub for<'py> fn(&Bound<'py, PyModule>) -> PyResult<()>); - unsafe impl Sync for ModuleDef {} impl ModuleDef { /// Make new module definition with given module name. - pub const unsafe fn new( - name: &'static CStr, - doc: &'static CStr, - initializer: ModuleInitializer, - ) -> Self { + pub const unsafe fn new(name: &'static CStr, doc: &'static CStr) -> Self { const INIT: ffi::PyModuleDef = ffi::PyModuleDef { m_base: ffi::PyModuleDef_HEAD_INIT, m_name: std::ptr::null(), @@ -74,7 +73,6 @@ impl ModuleDef { ModuleDef { ffi_def, - initializer, // -1 is never expected to be a valid interpreter ID #[cfg(all( not(any(PyPy, GraalPy)), @@ -85,8 +83,9 @@ impl ModuleDef { module: GILOnceCell::new(), } } + /// Builds a module using user given initializer. Used for [`#[pymodule]`][crate::pymodule]. - pub fn make_module(&'static self, py: Python<'_>) -> PyResult> { + pub fn make_module(&'static self, py: Python<'_>) -> PyResult<*mut ffi::PyModuleDef> { #[cfg(all(PyPy, not(Py_3_8)))] { use crate::types::any::PyAnyMethods; @@ -140,18 +139,31 @@ impl ModuleDef { } } } - self.module - .get_or_try_init(py, || { - let module = unsafe { - Py::::from_owned_ptr_or_err( - py, - ffi::PyModule_Create(self.ffi_def.get()), - )? - }; - self.initializer.0(module.bind(py))?; - Ok(module) - }) - .map(|py_module| py_module.clone_ref(py)) + + if (unsafe { *self.ffi_def.get() }).m_slots.is_null() { + return Err(PyImportError::new_err( + "'m_slots' of module definition is NULL", + )); + } + + let module_def_ptr = unsafe { ffi::PyModuleDef_Init(self.ffi_def.get()) }; + + if module_def_ptr.is_null() { + return Err(PyImportError::new_err("PyModuleDef_Init returned NULL")); + } + + Ok(module_def_ptr.cast()) + } + + pub fn set_multiphase_items(&'static self, slots: state::ModuleDefSlots) { + let ffi_def = self.ffi_def.get(); + unsafe { + (*ffi_def).m_size = std::mem::size_of::() as ffi::Py_ssize_t; + (*ffi_def).m_slots = slots.into_inner(); + (*ffi_def).m_traverse = Some(state::module_state_traverse); + (*ffi_def).m_clear = Some(state::module_state_clear); + (*ffi_def).m_free = Some(state::module_state_free); + }; } } @@ -204,7 +216,44 @@ impl PyAddToModule for PyMethodDef { /// For adding a module to a module. impl PyAddToModule for ModuleDef { fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { - module.add_submodule(self.make_module(module.py())?.bind(module.py())) + let parent_ptr = module.as_ptr(); + let parent_name = std::ffi::CString::new(module.name()?.to_string())?; + + let add_to_parent = |child_ptr: *mut ffi::PyObject| -> std::ffi::c_int { + // TODO: reference to child_ptr is stolen - check if this is fine here? + let ret = + unsafe { ffi::PyModule_AddObject(parent_ptr, parent_name.as_ptr(), child_ptr) }; + + // TODO: .. as well as this error handling here - is this fine + // inside Py_mod_exec slots? + if ret < 0 { + unsafe { ffi::Py_DECREF(parent_ptr) }; + return -1; + } + + 0 + }; + + // SAFETY: We only use this closure inside the ModuleDef's slots and + // then immediately initialize the module - this closure / + // "function pointer" isn't used anywhere else afterwards and can't + // outlive the current thread. + let add_to_parent = unsafe { state::alloc_closure(add_to_parent) }; + + let slots = [ + state::ModuleDefSlot::start(), + state::ModuleDefSlot::new(ffi::Py_mod_exec, add_to_parent), + #[cfg(Py_3_12)] + state::ModuleDefSlot::per_interpreter_gil(), + state::ModuleDefSlot::end(), + ]; + + let slots = state::alloc_slots(slots); + self.set_multiphase_items(slots); + + let _module_def_ptr = self.make_module(module.py())?; + + Ok(()) } } @@ -218,50 +267,31 @@ mod tests { use crate::{ ffi, + impl_::pymodule_state as state, types::{any::PyAnyMethods, module::PyModuleMethods, PyModule}, Bound, PyResult, Python, }; - use super::{ModuleDef, ModuleInitializer}; + use super::ModuleDef; #[test] fn module_init() { - static MODULE_DEF: ModuleDef = unsafe { - ModuleDef::new( - ffi::c_str!("test_module"), - ffi::c_str!("some doc"), - ModuleInitializer(|m| { - m.add("SOME_CONSTANT", 42)?; - Ok(()) - }), - ) - }; + static MODULE_DEF: ModuleDef = + unsafe { ModuleDef::new(ffi::c_str!("test_module"), ffi::c_str!("some doc")) }; + + let slots = [ + state::ModuleDefSlot::start(), + #[cfg(Py_3_12)] + state::ModuleDefSlot::per_interpreter_gil(), + state::ModuleDefSlot::end(), + ]; + + MODULE_DEF.set_multiphase_items(state::alloc_slots(slots)); + Python::with_gil(|py| { - let module = MODULE_DEF.make_module(py).unwrap().into_bound(py); - assert_eq!( - module - .getattr("__name__") - .unwrap() - .extract::>() - .unwrap(), - "test_module", - ); - assert_eq!( - module - .getattr("__doc__") - .unwrap() - .extract::>() - .unwrap(), - "some doc", - ); - assert_eq!( - module - .getattr("SOME_CONSTANT") - .unwrap() - .extract::() - .unwrap(), - 42, - ); + let module_def = MODULE_DEF.make_module(py).unwrap(); + // FIXME: get PyModule from PyModuleDef ..? + unimplemented!("Test currently not implemented"); }) } @@ -272,6 +302,13 @@ mod tests { static NAME: &CStr = ffi::c_str!("test_module"); static DOC: &CStr = ffi::c_str!("some doc"); + let slots = [ + state::ModuleDefSlot::start(), + #[cfg(Py_3_12)] + state::ModuleDefSlot::per_interpreter_gil(), + state::ModuleDefSlot::end(), + ]; + static INIT_CALLED: AtomicBool = AtomicBool::new(false); #[allow(clippy::unnecessary_wraps)] @@ -281,12 +318,12 @@ mod tests { } unsafe { - let module_def: ModuleDef = ModuleDef::new(NAME, DOC, ModuleInitializer(init)); - assert_eq!((*module_def.ffi_def.get()).m_name, NAME.as_ptr() as _); - assert_eq!((*module_def.ffi_def.get()).m_doc, DOC.as_ptr() as _); + static MODULE_DEF: ModuleDef = unsafe { ModuleDef::new(NAME, DOC) }; + MODULE_DEF.set_multiphase_items(state::alloc_slots(slots)); + assert_eq!((*MODULE_DEF.ffi_def.get()).m_name, NAME.as_ptr() as _); + assert_eq!((*MODULE_DEF.ffi_def.get()).m_doc, DOC.as_ptr() as _); - Python::with_gil(|py| { - module_def.initializer.0(&py.import_bound("builtins").unwrap()).unwrap(); + Python::with_gil(|_py| { assert!(INIT_CALLED.load(Ordering::SeqCst)); }) } diff --git a/src/impl_/pymodule_state.rs b/src/impl_/pymodule_state.rs new file mode 100644 index 00000000000..b471aaffc18 --- /dev/null +++ b/src/impl_/pymodule_state.rs @@ -0,0 +1,398 @@ +use std::ffi::{c_int, c_void}; +use std::ptr::{self, NonNull}; + +use crate::ffi; + +thread_local! { + /// De-facto statically allocated function pointers to be used as fields + /// of [`ffi::PyModuleDef_Slot`]s. + /// + /// The [`SlotsClosureAllocs`] wrapper ensures that no [`c_void`] + /// function pointer created at runtime is ever deallocated. Thus each pointer + /// continues to exist throughout the remaining lifetime of the program and + /// is therefore "de-facto static". These pointers may then be given to the + /// [`ffi::PyModuleDef_Slot`]s used during multi-phase module initialization. + static CLOSURE_ALLOCS: std::sync::Mutex = + std::sync::Mutex::new(SlotsClosureAllocs::new()); + + /// De-facto statically allocated [`ffi::PyModuleDef_Slot`]s. + /// + /// The [`SlotAllocs`] wrapper ensures that no [`ffi::PyModuleDef_Slot`]s + /// created at runtime are ever deallocated. Thus they continue to exist + /// throughout the remaining program, making them "de-facto static". This allows + /// these slots to be used for [`ffi::PyModuleDef`] (specifically as `m_slots` + /// field). + static SLOT_ALLOCS: std::sync::Mutex = + std::sync::Mutex::new(SlotAllocs::new()); +} + +/// `Send` and `Sync` wrapper `struct` for a closure that has been boxed by +/// and converted into a pointer ([`Box::into_raw`]) by [`alloc_closure`]. +/// +/// When dropped, boxes and calls the accompanying `dealloc_ptr`, which ought +/// to be *another* closure that converts the wrapped closure `closure_ptr` back +/// to its boxed type ([`Box::from_raw`]) and drops it. +struct SlotsClosure { + #[allow(unused)] + closure_ptr: *mut c_void, + dealloc_ptr: *mut dyn FnOnce(), +} + +unsafe impl Send for SlotsClosure {} +unsafe impl Sync for SlotsClosure {} + +impl SlotsClosure { + /// Creates a new [`SlotsClosure`]. + /// + /// SAFETY: `closure_ptr` and `dealloc_ptr` must have been acquired via + /// [`alloc_closure`] before. + const unsafe fn new(closure_ptr: *mut c_void, dealloc_ptr: *mut dyn FnOnce()) -> Self { + Self { + closure_ptr, + dealloc_ptr, + } + } +} + +impl Drop for SlotsClosure { + fn drop(&mut self) { + // SAFETY: We obtained this pointer via Box::into_raw earlier + let dealloc_boxed: Box = unsafe { Box::from_raw(self.dealloc_ptr) }; + dealloc_boxed(); // closure_ptr is now invalid! + } +} + +/// Stores function pointers used during multi-phase module initialization. +/// See [`CLOSURE_ALLOCS`] and [`SlotsClosureAllocs::alloc_closure`] for more +/// information. +struct SlotsClosureAllocs { + allocs: Vec, +} + +impl SlotsClosureAllocs { + const fn new() -> Self { + Self { allocs: Vec::new() } + } + + /// Underlying implementation of [`alloc_closure`]. + /// + /// This not only boxes and cases the given `closure` to a [`*mut c_void`], + /// but also creates a second closure that acts as the drop handler of the + /// passed one. + /// + /// The pointers of both closures are wrapped by [`SlotsClosure`] which + /// implements a corresponding [`Drop`] handler. Each [`SlotsClosure`] is + /// stored in [`Self::allocs`]. + /// + /// This means that each returned [`*mut c_void`] function pointer lives as + /// long as the [`SlotsClosureAllocs`] it is stored in. + /// + /// [`*mut c_void`]: c_void + unsafe fn alloc_closure(&mut self, closure: F) -> *mut c_void + where + F: FnMut(*mut ffi::PyObject) -> c_int, + { + let closure_ptr = Box::into_raw(Box::new(closure)); + + let casted_ptr = closure_ptr as *const _ as *mut u8; + + let dealloc_closure: Box = Box::new(move || { + let closure_ptr: *mut F = casted_ptr.cast(); + let boxed = unsafe { Box::from_raw(closure_ptr) }; + drop(boxed); + }); + + let closure_ptr = closure_ptr as *mut c_void; + + let dealloc_ptr = Box::into_raw(dealloc_closure); + + self.allocs + .push(unsafe { SlotsClosure::new(closure_ptr, dealloc_ptr) }); + + return closure_ptr; + } +} + +/// Takes a closure intended to be used as a function pointer for a +/// [`ffi::PyModuleDef_Slot`], boxes it, and returns a [`*mut c_void`] function +/// pointer. This pointer ought to then given to a [`ffi::PyModuleDef_Slot`] +/// directly or to its wrapper [`ModuleDefSlot`]. +/// +/// The boxed closure remains allocated for the remaining lifetime of the +/// program. +/// +/// SAFETY: The returned function pointer is solely to be used as [`Py_mod_exec`] +/// slot of the `m_slots` field of [`ffi::PyModuleDef`]. +/// +/// [`*mut c_void`]: c_void +/// [`Py_mod_exec`]: https://docs.python.org/3/c-api/module.html#c.Py_mod_exec +pub(crate) unsafe fn alloc_closure(closure: F) -> *mut c_void +where + F: FnMut(*mut ffi::PyObject) -> c_int, +{ + CLOSURE_ALLOCS.with(|allocs| { + let mut lock = allocs.lock().unwrap(); + unsafe { lock.alloc_closure(closure) } + }) +} + +/// [`Send`] and [`Sync`] wrapper of `ffi::PyModuleDef_Slot`. +#[repr(transparent)] +pub struct ModuleDefSlot(ffi::PyModuleDef_Slot); + +unsafe impl Send for ModuleDefSlot {} +unsafe impl Sync for ModuleDefSlot {} + +impl ModuleDefSlot { + /// Creates a new [`ModuleDefSlot`] from the given `slot` and `value`. + pub const fn new(slot: i32, value: *mut c_void) -> Self { + Self(ffi::PyModuleDef_Slot { slot, value }) + } + + /// Creates a new [`ModuleDefSlot`] with `id` [`ffi::Py_mod_exec`] and + /// [`module_state_init`] as function pointer. + /// + /// This slot should be used as the first element of the array of slots + /// passed to [`ffi::PyModuleDef`] during multi-phase initialization, + /// as it ensures that per-module state is initialized. + pub const fn start() -> Self { + Self::new(ffi::Py_mod_exec, module_state_init as *mut c_void) + } + + /// Creates a new [`ModuleDefSlot`] with `id` `0`, which marks the end of + /// the array of slots that is passed to [`ffi::PyModuleDef`] during + /// multi-phase initialization. + pub const fn end() -> Self { + Self::new(0, ptr::null_mut()) + } + + /// Checks if `self` is [`end`]. + /// + /// [`end`]: Self::end + pub const fn is_end(&self) -> bool { + self.0.slot == 0 + } +} + +#[cfg(Py_3_12)] +impl ModuleDefSlot { + /// Creates a new [`ModuleDefSlot`] that specifies that the module + /// [does not support being imported in subinterpreters]. + /// + /// [does not support being imported in subinterpreters]: https://docs.python.org/3/c-api/module.html#c.Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED + pub const fn no_multiple_interpreters() -> Self { + Self::new( + ffi::Py_mod_multiple_interpreters, + ffi::Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED, + ) + } + + /// Creates a new [`ModuleDefSlot`] that specifies that the module + /// [supports being imported in subinterpreters]. + /// + /// [supports being imported in subinterpreters]: https://docs.python.org/3/c-api/module.html#c.Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED + pub const fn multiple_interpreters() -> Self { + Self::new( + ffi::Py_mod_multiple_interpreters, + ffi::Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED, + ) + } + + /// Creates a new [`ModuleDefSlot`] that specifies that the module + /// [supports a separate GIL per subinterpreter]. + /// + /// [supports a separate GIL per subinterpreter]: https://docs.python.org/3/c-api/module.html#c.Py_MOD_PER_INTERPRETER_GIL_SUPPORTED + pub const fn per_interpreter_gil() -> Self { + Self::new( + ffi::Py_mod_multiple_interpreters, + ffi::Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, + ) + } +} + +/// [`Sync`] wrapper for a (de-facto) `static` slice of [`ffi::PyModuleDef_Slot`]s. +/// +/// This struct can be acquired by either converting it from an existing +/// [`&'static \[ModuleDefSlot\]`] or via [`alloc_slots`]. +/// +/// The inner [`*mut ffi::PyModuleDef_Slot`] points to the start of a C array +/// of [`ffi::PyModuleDef_Slot`] that is terminated by a slot with `id` `0` +/// (see [`ModuleDefSlot::end`]). +/// +/// [`*mut ffi::PyModuleDef_Slot`]: ffi::PyModuleDef_Slot +/// [`&'static \[ModuleDefSlot\]`]: ModuleDefSlot +#[repr(transparent)] +#[derive(Clone, Copy)] +pub struct ModuleDefSlots(*mut ffi::PyModuleDef_Slot); + +unsafe impl Sync for ModuleDefSlots {} + +impl ModuleDefSlots { + /// SAFETY: Requires that `ptr` points to an array of [`ffi::PyModuleDef_Slot`] + /// that is terminated by a slot with `id` `0`. + const unsafe fn new(ptr: *mut ffi::PyModuleDef_Slot) -> Self { + Self(ptr) + } + + /// Creates a new [`ModuleDefSlots`] struct from the given `slots`. + /// + /// NOTE: Will `panic!` if `slots` is empty or [not terminated correctly]. + /// + /// [not terminated correctly]: ModuleDefSlot::end + pub fn new_from_static(slots: &'static [ModuleDefSlot]) -> Self { + match slots.last() { + Some(last) if !last.is_end() => panic!("slot array is not terminated correctly"), + Some(_) => {} + None => panic!("slot array is empty"), + }; + + Self(slots as *const _ as *mut ffi::PyModuleDef_Slot) + } + + /// Returns the inner [`*mut ffi::PyModuleDef_Slot`]. + /// + /// [`*mut ffi::PyModuleDef_Slot`]: ffi::PyModuleDef_Slot + pub(crate) const fn into_inner(self) -> *mut ffi::PyModuleDef_Slot { + self.0 + } +} + +/// Stores vectors of slots used during multi-phase module initialization. +/// See [`SLOT_ALLOCS`] and [`SlotAllocs::alloc_slots`] for more information. +struct SlotAllocs { + slots_list: Vec>, +} + +impl SlotAllocs { + const fn new() -> Self { + Self { + slots_list: Vec::new(), + } + } + + fn alloc_slots(&mut self, slots: impl IntoIterator) -> ModuleDefSlots { + let slots: Vec = slots.into_iter().collect(); + + self.slots_list.push(slots); + let slots = self + .slots_list + .last() + .expect("slots_list.last() should never be None") + .as_slice(); + + unsafe { ModuleDefSlots::new(slots as *const _ as *mut ffi::PyModuleDef_Slot) } + } +} + +/// Takes the given `slots` to be used for multi-phase module initialization, +/// allocates them on the heap and returns a corresponding [`ModuleDefSlots`]. +/// +/// The `slots` remain allocated for the remaining lifetime of the program. +pub fn alloc_slots(slots: impl IntoIterator) -> ModuleDefSlots { + SLOT_ALLOCS.with(|allocs| { + let mut lock = allocs.lock().unwrap(); + lock.alloc_slots(slots) + }) +} + +/// Represents a Python module's state. +/// +/// More precisely, this `struct` resides on the per-module memory area +/// allocated during the module's creation. +#[repr(C)] +#[derive(Debug)] +pub struct ModuleState { + inner: Option>, +} + +impl ModuleState { + pub fn new() -> Self { + let boxed = Box::new(ModuleStateImpl::new()); + + Self { + inner: NonNull::new(Box::into_raw(boxed)), + } + } +} + +impl Default for ModuleState { + fn default() -> Self { + Self::new() + } +} + +/// Inner layout of [`ModuleState`]. +/// +/// In order to guarantee that all resources acquired during the initialization +/// of per-module state are correctly released, this `struct` exists as the sole +/// field of [`ModuleState`] in the form of a pointer. This allows +/// [`module_state_free`] to safely [`drop`] this `struct` when [`ModuleState`] +/// is being deallocated by the Python interpreter. +#[repr(C)] +#[derive(Debug)] +struct ModuleStateImpl {} + +impl ModuleStateImpl { + fn new() -> Self { + Self {} + } +} + +/// Called during multi-phase initialization in order to create an instance of +/// [`ModuleState`] on the memory area specific to modules. +/// +/// Slot: [`Py_mod_exec`] +/// +/// [`Py_mod_exec`]: https://docs.python.org/3/c-api/module.html#c.Py_mod_exec +pub unsafe extern "C" fn module_state_init(module: *mut ffi::PyObject) -> c_int { + let state: *mut ModuleState = ffi::PyModule_GetState(module.cast()).cast(); + + if state.is_null() { + *state = ModuleState::new(); + return 0; + } + + 0 +} + +/// Called during GC traversal of the module object. +/// +/// Used for the [`m_traverse`] field of [`PyModuleDef`]. +/// +/// [`m_traverse`]: https://docs.python.org/3/c-api/module.html#c.PyModuleDef.m_traverse +/// [`PyModuleDef`]: https://docs.python.org/3/c-api/module.html#c.PyModuleDef +pub unsafe extern "C" fn module_state_traverse( + _module: *mut ffi::PyObject, + _visit: ffi::visitproc, + _arg: *mut c_void, +) -> c_int { + 0 +} + +/// Called during GC clearing of the module object. +/// +/// Used for the [`m_clear`] field of [`PyModuleDef`]. +/// +/// [`m_clear`]: https://docs.python.org/3/c-api/module.html#c.PyModuleDef.m_clear +/// [`PyModuleDef`]: https://docs.python.org/3/c-api/module.html#c.PyModuleDef +pub unsafe extern "C" fn module_state_clear(_module: *mut ffi::PyObject) -> c_int { + // Should any PyObjects be made part of ModuleState or ModuleStateInner, + // these have to be Py_CLEARed here. + // See: examples/sequential/src/module.rs + 0 +} + +/// Called during deallocation of the module object. +/// +/// Used for the [`m_free`] field of [`PyModuleDef`]. +/// +/// [`m_free`]: https://docs.python.org/3/c-api/module.html#c.PyModuleDef.m_free +/// [`PyModuleDef`]: https://docs.python.org/3/c-api/module.html#c.PyModuleDef +pub unsafe extern "C" fn module_state_free(module: *mut c_void) { + let state: *mut ModuleState = ffi::PyModule_GetState(module.cast()).cast(); + if let Some(inner) = (*state).inner { + let ptr = inner.as_ptr(); + // SAFETY: We obtained this pointer via Box::into_raw beforehand. + drop(unsafe { Box::from_raw(ptr) }); + } +} diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs index f485258e5e5..0ba63680a74 100644 --- a/src/impl_/trampoline.rs +++ b/src/impl_/trampoline.rs @@ -12,14 +12,14 @@ use std::{ use crate::gil::GILGuard; use crate::{ callback::PyCallbackOutput, ffi, ffi_ptr_ext::FfiPtrExt, impl_::panic::PanicTrap, - methods::IPowModulo, panic::PanicException, types::PyModule, Py, PyResult, Python, + methods::IPowModulo, panic::PanicException, PyResult, Python, }; #[inline] pub unsafe fn module_init( - f: for<'py> unsafe fn(Python<'py>) -> PyResult>, + f: for<'py> unsafe fn(Python<'py>) -> PyResult<*mut ffi::PyModuleDef>, ) -> *mut ffi::PyObject { - trampoline(|py| f(py).map(|module| module.into_ptr())) + trampoline(|py| f(py).map(|module_def| module_def.cast())) } #[inline] diff --git a/src/instance.rs b/src/instance.rs index 3b8e2529a6e..e268af4de73 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1770,7 +1770,7 @@ impl Py { /// /// # Safety /// `ptr` must point to a Python object of type T. - unsafe fn from_non_null(ptr: NonNull) -> Self { + pub unsafe fn from_non_null(ptr: NonNull) -> Self { Self(ptr, PhantomData) } } diff --git a/src/types/module.rs b/src/types/module.rs index 6b90e5de197..2beb454c62a 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -45,6 +45,7 @@ impl PyModule { /// })?; /// # Ok(())} /// ``` + // TODO: Use multi-phase initialization pub fn new_bound<'py>(py: Python<'py>, name: &str) -> PyResult> { // Could use PyModule_NewObject, but it doesn't exist on PyPy. let name = CString::new(name)?;