Skip to content

Commit 5589b17

Browse files
bazaahdavidhewitt
andauthored
feat: implement multi-phase initialization (PEP-489) (#5525)
* convert module initialization to use multi-phase-init * attempt to fix CI issues * alternative spec implementation, fix oob read * fix MSRV * need to exec multi-phase-init module * tests/ui: rere invalid_result_conversion * newsfragments: add PR fragment * src/impl_: drop gil_used_once special casing This code was for user convenience when gil_used defaulted to true, but since we're changing that to false, it is no longer needed. * pyo3-macros-backend: switch default gil_used to false * tests/ui: rere invalid_property_args * src/impl_: use mem::zeroed over ZEROED_SLOT --------- Co-authored-by: David Hewitt <mail@davidhewitt.dev>
1 parent 1a3e53f commit 5589b17

File tree

7 files changed

+236
-109
lines changed

7 files changed

+236
-109
lines changed

newsfragments/5525.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
added PEP-489 multi-phase initialization for pymodules

pyo3-macros-backend/src/module.rs

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{
1515
get_doc,
1616
pyclass::PyClassPyO3Option,
1717
pyfunction::{impl_wrap_pyfunction, PyFunctionOptions},
18-
utils::{has_attribute, has_attribute_with_namespace, Ctx, IdentOrStr},
18+
utils::{has_attribute, has_attribute_with_namespace, Ctx, IdentOrStr, PythonDoc},
1919
};
2020
use proc_macro2::{Span, TokenStream};
2121
use quote::quote;
@@ -389,23 +389,15 @@ pub fn pymodule_module_impl(
389389
#[cfg(not(feature = "experimental-inspect"))]
390390
let introspection_id = quote! {};
391391

392-
let module_def = quote! {{
393-
use #pyo3_path::impl_::pymodule as impl_;
394-
const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule);
395-
unsafe {
396-
impl_::ModuleDef::new(
397-
__PYO3_NAME,
398-
#doc,
399-
INITIALIZER
400-
)
401-
}
402-
}};
392+
let gil_used = options.gil_used.is_some_and(|op| op.value.value);
393+
403394
let initialization = module_initialization(
404395
&name,
405396
ctx,
406-
module_def,
397+
quote! { __pyo3_pymodule },
407398
options.submodule.is_some(),
408-
options.gil_used.is_none_or(|op| op.value.value),
399+
gil_used,
400+
doc,
409401
);
410402

411403
let module_consts_names = module_consts.iter().map(|i| i.unraw().to_string());
@@ -456,12 +448,15 @@ pub fn pymodule_function_impl(
456448
let vis = &function.vis;
457449
let doc = get_doc(&function.attrs, None, ctx)?;
458450

451+
let gil_used = options.gil_used.is_some_and(|op| op.value.value);
452+
459453
let initialization = module_initialization(
460454
&name,
461455
ctx,
462-
quote! { MakeDef::make_def() },
456+
quote! { ModuleExec::__pyo3_module_exec },
463457
false,
464-
options.gil_used.is_none_or(|op| op.value.value),
458+
gil_used,
459+
doc,
465460
);
466461

467462
#[cfg(feature = "experimental-inspect")]
@@ -495,20 +490,9 @@ pub fn pymodule_function_impl(
495490
// (and `super` doesn't always refer to the outer scope, e.g. if the `#[pymodule] is
496491
// inside a function body)
497492
#[allow(unknown_lints, non_local_definitions)]
498-
impl #ident::MakeDef {
499-
const fn make_def() -> #pyo3_path::impl_::pymodule::ModuleDef {
500-
fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> {
501-
#ident(#(#module_args),*)
502-
}
503-
504-
const INITIALIZER: #pyo3_path::impl_::pymodule::ModuleInitializer = #pyo3_path::impl_::pymodule::ModuleInitializer(__pyo3_pymodule);
505-
unsafe {
506-
#pyo3_path::impl_::pymodule::ModuleDef::new(
507-
#ident::__PYO3_NAME,
508-
#doc,
509-
INITIALIZER
510-
)
511-
}
493+
impl #ident::ModuleExec {
494+
fn __pyo3_module_exec(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> {
495+
#ident(#(#module_args),*)
512496
}
513497
}
514498
})
@@ -517,9 +501,10 @@ pub fn pymodule_function_impl(
517501
fn module_initialization(
518502
name: &syn::Ident,
519503
ctx: &Ctx,
520-
module_def: TokenStream,
504+
module_exec: TokenStream,
521505
is_submodule: bool,
522506
gil_used: bool,
507+
doc: PythonDoc,
523508
) -> TokenStream {
524509
let Ctx { pyo3_path, .. } = ctx;
525510
let pyinit_symbol = format!("PyInit_{name}");
@@ -530,9 +515,27 @@ fn module_initialization(
530515
#[doc(hidden)]
531516
pub const __PYO3_NAME: &'static ::std::ffi::CStr = #pyo3_name;
532517

533-
pub(super) struct MakeDef;
518+
// This structure exists for `fn` modules declared within `fn` bodies, where due to the hidden
519+
// module (used for importing) the `fn` to initialize the module cannot be seen from the #module_def
520+
// declaration just below.
534521
#[doc(hidden)]
535-
pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = #module_def;
522+
pub(super) struct ModuleExec;
523+
524+
#[doc(hidden)]
525+
pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = {
526+
use #pyo3_path::impl_::pymodule as impl_;
527+
528+
unsafe extern "C" fn __pyo3_module_exec(module: *mut #pyo3_path::ffi::PyObject) -> ::std::os::raw::c_int {
529+
#pyo3_path::impl_::trampoline::module_exec(module, #module_exec)
530+
}
531+
532+
static SLOTS: impl_::PyModuleSlots<4> = impl_::PyModuleSlotsBuilder::new()
533+
.with_mod_exec(__pyo3_module_exec)
534+
.with_gil_used(#gil_used)
535+
.build();
536+
537+
impl_::ModuleDef::new(__PYO3_NAME, #doc, &SLOTS)
538+
};
536539
#[doc(hidden)]
537540
// so wrapped submodules can see what gil_used is
538541
pub static __PYO3_GIL_USED: bool = #gil_used;
@@ -544,7 +547,10 @@ fn module_initialization(
544547
#[doc(hidden)]
545548
#[export_name = #pyinit_symbol]
546549
pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject {
547-
unsafe { #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py, #gil_used)) }
550+
_PYO3_DEF.init_multi_phase(
551+
unsafe { #pyo3_path::Python::assume_attached() },
552+
#gil_used
553+
)
548554
}
549555
});
550556
}

pytests/tests/test_misc.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def test_multiple_imports_same_interpreter_ok():
2424
spec = importlib.util.find_spec("pyo3_pytests.pyo3_pytests")
2525

2626
module = importlib.util.module_from_spec(spec)
27+
spec.loader.exec_module(module)
2728
assert dir(module) == dir(pyo3_pytests.pyo3_pytests)
2829

2930

0 commit comments

Comments
 (0)