From 117205b43ec3417a3f93baa77385c7881246917c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 8 May 2026 11:12:55 -0600 Subject: [PATCH 01/15] Add support for the 3.15 ABI as of 3.15.0b1 --- .github/workflows/ci.yml | 18 +++ newsfragments/6014.added.md | 4 + newsfragments/6014.changed.md | 1 + pyo3-ffi-check/macro/src/lib.rs | 7 + pyo3-ffi/examples/sequential/src/lib.rs | 2 +- pyo3-ffi/examples/sequential/src/module.rs | 66 ++++----- pyo3-ffi/examples/string-sum/src/lib.rs | 44 +++--- pyo3-ffi/src/cpython/critical_section.rs | 29 +--- pyo3-ffi/src/cpython/mod.rs | 6 +- pyo3-ffi/src/cpython/object.rs | 3 + pyo3-ffi/src/critical_section.rs | 34 +++++ pyo3-ffi/src/lib.rs | 64 ++++----- pyo3-ffi/src/moduleobject.rs | 34 +---- pyo3-ffi/src/object.rs | 22 +++ pyo3-ffi/src/slots.rs | 152 +++++++++++++++++++++ pyo3-ffi/src/slots_generated.rs | 142 +++++++++++++++++++ pyo3-ffi/src/typeslots.rs | 68 ++++++++- pyo3-macros-backend/src/module.rs | 11 +- src/impl_/pymodule.rs | 115 ++++++++++++++-- 19 files changed, 645 insertions(+), 177 deletions(-) create mode 100644 newsfragments/6014.added.md create mode 100644 newsfragments/6014.changed.md create mode 100644 pyo3-ffi/src/critical_section.rs create mode 100644 pyo3-ffi/src/slots.rs create mode 100644 pyo3-ffi/src/slots_generated.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38a8e2bddee..1526f613657 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -204,6 +204,24 @@ jobs: python-architecture: "x64", rust-target: "x86_64-unknown-linux-gnu", } + # Also test Python 3.15 and 3.15t, on ubuntu + # (run for all OSes on build-full) + - rust: stable + python-version: "3.15-dev" + platform: + { + os: "ubuntu-latest", + python-architecture: "x64", + rust-target: "x86_64-unknown-linux-gnu", + } + - rust: stable + python-version: "3.15t-dev" + platform: + { + os: "ubuntu-latest", + python-architecture: "x64", + rust-target: "x86_64-unknown-linux-gnu", + } build-full: if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} diff --git a/newsfragments/6014.added.md b/newsfragments/6014.added.md new file mode 100644 index 00000000000..feb0e2a00bb --- /dev/null +++ b/newsfragments/6014.added.md @@ -0,0 +1,4 @@ +* Added support for Python 3.15.0b1. +* Added FFI bindings for the PEP 820 PySlot C API. +* Added bindings for the critical section API in the limited API on Python 3.15 + and newer. diff --git a/newsfragments/6014.changed.md b/newsfragments/6014.changed.md new file mode 100644 index 00000000000..5ff683a0dd3 --- /dev/null +++ b/newsfragments/6014.changed.md @@ -0,0 +1 @@ +* Updated FFI bindings and module initialization to use PEP 820 PySlot API. diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index 7434f4dbc2b..a14319d6ecf 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -181,6 +181,13 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream // the field name in the C API is `type`, but that's a keyword in Rust // so PyO3 picked type_code, bindgen picked type_ Ident::new("type_", Span::call_site()) + } else if struct_name == "PySlot" && field_name == "anon1" { + // PySlot has two anonymous unions (since they aren't allowed in Rust, + // PyO3 invented names for them); bindgen names the first one __bindgen_anon_1 + Ident::new("__bindgen_anon_1", Span::call_site()) + } else if struct_name == "PySlot" && field_name == "anon2" { + // ...and the second one __bindgen_anon_2 + Ident::new("__bindgen_anon_2", Span::call_site()) } else { field_ident.clone() }; diff --git a/pyo3-ffi/examples/sequential/src/lib.rs b/pyo3-ffi/examples/sequential/src/lib.rs index 3c08e6a7c42..499e2877e87 100644 --- a/pyo3-ffi/examples/sequential/src/lib.rs +++ b/pyo3-ffi/examples/sequential/src/lib.rs @@ -17,6 +17,6 @@ pub unsafe extern "C" fn PyInit_sequential() -> *mut PyObject { #[cfg(Py_3_15)] #[allow(non_snake_case, reason = "must be named `PyModExport_`")] #[no_mangle] -pub unsafe extern "C" fn PyModExport_sequential() -> *mut PyModuleDef_Slot { +pub unsafe extern "C" fn PyModExport_sequential() -> *mut PySlot { (&raw mut SEQUENTIAL_SLOTS).cast() } diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index a7d47f259a8..f66129fc95a 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -20,44 +20,36 @@ PyABIInfo_VAR!(ABI_INFO); const SEQUENTIAL_SLOTS_LEN: usize = 2 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 7 * (cfg!(Py_3_15) as usize); +#[cfg(Py_3_15)] +pub static mut SEQUENTIAL_SLOTS: [PySlot; SEQUENTIAL_SLOTS_LEN] = [ + PySlot_STATIC_DATA(Py_mod_abi, (&raw mut ABI_INFO).cast()), + PySlot_STATIC_DATA(Py_mod_name, c"sequential".as_ptr() as *mut c_void), + PySlot_STATIC_DATA( + Py_mod_doc, + c"A library for generating sequential ids, written in Rust.".as_ptr() as *mut c_void, + ), + PySlot_SIZE( + Py_mod_state_size, + mem::size_of::() as Py_ssize_t, + ), + PySlot_FUNC!(Py_mod_state_traverse, traverseproc, sequential_traverse), + PySlot_FUNC!(Py_mod_state_clear, inquiry, sequential_clear), + PySlot_FUNC!(Py_mod_state_free, freefunc, sequential_free), + PySlot_FUNC!( + Py_mod_exec, + unsafe extern "C" fn(*mut PyObject) -> c_int, + sequential_exec + ), + PySlot_DATA( + Py_mod_multiple_interpreters, + Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, + ), + #[cfg(Py_GIL_DISABLED)] + PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED), + PySlot_END(), +]; +#[cfg(not(Py_3_15))] pub static mut SEQUENTIAL_SLOTS: [PyModuleDef_Slot; SEQUENTIAL_SLOTS_LEN] = [ - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_abi, - value: (&raw mut ABI_INFO).cast(), - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_name, - // safety: Python does not write to this field - value: c"sequential".as_ptr() as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_doc, - // safety: Python does not write to this field - value: c"A library for generating sequential ids, written in Rust.".as_ptr() as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_state_size, - value: mem::size_of::() as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_state_traverse, - value: sequential_traverse as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_state_clear, - value: sequential_clear as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_state_free, - value: sequential_free as *mut c_void, - }, PyModuleDef_Slot { slot: Py_mod_exec, value: sequential_exec as *mut c_void, diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index 5081405b773..e4f7eb0347c 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -1,7 +1,6 @@ #[cfg(Py_3_15)] use std::ffi::c_void; use std::ffi::{c_char, c_long}; -use std::ptr; use pyo3_ffi::*; @@ -36,29 +35,26 @@ PyABIInfo_VAR!(ABI_INFO); const SLOTS_LEN: usize = 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 4 * (cfg!(Py_3_15) as usize); +#[cfg(Py_3_15)] +static mut SLOTS: [PySlot; SLOTS_LEN] = [ + PySlot_STATIC_DATA(Py_mod_abi, (&raw mut ABI_INFO).cast()), + // safety: Python does not write to these static fields + PySlot_STATIC_DATA(Py_mod_name, c"string_sum".as_ptr() as *mut c_void), + PySlot_STATIC_DATA( + Py_mod_doc, + c"A Python module written in Rust.".as_ptr() as *mut c_void, + ), + PySlot_STATIC_DATA(Py_mod_methods, (&raw mut METHODS).cast()), + PySlot_DATA( + Py_mod_multiple_interpreters, + Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, + ), + #[cfg(Py_GIL_DISABLED)] + PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED), + PySlot_END(), +]; +#[cfg(not(Py_3_15))] static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_abi, - value: (&raw mut ABI_INFO).cast(), - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_name, - // safety: Python does not write to this field - value: c"string_sum".as_ptr() as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_doc, - // safety: Python does not write to this field - value: c"A Python module written in Rust.".as_ptr() as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_methods, - value: (&raw mut METHODS).cast(), - }, #[cfg(Py_3_12)] PyModuleDef_Slot { slot: Py_mod_multiple_interpreters, @@ -71,7 +67,7 @@ static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ }, PyModuleDef_Slot { slot: 0, - value: ptr::null_mut(), + value: std::ptr::null_mut(), }, ]; diff --git a/pyo3-ffi/src/cpython/critical_section.rs b/pyo3-ffi/src/cpython/critical_section.rs index 5db205b9840..4862a24f281 100644 --- a/pyo3-ffi/src/cpython/critical_section.rs +++ b/pyo3-ffi/src/cpython/critical_section.rs @@ -1,38 +1,11 @@ -#[cfg(any(Py_3_14, Py_GIL_DISABLED))] use crate::PyMutex; -use crate::PyObject; - -#[repr(C)] -#[cfg(Py_GIL_DISABLED)] -pub struct PyCriticalSection { - _cs_prev: usize, - _cs_mutex: *mut PyMutex, -} - -#[repr(C)] -#[cfg(Py_GIL_DISABLED)] -pub struct PyCriticalSection2 { - _cs_base: PyCriticalSection, - _cs_mutex2: *mut PyMutex, -} - -#[cfg(not(Py_GIL_DISABLED))] -opaque_struct!(pub PyCriticalSection); - -#[cfg(not(Py_GIL_DISABLED))] -opaque_struct!(pub PyCriticalSection2); +use crate::{PyCriticalSection, PyCriticalSection2}; extern_libpython! { - pub fn PyCriticalSection_Begin(c: *mut PyCriticalSection, op: *mut PyObject); - #[cfg(Py_3_14)] pub fn PyCriticalSection_BeginMutex(c: *mut PyCriticalSection, m: *mut PyMutex); - pub fn PyCriticalSection_End(c: *mut PyCriticalSection); - pub fn PyCriticalSection2_Begin(c: *mut PyCriticalSection2, a: *mut PyObject, b: *mut PyObject); - #[cfg(Py_3_14)] pub fn PyCriticalSection2_BeginMutex( c: *mut PyCriticalSection2, m1: *mut PyMutex, m2: *mut PyMutex, ); - pub fn PyCriticalSection2_End(c: *mut PyCriticalSection2); } diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index c8215212b37..3590c9f4d38 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -7,7 +7,7 @@ pub(crate) mod ceval; pub(crate) mod code; pub(crate) mod compile; pub(crate) mod complexobject; -#[cfg(Py_3_13)] +#[cfg(all(Py_3_14, Py_GIL_DISABLED))] pub(crate) mod critical_section; pub(crate) mod descrobject; pub(crate) mod dictobject; @@ -56,8 +56,8 @@ pub use self::ceval::*; pub use self::code::*; pub use self::compile::*; pub use self::complexobject::*; -#[cfg(Py_3_13)] -pub use self::critical_section::*; +#[cfg(all(Py_3_14, Py_GIL_DISABLED))] +pub use self::critical_section::{PyCriticalSection2_BeginMutex, PyCriticalSection_BeginMutex}; pub use self::descrobject::*; pub use self::dictobject::*; pub use self::floatobject::*; diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index dcde54bd90b..1aff7835a57 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -6,6 +6,7 @@ use std::mem; // skipped private _Py_NewReference // skipped private _Py_NewReferenceNoTotal // skipped private _Py_ResurrectReference +// skipped private _Py_ForgetReference // skipped private _Py_GetGlobalRefTotal // skipped private _Py_GetRefTotal @@ -277,6 +278,8 @@ pub struct PyTypeObject { pub tp_next: *mut PyTypeObject, #[cfg(Py_3_13)] pub tp_versions_used: u16, + #[cfg(Py_3_15)] + pub _tp_iteritem: Option, } #[cfg(Py_3_11)] diff --git a/pyo3-ffi/src/critical_section.rs b/pyo3-ffi/src/critical_section.rs new file mode 100644 index 00000000000..9249df7aad4 --- /dev/null +++ b/pyo3-ffi/src/critical_section.rs @@ -0,0 +1,34 @@ +#[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] +use crate::PyMutex; +#[cfg(any(all(Py_GIL_DISABLED, Py_3_13), Py_3_15))] +use crate::PyObject; + +#[cfg(all(Py_LIMITED_API, Py_3_13, not(Py_3_15)))] +opaque_struct!(pub PyMutex); + +#[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] +#[repr(C)] +pub struct PyCriticalSection { + _cs_prev: usize, + _cs_mutex: *mut PyMutex, +} + +#[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] +#[repr(C)] +pub struct PyCriticalSection2 { + _cs_base: PyCriticalSection, + _cs_mutex2: *mut PyMutex, +} + +#[cfg(all(not(Py_GIL_DISABLED), Py_3_13, not(Py_3_15), not(Py_LIMITED_API)))] +opaque_struct!(pub PyCriticalSection); +#[cfg(all(not(Py_GIL_DISABLED), Py_3_13, not(Py_3_15), not(Py_LIMITED_API)))] +opaque_struct!(pub PyCriticalSection2); + +#[cfg(any(all(Py_GIL_DISABLED, Py_3_13), Py_3_15))] +extern_libpython! { + pub fn PyCriticalSection_Begin(c: *mut PyCriticalSection, op: *mut PyObject); + pub fn PyCriticalSection_End(c: *mut PyCriticalSection); + pub fn PyCriticalSection2_Begin(c: *mut PyCriticalSection2, a: *mut PyObject, b: *mut PyObject); + pub fn PyCriticalSection2_End(c: *mut PyCriticalSection2); +} diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index df72ac7b23e..1038b6f254b 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -40,7 +40,7 @@ //! PyO3 uses `rustc`'s `--cfg` flags to enable or disable code used for different Python versions. //! If you want to do this for your own crate, you can do so with the [`pyo3-build-config`] crate. //! -//! - `Py_3_8`, `Py_3_9`, `Py_3_10`, `Py_3_11`, `Py_3_12`, `Py_3_13`, `Py_3_14`: Marks code that is +//! - `Py_3_8`, `Py_3_9`, `Py_3_10`, `Py_3_11`, `Py_3_12`, `Py_3_13`, `Py_3_14`, `Py_3_15`: Marks code that is //! only enabled when compiling for a given minimum Python version. //! - `Py_LIMITED_API`: Marks code enabled when the `abi3` feature flag is enabled. //! - `Py_GIL_DISABLED`: Marks code that runs only in the free-threaded build of CPython. @@ -128,6 +128,8 @@ //! ```rust,no_run //! #[cfg(Py_3_15)] //! use std::ffi::c_void; +//! #[cfg(not(Py_3_15))] +//! use std::ffi::c_int; //! use std::ffi::{c_char, c_long}; //! use std::ptr; //! @@ -159,35 +161,11 @@ //! PyMethodDef::zeroed(), //! ]; //! -//! #[cfg(Py_3_15)] -//! PyABIInfo_VAR!(ABI_INFO); -//! +//! #[cfg(not(Py_3_15))] //! const SLOTS_LEN: usize = -//! 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 4 * (cfg!(Py_3_15) as usize); +//! 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize; +//! #[cfg(not(Py_3_15))] //! static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ -//! #[cfg(Py_3_15)] -//! PyModuleDef_Slot { -//! slot: Py_mod_abi, -//! value: (&raw mut ABI_INFO).cast(), -//! }, -//! #[cfg(Py_3_15)] -//! PyModuleDef_Slot { -//! slot: Py_mod_name, -//! // safety: Python does not write to this field -//! value: c"string_sum".as_ptr() as *mut c_void, -//! }, -//! #[cfg(Py_3_15)] -//! PyModuleDef_Slot { -//! slot: Py_mod_doc, -//! // safety: Python does not write to this field -//! value: c"A Python module written in Rust.".as_ptr() as *mut c_void, -//! }, -//! #[cfg(Py_3_15)] -//! PyModuleDef_Slot { -//! slot: Py_mod_methods, -//! value: (&raw mut METHODS).cast(), -//! }, -//! #[cfg(Py_3_12)] //! PyModuleDef_Slot { //! slot: Py_mod_multiple_interpreters, //! value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, @@ -203,6 +181,24 @@ //! }, //! ]; //! +//! #[cfg(Py_3_15)] +//! PyABIInfo_VAR!(ABI_INFO); +//! +//! #[cfg(Py_3_15)] +//! const SLOTS_LEN: usize = +//! 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 4 * (cfg!(Py_3_15) as usize); +//! #[cfg(Py_3_15)] +//! static mut SLOTS: [PySlot; SLOTS_LEN] = [ +//! PySlot_STATIC_DATA(Py_mod_abi, std::ptr::addr_of_mut!(ABI_INFO).cast()), +//! PySlot_STATIC_DATA(Py_mod_name, c"string_sum".as_ptr() as *mut c_void), +//! PySlot_STATIC_DATA(Py_mod_doc, c"A Python module written in Rust.".as_ptr() as *mut c_void), +//! PySlot_STATIC_DATA(Py_mod_methods, (&raw mut METHODS).cast()), +//! PySlot_DATA(Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED), +//! #[cfg(Py_GIL_DISABLED)] +//! PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED), +//! PySlot_END(), +//! ]; +//! //! // The module initialization function //! #[cfg(not(Py_3_15))] //! #[allow(non_snake_case, reason = "must be named `PyInit_`")] @@ -214,7 +210,7 @@ //! #[cfg(Py_3_15)] //! #[allow(non_snake_case, reason = "must be named `PyModExport_`")] //! #[no_mangle] -//! pub unsafe extern "C" fn PyModExport_string_sum() -> *mut PyModuleDef_Slot { +//! pub unsafe extern "C" fn PyModExport_string_sum() -> *mut PySlot { //! (&raw mut SLOTS).cast() //! } //! @@ -439,6 +435,8 @@ pub use self::compile::*; pub use self::complexobject::*; #[cfg(not(Py_LIMITED_API))] pub use self::context::*; +#[cfg(Py_3_13)] +pub use self::critical_section::*; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::*; pub use self::descrobject::*; @@ -480,11 +478,13 @@ pub use self::rangeobject::*; pub use self::refcount::*; pub use self::setobject::*; pub use self::sliceobject::*; +#[cfg(Py_3_15)] +pub use self::slots::*; +pub use self::slots_generated::*; pub use self::structseq::*; pub use self::sysmodule::*; pub use self::traceback::*; pub use self::tupleobject::*; -pub use self::typeslots::*; pub use self::unicodeobject::*; pub use self::warnings::*; pub use self::weakrefobject::*; @@ -504,6 +504,7 @@ mod compile; mod complexobject; #[cfg(not(Py_LIMITED_API))] mod context; +mod critical_section; #[cfg(not(Py_LIMITED_API))] pub(crate) mod datetime; mod descrobject; @@ -570,12 +571,13 @@ mod rangeobject; mod refcount; mod setobject; mod sliceobject; +mod slots; +mod slots_generated; mod structseq; mod sysmodule; mod traceback; // skipped tracemalloc.h mod tupleobject; -mod typeslots; mod unicodeobject; mod warnings; mod weakrefobject; diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index 391c5a8518e..68767493b48 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -1,6 +1,8 @@ use crate::methodobject::PyMethodDef; use crate::object::*; use crate::pyport::Py_ssize_t; +#[cfg(Py_3_15)] +use crate::slots::PySlot; use std::ffi::{c_char, c_int, c_void}; extern_libpython! { @@ -85,33 +87,6 @@ impl Default for PyModuleDef_Slot { } } -pub const Py_mod_create: c_int = 1; -pub const Py_mod_exec: c_int = 2; -#[cfg(Py_3_12)] -pub const Py_mod_multiple_interpreters: c_int = 3; -#[cfg(Py_3_13)] -pub const Py_mod_gil: c_int = 4; -#[cfg(Py_3_15)] -pub const Py_mod_abi: c_int = 5; -#[cfg(Py_3_15)] -pub const Py_mod_name: c_int = 6; -#[cfg(Py_3_15)] -pub const Py_mod_doc: c_int = 7; -#[cfg(Py_3_15)] -pub const Py_mod_state_size: c_int = 8; -#[cfg(Py_3_15)] -pub const Py_mod_methods: c_int = 9; -#[cfg(Py_3_15)] -pub const Py_mod_state_traverse: c_int = 10; -#[cfg(Py_3_15)] -pub const Py_mod_state_clear: c_int = 11; -#[cfg(Py_3_15)] -pub const Py_mod_state_free: c_int = 12; -#[cfg(Py_3_15)] -pub const Py_mod_token: c_int = 13; - -// skipped private _Py_mod_LAST_SLOT - #[cfg(Py_3_12)] #[allow( clippy::zero_ptr, @@ -139,10 +114,7 @@ extern_libpython! { #[cfg(Py_3_15)] extern_libpython! { - pub fn PyModule_FromSlotsAndSpec( - slots: *const PyModuleDef_Slot, - spec: *mut PyObject, - ) -> *mut PyObject; + pub fn PyModule_FromSlotsAndSpec(slots: *const PySlot, spec: *mut PyObject) -> *mut PyObject; pub fn PyModule_Exec(_mod: *mut PyObject) -> c_int; pub fn PyModule_GetStateSize(_mod: *mut PyObject, result: *mut Py_ssize_t) -> c_int; pub fn PyModule_GetToken(module: *mut PyObject, result: *mut *mut c_void) -> c_int; diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 8e712c23dc1..d67d5e39c7c 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -3,6 +3,8 @@ use crate::pyport::{Py_hash_t, Py_ssize_t}; use crate::refcount; #[cfg(Py_GIL_DISABLED)] use crate::PyMutex; +#[cfg(Py_3_15)] +use crate::PySlot; use std::ffi::{c_char, c_int, c_uint, c_ulong, c_void}; use std::mem; #[cfg(Py_GIL_DISABLED)] @@ -275,6 +277,14 @@ pub type hashfunc = unsafe extern "C" fn(*mut PyObject) -> Py_hash_t; pub type richcmpfunc = unsafe extern "C" fn(*mut PyObject, *mut PyObject, c_int) -> *mut PyObject; pub type getiterfunc = unsafe extern "C" fn(*mut PyObject) -> *mut PyObject; pub type iternextfunc = unsafe extern "C" fn(*mut PyObject) -> *mut PyObject; +#[cfg(Py_3_15)] +#[repr(C)] +pub struct _PyObjectIndexPair { + pub object: *mut PyObject, + pub index: Py_ssize_t, +} +#[cfg(Py_3_15)] +pub type _Py_iteritemfunc = unsafe extern "C" fn(*mut PyObject, Py_ssize_t) -> _PyObjectIndexPair; pub type descrgetfunc = unsafe extern "C" fn(*mut PyObject, *mut PyObject, *mut PyObject) -> *mut PyObject; pub type descrsetfunc = unsafe extern "C" fn(*mut PyObject, *mut PyObject, *mut PyObject) -> c_int; @@ -377,6 +387,18 @@ extern_libpython! { #[cfg_attr(PyPy, link_name = "PyPyType_GetTypeDataSize")] pub fn PyType_GetTypeDataSize(cls: *mut PyTypeObject) -> Py_ssize_t; + #[cfg(Py_3_14)] + #[cfg_attr(PyPy, link_name = "PyPyType_GetBaseByToken")] + pub fn PyType_GetBaseByToken( + type_: *mut PyTypeObject, + token: *mut c_void, + result: *mut *mut PyTypeObject, + ) -> c_int; + + #[cfg(Py_3_15)] + #[cfg_attr(PyPy, link_name = "PyPyType_FromSlot")] + pub fn PyType_FromSlots(slots: *mut PySlot) -> *mut PyObject; + #[cfg_attr(PyPy, link_name = "PyPyType_IsSubtype")] pub fn PyType_IsSubtype(a: *mut PyTypeObject, b: *mut PyTypeObject) -> c_int; } diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs new file mode 100644 index 00000000000..469f1f99e56 --- /dev/null +++ b/pyo3-ffi/src/slots.rs @@ -0,0 +1,152 @@ +#[cfg(Py_3_15)] +use crate::Py_ssize_t; +#[cfg(Py_3_15)] +use std::ffi::{c_int, c_void}; + +#[cfg(Py_3_15)] +pub type _Py_funcptr_t = unsafe extern "C" fn(); + +#[derive(Copy, Clone)] +#[repr(C)] +#[cfg(Py_3_15)] +pub union _anon_union_32b { + pub sl_reserved: u32, +} + +#[derive(Copy, Clone)] +#[repr(C)] +#[cfg(Py_3_15)] +pub union _anon_union_64b { + pub sl_ptr: *mut c_void, + pub sl_func: _Py_funcptr_t, + pub sl_size: Py_ssize_t, + pub sl_int64: i64, + pub sl_uint64: u64, +} + +#[derive(Copy, Clone)] +#[repr(C)] +#[cfg(Py_3_15)] +pub struct PySlot { + pub sl_id: u16, + pub sl_flags: u16, + pub anon1: _anon_union_32b, + pub anon2: _anon_union_64b, +} + +#[cfg(Py_3_15)] +pub const PySlot_OPTIONAL: u16 = 0x01; +#[cfg(Py_3_15)] +pub const PySlot_STATIC: u16 = 0x02; +#[cfg(Py_3_15)] +pub const PySlot_INTPTR: u16 = 0x04; +#[cfg(Py_3_15)] +pub const Py_slot_invalid: u16 = 0xffff; + +#[cfg(Py_3_15)] +const fn safe_cast_c_int_to_u16(val: i32) -> u16 { + if val >= 0 && val <= u16::MAX as c_int { + val as u16 + } else { + panic!("Slot ID out of range for u16!"); + } +} + +#[cfg(Py_3_15)] +pub const fn PySlot_DATA(NAME: c_int, VALUE: *mut c_void) -> PySlot { + PySlot { + sl_id: safe_cast_c_int_to_u16(NAME), + sl_flags: PySlot_INTPTR, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_ptr: VALUE }, + } +} + +/// # Safety +/// +/// `$fn_ty$` must be the action found-pointer type of `$value` and must be a +/// valid signature for slot `$name`. A mismatch between the slot and expected signature +/// from CPython's point of view is UB. +#[macro_export] +#[cfg(Py_3_15)] +macro_rules! PySlot_FUNC { + ($name:expr, $fn_ty:ty, $value:expr) => { + $crate::PySlot { + sl_id: if $name >= 0 && $name <= u16::MAX as c_int { + $name as u16 + } else { + panic!("Slot ID out of range for u16!"); + }, + sl_flags: 0, + anon1: $crate::_anon_union_32b { sl_reserved: 0 }, + anon2: $crate::_anon_union_64b { + sl_func: unsafe { ::std::mem::transmute::<$fn_ty, $crate::_Py_funcptr_t>($value) }, + }, + } + }; +} + +#[cfg(Py_3_15)] +pub const fn PySlot_SIZE(NAME: c_int, VALUE: Py_ssize_t) -> PySlot { + PySlot { + sl_id: safe_cast_c_int_to_u16(NAME), + sl_flags: 0, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_size: VALUE }, + } +} + +#[cfg(Py_3_15)] +pub const fn PySlot_INT64(NAME: c_int, VALUE: i64) -> PySlot { + PySlot { + sl_id: safe_cast_c_int_to_u16(NAME), + sl_flags: 0, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_int64: VALUE }, + } +} + +#[cfg(Py_3_15)] +pub const fn PySlot_UINT64(NAME: c_int, VALUE: u64) -> PySlot { + PySlot { + sl_id: safe_cast_c_int_to_u16(NAME), + sl_flags: 0, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_uint64: VALUE }, + } +} + +#[cfg(Py_3_15)] +pub const fn PySlot_STATIC_DATA(NAME: c_int, VALUE: *mut c_void) -> PySlot { + PySlot { + sl_id: safe_cast_c_int_to_u16(NAME), + sl_flags: PySlot_STATIC, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_ptr: VALUE }, + } +} + +#[cfg(Py_3_15)] +pub const fn PySlot_PTR(NAME: c_int, VALUE: *mut c_void) -> PySlot { + PySlot { + sl_id: safe_cast_c_int_to_u16(NAME), + sl_flags: PySlot_INTPTR, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_ptr: VALUE }, + } +} + +#[cfg(Py_3_15)] +pub const fn PySlot_PTR_STATIC(NAME: c_int, VALUE: *mut c_void) -> PySlot { + PySlot { + sl_id: safe_cast_c_int_to_u16(NAME), + sl_flags: PySlot_INTPTR | PySlot_STATIC, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_ptr: VALUE }, + } +} + +#[cfg(Py_3_15)] +pub const fn PySlot_END() -> PySlot { + unsafe { std::mem::zeroed() } +} diff --git a/pyo3-ffi/src/slots_generated.rs b/pyo3-ffi/src/slots_generated.rs new file mode 100644 index 00000000000..935a6bb3b23 --- /dev/null +++ b/pyo3-ffi/src/slots_generated.rs @@ -0,0 +1,142 @@ +use std::ffi::c_int; + +#[allow(unused_variables)] +const fn _Py_SLOT_COMPAT_VALUE(OLD: c_int, NEW: c_int) -> c_int { + #[cfg(Py_3_15)] + { + NEW + } + + #[cfg(not(Py_3_15))] + { + OLD + } +} + +#[cfg(Py_3_15)] +pub const Py_slot_end: u16 = 0; +pub const Py_mp_subscript: c_int = 5; +pub const Py_nb_absolute: c_int = 6; +pub const Py_nb_add: c_int = 7; +pub const Py_nb_and: c_int = 8; +pub const Py_nb_bool: c_int = 9; +pub const Py_nb_divmod: c_int = 10; +pub const Py_nb_float: c_int = 11; +pub const Py_nb_floor_divide: c_int = 12; +pub const Py_nb_index: c_int = 13; +pub const Py_nb_inplace_add: c_int = 14; +pub const Py_nb_inplace_and: c_int = 15; +pub const Py_nb_inplace_floor_divide: c_int = 16; +pub const Py_nb_inplace_lshift: c_int = 17; +pub const Py_nb_inplace_multiply: c_int = 18; +pub const Py_nb_inplace_or: c_int = 19; +pub const Py_nb_inplace_power: c_int = 20; +pub const Py_nb_inplace_remainder: c_int = 21; +pub const Py_nb_inplace_rshift: c_int = 22; +pub const Py_nb_inplace_subtract: c_int = 23; +pub const Py_nb_inplace_true_divide: c_int = 24; +pub const Py_nb_inplace_xor: c_int = 25; +pub const Py_nb_int: c_int = 26; +pub const Py_nb_invert: c_int = 27; +pub const Py_nb_lshift: c_int = 28; +pub const Py_nb_multiply: c_int = 29; +pub const Py_nb_negative: c_int = 30; +pub const Py_nb_or: c_int = 31; +pub const Py_nb_positive: c_int = 32; +pub const Py_nb_power: c_int = 33; +pub const Py_nb_remainder: c_int = 34; +pub const Py_nb_rshift: c_int = 35; +pub const Py_nb_subtract: c_int = 36; +pub const Py_nb_true_divide: c_int = 37; +pub const Py_nb_xor: c_int = 38; +pub const Py_sq_ass_item: c_int = 39; +pub const Py_sq_concat: c_int = 40; +pub const Py_sq_contains: c_int = 41; +pub const Py_sq_inplace_concat: c_int = 42; +pub const Py_sq_inplace_repeat: c_int = 43; +pub const Py_sq_item: c_int = 44; +pub const Py_sq_length: c_int = 45; +pub const Py_sq_repeat: c_int = 46; +pub const Py_tp_alloc: c_int = 47; +pub const Py_tp_base: c_int = 48; +pub const Py_tp_bases: c_int = 49; +pub const Py_tp_call: c_int = 50; +pub const Py_tp_clear: c_int = 51; +pub const Py_tp_dealloc: c_int = 52; +pub const Py_tp_del: c_int = 53; +pub const Py_tp_descr_get: c_int = 54; +pub const Py_tp_descr_set: c_int = 55; +pub const Py_tp_doc: c_int = 56; +pub const Py_tp_getattr: c_int = 57; +pub const Py_tp_getattro: c_int = 58; +pub const Py_tp_hash: c_int = 59; +pub const Py_tp_init: c_int = 60; +pub const Py_tp_is_gc: c_int = 61; +pub const Py_tp_iter: c_int = 62; +pub const Py_tp_iternext: c_int = 63; +pub const Py_tp_methods: c_int = 64; +pub const Py_tp_new: c_int = 65; +pub const Py_tp_repr: c_int = 66; +pub const Py_tp_richcompare: c_int = 67; +pub const Py_tp_setattr: c_int = 68; +pub const Py_tp_setattro: c_int = 69; +pub const Py_tp_str: c_int = 70; +pub const Py_tp_traverse: c_int = 71; +pub const Py_tp_members: c_int = 72; +pub const Py_tp_getset: c_int = 73; +pub const Py_tp_free: c_int = 74; +pub const Py_nb_matrix_multiply: c_int = 75; +pub const Py_nb_inplace_matrix_multiply: c_int = 76; +pub const Py_am_await: c_int = 77; +pub const Py_am_aiter: c_int = 78; +pub const Py_am_anext: c_int = 79; +pub const Py_tp_finalize: c_int = 80; +pub const Py_am_send: c_int = 81; +pub const Py_tp_vectorcall: c_int = 82; +pub const Py_tp_token: c_int = 83; +pub const Py_mod_create: c_int = _Py_SLOT_COMPAT_VALUE(1, 84); +pub const Py_mod_exec: c_int = _Py_SLOT_COMPAT_VALUE(2, 85); +pub const Py_mod_multiple_interpreters: c_int = _Py_SLOT_COMPAT_VALUE(3, 86); +pub const Py_mod_gil: c_int = _Py_SLOT_COMPAT_VALUE(4, 87); +pub const Py_bf_getbuffer: c_int = _Py_SLOT_COMPAT_VALUE(1, 88); +pub const Py_bf_releasebuffer: c_int = _Py_SLOT_COMPAT_VALUE(2, 89); +pub const Py_mp_ass_subscript: c_int = _Py_SLOT_COMPAT_VALUE(3, 90); +pub const Py_mp_length: c_int = _Py_SLOT_COMPAT_VALUE(4, 91); +#[cfg(Py_3_15)] +pub const Py_slot_subslots: c_int = 92; +#[cfg(Py_3_15)] +pub const Py_tp_slots: c_int = 93; +#[cfg(Py_3_15)] +pub const Py_mod_slots: c_int = 94; +#[cfg(Py_3_15)] +pub const Py_tp_name: c_int = 95; +#[cfg(Py_3_15)] +pub const Py_tp_basicsize: c_int = 96; +#[cfg(Py_3_15)] +pub const Py_tp_extra_basicsize: c_int = 97; +#[cfg(Py_3_15)] +pub const Py_tp_itemsize: c_int = 98; +#[cfg(Py_3_15)] +pub const Py_tp_flags: c_int = 99; +#[cfg(Py_3_15)] +pub const Py_mod_name: c_int = 100; +#[cfg(Py_3_15)] +pub const Py_mod_doc: c_int = 101; +#[cfg(Py_3_15)] +pub const Py_mod_state_size: c_int = 102; +#[cfg(Py_3_15)] +pub const Py_mod_methods: c_int = 103; +#[cfg(Py_3_15)] +pub const Py_mod_state_traverse: c_int = 104; +#[cfg(Py_3_15)] +pub const Py_mod_state_clear: c_int = 105; +#[cfg(Py_3_15)] +pub const Py_mod_state_free: c_int = 106; +#[cfg(Py_3_15)] +pub const Py_tp_metaclass: c_int = 107; +#[cfg(Py_3_15)] +pub const Py_tp_module: c_int = 108; +#[cfg(Py_3_15)] +pub const Py_mod_abi: c_int = 109; +#[cfg(Py_3_15)] +pub const Py_mod_token: c_int = 110; diff --git a/pyo3-ffi/src/typeslots.rs b/pyo3-ffi/src/typeslots.rs index fd0c3b77a74..935a6bb3b23 100644 --- a/pyo3-ffi/src/typeslots.rs +++ b/pyo3-ffi/src/typeslots.rs @@ -1,9 +1,20 @@ use std::ffi::c_int; -pub const Py_bf_getbuffer: c_int = 1; -pub const Py_bf_releasebuffer: c_int = 2; -pub const Py_mp_ass_subscript: c_int = 3; -pub const Py_mp_length: c_int = 4; +#[allow(unused_variables)] +const fn _Py_SLOT_COMPAT_VALUE(OLD: c_int, NEW: c_int) -> c_int { + #[cfg(Py_3_15)] + { + NEW + } + + #[cfg(not(Py_3_15))] + { + OLD + } +} + +#[cfg(Py_3_15)] +pub const Py_slot_end: u16 = 0; pub const Py_mp_subscript: c_int = 5; pub const Py_nb_absolute: c_int = 6; pub const Py_nb_add: c_int = 7; @@ -80,3 +91,52 @@ pub const Py_am_await: c_int = 77; pub const Py_am_aiter: c_int = 78; pub const Py_am_anext: c_int = 79; pub const Py_tp_finalize: c_int = 80; +pub const Py_am_send: c_int = 81; +pub const Py_tp_vectorcall: c_int = 82; +pub const Py_tp_token: c_int = 83; +pub const Py_mod_create: c_int = _Py_SLOT_COMPAT_VALUE(1, 84); +pub const Py_mod_exec: c_int = _Py_SLOT_COMPAT_VALUE(2, 85); +pub const Py_mod_multiple_interpreters: c_int = _Py_SLOT_COMPAT_VALUE(3, 86); +pub const Py_mod_gil: c_int = _Py_SLOT_COMPAT_VALUE(4, 87); +pub const Py_bf_getbuffer: c_int = _Py_SLOT_COMPAT_VALUE(1, 88); +pub const Py_bf_releasebuffer: c_int = _Py_SLOT_COMPAT_VALUE(2, 89); +pub const Py_mp_ass_subscript: c_int = _Py_SLOT_COMPAT_VALUE(3, 90); +pub const Py_mp_length: c_int = _Py_SLOT_COMPAT_VALUE(4, 91); +#[cfg(Py_3_15)] +pub const Py_slot_subslots: c_int = 92; +#[cfg(Py_3_15)] +pub const Py_tp_slots: c_int = 93; +#[cfg(Py_3_15)] +pub const Py_mod_slots: c_int = 94; +#[cfg(Py_3_15)] +pub const Py_tp_name: c_int = 95; +#[cfg(Py_3_15)] +pub const Py_tp_basicsize: c_int = 96; +#[cfg(Py_3_15)] +pub const Py_tp_extra_basicsize: c_int = 97; +#[cfg(Py_3_15)] +pub const Py_tp_itemsize: c_int = 98; +#[cfg(Py_3_15)] +pub const Py_tp_flags: c_int = 99; +#[cfg(Py_3_15)] +pub const Py_mod_name: c_int = 100; +#[cfg(Py_3_15)] +pub const Py_mod_doc: c_int = 101; +#[cfg(Py_3_15)] +pub const Py_mod_state_size: c_int = 102; +#[cfg(Py_3_15)] +pub const Py_mod_methods: c_int = 103; +#[cfg(Py_3_15)] +pub const Py_mod_state_traverse: c_int = 104; +#[cfg(Py_3_15)] +pub const Py_mod_state_clear: c_int = 105; +#[cfg(Py_3_15)] +pub const Py_mod_state_free: c_int = 106; +#[cfg(Py_3_15)] +pub const Py_tp_metaclass: c_int = 107; +#[cfg(Py_3_15)] +pub const Py_tp_module: c_int = 108; +#[cfg(Py_3_15)] +pub const Py_mod_abi: c_int = 109; +#[cfg(Py_3_15)] +pub const Py_mod_token: c_int = 110; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 6f766523711..cf1c0f33f77 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -569,13 +569,10 @@ fn module_initialization( _PYO3_DEF.init_multi_phase() } - /// This autogenerated function is called by the python interpreter when importing - /// the module on Python 3.15 and newer. - #[doc(hidden)] - #[export_name = #pymodexport_symbol] - pub unsafe extern "C" fn __pyo3_export() -> *mut #pyo3_path::ffi::PyModuleDef_Slot { - _PYO3_DEF.get_slots() - } + // Defines the `PyModExport_` entry point used by Python 3.15 and newer. + // Expands to nothing on older Python versions, so this stays version-agnostic + // and `ffi::PySlot` is only required to exist on 3.15+. + #pyo3_path::__pyo3_pymodexport!(#pymodexport_symbol, _PYO3_DEF); }); } Ok(result) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index d0a32881b6c..1a1bdf8e5a1 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -1,5 +1,4 @@ //! Implementation details of `#[pymodule]` which need to be accessible from proc-macro generated code. - use std::{ cell::UnsafeCell, ffi::CStr, @@ -49,6 +48,7 @@ pub struct ModuleDef { name: &'static CStr, #[cfg(Py_3_15)] doc: &'static CStr, + #[cfg(Py_3_15)] slots: &'static PyModuleSlots, /// Interpreter ID where module was initialized (not applicable on PyPy). #[cfg(all( @@ -101,6 +101,7 @@ impl ModuleDef { name, #[cfg(Py_3_15)] doc, + #[cfg(Py_3_15)] slots, // -1 is never expected to be a valid interpreter ID #[cfg(all( @@ -227,11 +228,37 @@ impl ModuleDef { .map(|py_module| py_module.clone_ref(py)) } } - pub fn get_slots(&'static self) -> *mut ffi::PyModuleDef_Slot { - self.slots.0.get() as *mut ffi::PyModuleDef_Slot + #[cfg(Py_3_15)] + pub fn get_slots(&'static self) -> *mut ffi::PySlot { + self.slots.0.get() as *mut ffi::PySlot } } +/// Defines the `PyModExport_` entry point used by Python 3.15 and newer. +/// +/// This is wrapped in a `macro_rules!` so the proc-macro backend can emit a single +/// version-agnostic invocation; the body only expands on Python 3.15+, where +/// `ffi::PySlot` is defined. +#[cfg(Py_3_15)] +#[doc(hidden)] +#[macro_export] +macro_rules! __pyo3_pymodexport { + ($symbol:literal, $def:path) => { + #[doc(hidden)] + #[export_name = $symbol] + pub unsafe extern "C" fn __pyo3_export() -> *mut $crate::ffi::PySlot { + $def.get_slots() + } + }; +} + +#[cfg(not(Py_3_15))] +#[doc(hidden)] +#[macro_export] +macro_rules! __pyo3_pymodexport { + ($symbol:literal, $def:path) => {}; +} + /// Type of the exec slot used to initialise module contents pub type ModuleExecSlot = unsafe extern "C" fn(*mut ffi::PyObject) -> c_int; @@ -249,7 +276,10 @@ const MAX_SLOTS_WITH_TRAILING_NULL: usize = MAX_SLOTS + 1; /// actual slots pushed due to the need to have a zeroed element on the end. pub struct PyModuleSlotsBuilder { // values (initially all zeroed) + #[cfg(not(Py_3_15))] values: [ffi::PyModuleDef_Slot; MAX_SLOTS_WITH_TRAILING_NULL], + #[cfg(Py_3_15)] + values: [ffi::PySlot; MAX_SLOTS_WITH_TRAILING_NULL], // current length len: usize, } @@ -270,11 +300,18 @@ impl PyModuleSlotsBuilder { } pub const fn with_mod_exec(self, exec: ModuleExecSlot) -> Self { - self.push(ffi::Py_mod_exec, exec as *mut c_void) + #[cfg(not(Py_3_15))] + { + self.push(ffi::Py_mod_exec, exec as *mut c_void) + } + #[cfg(Py_3_15)] + { + self.push_value(ffi::PySlot_FUNC!(ffi::Py_mod_exec, ModuleExecSlot, exec)) + } } pub const fn with_gil_used(self, gil_used: bool) -> Self { - #[cfg(Py_3_13)] + #[cfg(all(Py_3_13, not(Py_3_15)))] { self.push( ffi::Py_mod_gil, @@ -286,6 +323,18 @@ impl PyModuleSlotsBuilder { ) } + #[cfg(Py_3_15)] + { + self.push_value(ffi::PySlot_DATA( + ffi::Py_mod_gil, + if gil_used { + ffi::Py_MOD_GIL_USED + } else { + ffi::Py_MOD_GIL_NOT_USED + }, + )) + } + #[cfg(not(Py_3_13))] { // Silence unused variable warning @@ -297,7 +346,10 @@ impl PyModuleSlotsBuilder { pub const fn with_name(self, name: &'static CStr) -> Self { #[cfg(Py_3_15)] { - self.push(ffi::Py_mod_name, name.as_ptr() as *mut c_void) + self.push_value(ffi::PySlot_STATIC_DATA( + ffi::Py_mod_name, + name.as_ptr() as *mut c_void, + )) } #[cfg(not(Py_3_15))] @@ -312,7 +364,10 @@ impl PyModuleSlotsBuilder { #[cfg(Py_3_15)] { ffi::PyABIInfo_VAR!(ABI_INFO); - self.push(ffi::Py_mod_abi, std::ptr::addr_of_mut!(ABI_INFO).cast()) + self.push_value(ffi::PySlot_STATIC_DATA( + ffi::Py_mod_abi, + std::ptr::addr_of_mut!(ABI_INFO).cast(), + )) } #[cfg(not(Py_3_15))] @@ -324,7 +379,10 @@ impl PyModuleSlotsBuilder { pub const fn with_doc(self, doc: &'static CStr) -> Self { #[cfg(Py_3_15)] { - self.push(ffi::Py_mod_doc, doc.as_ptr() as *mut c_void) + self.push_value(ffi::PySlot_STATIC_DATA( + ffi::Py_mod_doc, + doc.as_ptr() as *mut c_void, + )) } #[cfg(not(Py_3_15))] @@ -339,6 +397,7 @@ impl PyModuleSlotsBuilder { PyModuleSlots(UnsafeCell::new(self.values)) } + #[cfg(not(Py_3_15))] const fn push(mut self, slot: c_int, value: *mut c_void) -> Self { // Required to guarantee there's still a zeroed element // at the end @@ -350,10 +409,24 @@ impl PyModuleSlotsBuilder { self.len += 1; self } + + #[cfg(Py_3_15)] + const fn push_value(mut self, value: ffi::PySlot) -> Self { + assert!( + self.len < MAX_SLOTS, + "Cannot add more than MAX_SLOTS slots to a PyModuleSlots", + ); + self.values[self.len] = value; + self.len += 1; + self + } } /// Wrapper to safely store module slots, to be used in a `ModuleDef`. +#[cfg(not(Py_3_15))] pub struct PyModuleSlots(UnsafeCell<[ffi::PyModuleDef_Slot; MAX_SLOTS_WITH_TRAILING_NULL]>); +#[cfg(Py_3_15)] +pub struct PyModuleSlots(UnsafeCell<[ffi::PySlot; MAX_SLOTS_WITH_TRAILING_NULL]>); // It might be possible to avoid this with SyncUnsafeCell in the future // @@ -508,7 +581,6 @@ mod tests { assert_eq!(module_def.name, NAME); assert_eq!(module_def.doc, DOC); } - assert_eq!(module_def.slots.0.get(), SLOTS.0.get()); } #[test] @@ -520,9 +592,30 @@ mod tests { .with_doc(c"some doc") .with_gil_used(false) .with_abi_info(); + let second_last = builder.values[builder.len - 1]; + let last = builder.values[builder.len]; - assert!(builder.values[builder.len] == unsafe { std::mem::zeroed() }); - assert!(builder.values[builder.len - 1] != unsafe { std::mem::zeroed() }); + #[cfg(Py_3_15)] + { + let zeroed = unsafe { std::mem::zeroed() }; + fn raw_bytes(inst: &ffi::PySlot) -> &[u8] { + unsafe { + std::slice::from_raw_parts( + inst as *const ffi::PySlot as *const u8, + std::mem::size_of::(), + ) + } + } + let zeroed_bytes = raw_bytes(&zeroed); + assert_eq!(raw_bytes(&last), zeroed_bytes); + assert_ne!(raw_bytes(&second_last), zeroed_bytes); + } + #[cfg(not(Py_3_15))] + { + let zeroed = ffi::PyModuleDef_Slot::default(); + assert!(last == zeroed); + assert!(second_last != zeroed); + } assert!(builder.len == MAX_SLOTS); let result = std::panic::catch_unwind(|| builder.with_mod_exec(module_exec).build()); From a3c0ac8795bb775373be079b838cbc610deacaa5 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 9 May 2026 10:16:20 -0600 Subject: [PATCH 02/15] Apply suggestions from code review Co-authored-by: David Hewitt --- pyo3-ffi/src/slots_generated.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pyo3-ffi/src/slots_generated.rs b/pyo3-ffi/src/slots_generated.rs index 935a6bb3b23..72d63bf5b17 100644 --- a/pyo3-ffi/src/slots_generated.rs +++ b/pyo3-ffi/src/slots_generated.rs @@ -2,15 +2,7 @@ use std::ffi::c_int; #[allow(unused_variables)] const fn _Py_SLOT_COMPAT_VALUE(OLD: c_int, NEW: c_int) -> c_int { - #[cfg(Py_3_15)] - { - NEW - } - - #[cfg(not(Py_3_15))] - { - OLD - } + if cfg!(Py_3_15) { NEW } else { OLD } } #[cfg(Py_3_15)] From f94aa5b0fddd48e5cb96b359a7f79759b2d6b9c5 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 9 May 2026 10:18:01 -0600 Subject: [PATCH 03/15] apply trivial suggestions from david --- pyo3-ffi/src/slots_generated.rs | 6 +++++- pyo3-ffi/src/typeslots.rs | 8 ++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyo3-ffi/src/slots_generated.rs b/pyo3-ffi/src/slots_generated.rs index 72d63bf5b17..1b2df5cf3c0 100644 --- a/pyo3-ffi/src/slots_generated.rs +++ b/pyo3-ffi/src/slots_generated.rs @@ -2,7 +2,11 @@ use std::ffi::c_int; #[allow(unused_variables)] const fn _Py_SLOT_COMPAT_VALUE(OLD: c_int, NEW: c_int) -> c_int { - if cfg!(Py_3_15) { NEW } else { OLD } + if cfg!(Py_3_15) { + NEW + } else { + OLD + } } #[cfg(Py_3_15)] diff --git a/pyo3-ffi/src/typeslots.rs b/pyo3-ffi/src/typeslots.rs index 935a6bb3b23..1b2df5cf3c0 100644 --- a/pyo3-ffi/src/typeslots.rs +++ b/pyo3-ffi/src/typeslots.rs @@ -2,13 +2,9 @@ use std::ffi::c_int; #[allow(unused_variables)] const fn _Py_SLOT_COMPAT_VALUE(OLD: c_int, NEW: c_int) -> c_int { - #[cfg(Py_3_15)] - { + if cfg!(Py_3_15) { NEW - } - - #[cfg(not(Py_3_15))] - { + } else { OLD } } From 79f71d9b323285d7c41841ecf0c56c9a5b64b0b9 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 9 May 2026 10:47:13 -0600 Subject: [PATCH 04/15] apply remaining suggestions --- pyo3-ffi/examples/sequential/src/module.rs | 12 ++---- pyo3-ffi/src/slots.rs | 46 ++++++++++------------ src/impl_/pymodule.rs | 4 +- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index f66129fc95a..b71d8b3fbcc 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -32,14 +32,10 @@ pub static mut SEQUENTIAL_SLOTS: [PySlot; SEQUENTIAL_SLOTS_LEN] = [ Py_mod_state_size, mem::size_of::() as Py_ssize_t, ), - PySlot_FUNC!(Py_mod_state_traverse, traverseproc, sequential_traverse), - PySlot_FUNC!(Py_mod_state_clear, inquiry, sequential_clear), - PySlot_FUNC!(Py_mod_state_free, freefunc, sequential_free), - PySlot_FUNC!( - Py_mod_exec, - unsafe extern "C" fn(*mut PyObject) -> c_int, - sequential_exec - ), + PySlot_FUNC(Py_mod_state_traverse, sequential_traverse as *mut c_void), + PySlot_FUNC(Py_mod_state_clear, sequential_clear as *mut c_void), + PySlot_FUNC(Py_mod_state_free, sequential_free as *mut c_void), + PySlot_FUNC(Py_mod_exec, sequential_exec as *mut c_void), PySlot_DATA( Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index 469f1f99e56..e4cb5ae9485 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -18,7 +18,7 @@ pub union _anon_union_32b { #[cfg(Py_3_15)] pub union _anon_union_64b { pub sl_ptr: *mut c_void, - pub sl_func: _Py_funcptr_t, + pub sl_func: Option<_Py_funcptr_t>, pub sl_size: Py_ssize_t, pub sl_int64: i64, pub sl_uint64: u64, @@ -43,12 +43,18 @@ pub const PySlot_INTPTR: u16 = 0x04; #[cfg(Py_3_15)] pub const Py_slot_invalid: u16 = 0xffff; -#[cfg(Py_3_15)] -const fn safe_cast_c_int_to_u16(val: i32) -> u16 { +// The slot IDs are integer constants in the C headers and don't have +// explicit types. Legacy slot IDs are c integers but PySlot IDs are +// u16s. We use this to convert from c_int to u16 safely, without using +// `as`. The panic should only be possible if there is a programming +// error somewhere leading to a slot ID outside the range of u16. If +// TryInto becomes possible in const contexts we could use that instead. +#[cfg(Py_3_15)] +const fn safe_cast_c_int_to_u16(val: c_int) -> u16 { if val >= 0 && val <= u16::MAX as c_int { val as u16 } else { - panic!("Slot ID out of range for u16!"); + panic!("Slot ID out of range for u16"); } } @@ -62,28 +68,16 @@ pub const fn PySlot_DATA(NAME: c_int, VALUE: *mut c_void) -> PySlot { } } -/// # Safety -/// -/// `$fn_ty$` must be the action found-pointer type of `$value` and must be a -/// valid signature for slot `$name`. A mismatch between the slot and expected signature -/// from CPython's point of view is UB. -#[macro_export] -#[cfg(Py_3_15)] -macro_rules! PySlot_FUNC { - ($name:expr, $fn_ty:ty, $value:expr) => { - $crate::PySlot { - sl_id: if $name >= 0 && $name <= u16::MAX as c_int { - $name as u16 - } else { - panic!("Slot ID out of range for u16!"); - }, - sl_flags: 0, - anon1: $crate::_anon_union_32b { sl_reserved: 0 }, - anon2: $crate::_anon_union_64b { - sl_func: unsafe { ::std::mem::transmute::<$fn_ty, $crate::_Py_funcptr_t>($value) }, - }, - } - }; +#[cfg(Py_3_15)] +pub const fn PySlot_FUNC(NAME: c_int, VALUE: *mut c_void) -> PySlot { + PySlot { + sl_id: safe_cast_c_int_to_u16(NAME), + sl_flags: 0, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { + sl_func: Some(unsafe { std::mem::transmute::<*mut c_void, _Py_funcptr_t>(VALUE) }), + }, + } } #[cfg(Py_3_15)] diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 1a1bdf8e5a1..18f3e327f09 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -306,7 +306,7 @@ impl PyModuleSlotsBuilder { } #[cfg(Py_3_15)] { - self.push_value(ffi::PySlot_FUNC!(ffi::Py_mod_exec, ModuleExecSlot, exec)) + self.push_value(ffi::PySlot_FUNC(ffi::Py_mod_exec, exec as *mut c_void)) } } @@ -366,7 +366,7 @@ impl PyModuleSlotsBuilder { ffi::PyABIInfo_VAR!(ABI_INFO); self.push_value(ffi::PySlot_STATIC_DATA( ffi::Py_mod_abi, - std::ptr::addr_of_mut!(ABI_INFO).cast(), + (&raw mut ABI_INFO).cast(), )) } From cab5e0065a30a5ea157c58d579221c256bf14727 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 9 May 2026 11:18:01 -0600 Subject: [PATCH 05/15] mark PySlot_FUNC as unsafe to fix clippy --- pyo3-ffi/examples/sequential/src/module.rs | 9 +++++---- pyo3-ffi/src/slots.rs | 4 +++- src/impl_/pymodule.rs | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index b71d8b3fbcc..a98876e09cb 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -32,10 +32,11 @@ pub static mut SEQUENTIAL_SLOTS: [PySlot; SEQUENTIAL_SLOTS_LEN] = [ Py_mod_state_size, mem::size_of::() as Py_ssize_t, ), - PySlot_FUNC(Py_mod_state_traverse, sequential_traverse as *mut c_void), - PySlot_FUNC(Py_mod_state_clear, sequential_clear as *mut c_void), - PySlot_FUNC(Py_mod_state_free, sequential_free as *mut c_void), - PySlot_FUNC(Py_mod_exec, sequential_exec as *mut c_void), + // safety: all function pointers are the correct type for the slot and are non-null + unsafe { PySlot_FUNC(Py_mod_state_traverse, sequential_traverse as *mut c_void) }, + unsafe { PySlot_FUNC(Py_mod_state_clear, sequential_clear as *mut c_void) }, + unsafe { PySlot_FUNC(Py_mod_state_free, sequential_free as *mut c_void) }, + unsafe { PySlot_FUNC(Py_mod_exec, sequential_exec as *mut c_void) }, PySlot_DATA( Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index e4cb5ae9485..84cb0df91df 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -68,8 +68,10 @@ pub const fn PySlot_DATA(NAME: c_int, VALUE: *mut c_void) -> PySlot { } } +/// Safety: VALUE must be the correct function pointer type for the +/// corresponding slot ID. Neither CPython or pyo3-ffi do type checking here. #[cfg(Py_3_15)] -pub const fn PySlot_FUNC(NAME: c_int, VALUE: *mut c_void) -> PySlot { +pub const unsafe fn PySlot_FUNC(NAME: c_int, VALUE: *mut c_void) -> PySlot { PySlot { sl_id: safe_cast_c_int_to_u16(NAME), sl_flags: 0, diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 18f3e327f09..be743dbb866 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -306,7 +306,8 @@ impl PyModuleSlotsBuilder { } #[cfg(Py_3_15)] { - self.push_value(ffi::PySlot_FUNC(ffi::Py_mod_exec, exec as *mut c_void)) + // safety: exec cannot be NULL and is the correct function pointer type + self.push_value(unsafe { ffi::PySlot_FUNC(ffi::Py_mod_exec, exec as *mut c_void) }) } } From 75a3babc5b5209b79967d202a09b106c7e516e3c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 9 May 2026 11:21:47 -0600 Subject: [PATCH 06/15] do it without unsafe --- pyo3-ffi/examples/sequential/src/module.rs | 9 ++++----- pyo3-ffi/src/slots.rs | 8 ++++---- src/impl_/pymodule.rs | 6 ++++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index a98876e09cb..b71d8b3fbcc 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -32,11 +32,10 @@ pub static mut SEQUENTIAL_SLOTS: [PySlot; SEQUENTIAL_SLOTS_LEN] = [ Py_mod_state_size, mem::size_of::() as Py_ssize_t, ), - // safety: all function pointers are the correct type for the slot and are non-null - unsafe { PySlot_FUNC(Py_mod_state_traverse, sequential_traverse as *mut c_void) }, - unsafe { PySlot_FUNC(Py_mod_state_clear, sequential_clear as *mut c_void) }, - unsafe { PySlot_FUNC(Py_mod_state_free, sequential_free as *mut c_void) }, - unsafe { PySlot_FUNC(Py_mod_exec, sequential_exec as *mut c_void) }, + PySlot_FUNC(Py_mod_state_traverse, sequential_traverse as *mut c_void), + PySlot_FUNC(Py_mod_state_clear, sequential_clear as *mut c_void), + PySlot_FUNC(Py_mod_state_free, sequential_free as *mut c_void), + PySlot_FUNC(Py_mod_exec, sequential_exec as *mut c_void), PySlot_DATA( Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index 84cb0df91df..ecfd1f4cf3b 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -68,16 +68,16 @@ pub const fn PySlot_DATA(NAME: c_int, VALUE: *mut c_void) -> PySlot { } } -/// Safety: VALUE must be the correct function pointer type for the -/// corresponding slot ID. Neither CPython or pyo3-ffi do type checking here. #[cfg(Py_3_15)] -pub const unsafe fn PySlot_FUNC(NAME: c_int, VALUE: *mut c_void) -> PySlot { +pub const fn PySlot_FUNC(NAME: c_int, VALUE: Option<*mut c_void>) -> PySlot { PySlot { sl_id: safe_cast_c_int_to_u16(NAME), sl_flags: 0, anon1: _anon_union_32b { sl_reserved: 0 }, anon2: _anon_union_64b { - sl_func: Some(unsafe { std::mem::transmute::<*mut c_void, _Py_funcptr_t>(VALUE) }), + sl_func: Some(unsafe { + std::mem::transmute::<*mut c_void, _Py_funcptr_t>(VALUE.unwrap()) + }), }, } } diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index be743dbb866..a4c83869008 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -306,8 +306,10 @@ impl PyModuleSlotsBuilder { } #[cfg(Py_3_15)] { - // safety: exec cannot be NULL and is the correct function pointer type - self.push_value(unsafe { ffi::PySlot_FUNC(ffi::Py_mod_exec, exec as *mut c_void) }) + self.push_value(ffi::PySlot_FUNC( + ffi::Py_mod_exec, + Some(exec as *mut c_void), + )) } } From 97a3e9607f67139f9b8b450d9ff1991e604e7c6e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 9 May 2026 12:20:52 -0600 Subject: [PATCH 07/15] fix sequential example for Python 3.15 --- pyo3-ffi/examples/sequential/src/module.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index b71d8b3fbcc..abb08f25f9d 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -32,10 +32,13 @@ pub static mut SEQUENTIAL_SLOTS: [PySlot; SEQUENTIAL_SLOTS_LEN] = [ Py_mod_state_size, mem::size_of::() as Py_ssize_t, ), - PySlot_FUNC(Py_mod_state_traverse, sequential_traverse as *mut c_void), - PySlot_FUNC(Py_mod_state_clear, sequential_clear as *mut c_void), - PySlot_FUNC(Py_mod_state_free, sequential_free as *mut c_void), - PySlot_FUNC(Py_mod_exec, sequential_exec as *mut c_void), + PySlot_FUNC( + Py_mod_state_traverse, + Some(sequential_traverse as *mut c_void), + ), + PySlot_FUNC(Py_mod_state_clear, Some(sequential_clear as *mut c_void)), + PySlot_FUNC(Py_mod_state_free, Some(sequential_free as *mut c_void)), + PySlot_FUNC(Py_mod_exec, Some(sequential_exec as *mut c_void)), PySlot_DATA( Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, From 12e51abcf794b0805192670710b04b20458a1c99 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 9 May 2026 12:24:54 -0600 Subject: [PATCH 08/15] simplify SLOTS_LEN definition in FFI example --- pyo3-ffi/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index cd23a6ac3ca..fa67614a38e 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -161,9 +161,9 @@ //! PyMethodDef::zeroed(), //! ]; //! -//! #[cfg(not(Py_3_15))] //! const SLOTS_LEN: usize = -//! 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize; +//! 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 4 * (cfg!(Py_3_15) as usize); +//! //! #[cfg(not(Py_3_15))] //! static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ //! PyModuleDef_Slot { @@ -185,9 +185,6 @@ //! PyABIInfo_VAR!(ABI_INFO); //! //! #[cfg(Py_3_15)] -//! const SLOTS_LEN: usize = -//! 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 4 * (cfg!(Py_3_15) as usize); -//! #[cfg(Py_3_15)] //! static mut SLOTS: [PySlot; SLOTS_LEN] = [ //! PySlot_STATIC_DATA(Py_mod_abi, std::ptr::addr_of_mut!(ABI_INFO).cast()), //! PySlot_STATIC_DATA(Py_mod_name, c"string_sum".as_ptr() as *mut c_void), From 517c56003845c418121c55fb608174dc6d44cf78 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 9 May 2026 12:28:34 -0600 Subject: [PATCH 09/15] delete dead code --- pyo3-ffi/src/typeslots.rs | 138 -------------------------------------- 1 file changed, 138 deletions(-) delete mode 100644 pyo3-ffi/src/typeslots.rs diff --git a/pyo3-ffi/src/typeslots.rs b/pyo3-ffi/src/typeslots.rs deleted file mode 100644 index 1b2df5cf3c0..00000000000 --- a/pyo3-ffi/src/typeslots.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::ffi::c_int; - -#[allow(unused_variables)] -const fn _Py_SLOT_COMPAT_VALUE(OLD: c_int, NEW: c_int) -> c_int { - if cfg!(Py_3_15) { - NEW - } else { - OLD - } -} - -#[cfg(Py_3_15)] -pub const Py_slot_end: u16 = 0; -pub const Py_mp_subscript: c_int = 5; -pub const Py_nb_absolute: c_int = 6; -pub const Py_nb_add: c_int = 7; -pub const Py_nb_and: c_int = 8; -pub const Py_nb_bool: c_int = 9; -pub const Py_nb_divmod: c_int = 10; -pub const Py_nb_float: c_int = 11; -pub const Py_nb_floor_divide: c_int = 12; -pub const Py_nb_index: c_int = 13; -pub const Py_nb_inplace_add: c_int = 14; -pub const Py_nb_inplace_and: c_int = 15; -pub const Py_nb_inplace_floor_divide: c_int = 16; -pub const Py_nb_inplace_lshift: c_int = 17; -pub const Py_nb_inplace_multiply: c_int = 18; -pub const Py_nb_inplace_or: c_int = 19; -pub const Py_nb_inplace_power: c_int = 20; -pub const Py_nb_inplace_remainder: c_int = 21; -pub const Py_nb_inplace_rshift: c_int = 22; -pub const Py_nb_inplace_subtract: c_int = 23; -pub const Py_nb_inplace_true_divide: c_int = 24; -pub const Py_nb_inplace_xor: c_int = 25; -pub const Py_nb_int: c_int = 26; -pub const Py_nb_invert: c_int = 27; -pub const Py_nb_lshift: c_int = 28; -pub const Py_nb_multiply: c_int = 29; -pub const Py_nb_negative: c_int = 30; -pub const Py_nb_or: c_int = 31; -pub const Py_nb_positive: c_int = 32; -pub const Py_nb_power: c_int = 33; -pub const Py_nb_remainder: c_int = 34; -pub const Py_nb_rshift: c_int = 35; -pub const Py_nb_subtract: c_int = 36; -pub const Py_nb_true_divide: c_int = 37; -pub const Py_nb_xor: c_int = 38; -pub const Py_sq_ass_item: c_int = 39; -pub const Py_sq_concat: c_int = 40; -pub const Py_sq_contains: c_int = 41; -pub const Py_sq_inplace_concat: c_int = 42; -pub const Py_sq_inplace_repeat: c_int = 43; -pub const Py_sq_item: c_int = 44; -pub const Py_sq_length: c_int = 45; -pub const Py_sq_repeat: c_int = 46; -pub const Py_tp_alloc: c_int = 47; -pub const Py_tp_base: c_int = 48; -pub const Py_tp_bases: c_int = 49; -pub const Py_tp_call: c_int = 50; -pub const Py_tp_clear: c_int = 51; -pub const Py_tp_dealloc: c_int = 52; -pub const Py_tp_del: c_int = 53; -pub const Py_tp_descr_get: c_int = 54; -pub const Py_tp_descr_set: c_int = 55; -pub const Py_tp_doc: c_int = 56; -pub const Py_tp_getattr: c_int = 57; -pub const Py_tp_getattro: c_int = 58; -pub const Py_tp_hash: c_int = 59; -pub const Py_tp_init: c_int = 60; -pub const Py_tp_is_gc: c_int = 61; -pub const Py_tp_iter: c_int = 62; -pub const Py_tp_iternext: c_int = 63; -pub const Py_tp_methods: c_int = 64; -pub const Py_tp_new: c_int = 65; -pub const Py_tp_repr: c_int = 66; -pub const Py_tp_richcompare: c_int = 67; -pub const Py_tp_setattr: c_int = 68; -pub const Py_tp_setattro: c_int = 69; -pub const Py_tp_str: c_int = 70; -pub const Py_tp_traverse: c_int = 71; -pub const Py_tp_members: c_int = 72; -pub const Py_tp_getset: c_int = 73; -pub const Py_tp_free: c_int = 74; -pub const Py_nb_matrix_multiply: c_int = 75; -pub const Py_nb_inplace_matrix_multiply: c_int = 76; -pub const Py_am_await: c_int = 77; -pub const Py_am_aiter: c_int = 78; -pub const Py_am_anext: c_int = 79; -pub const Py_tp_finalize: c_int = 80; -pub const Py_am_send: c_int = 81; -pub const Py_tp_vectorcall: c_int = 82; -pub const Py_tp_token: c_int = 83; -pub const Py_mod_create: c_int = _Py_SLOT_COMPAT_VALUE(1, 84); -pub const Py_mod_exec: c_int = _Py_SLOT_COMPAT_VALUE(2, 85); -pub const Py_mod_multiple_interpreters: c_int = _Py_SLOT_COMPAT_VALUE(3, 86); -pub const Py_mod_gil: c_int = _Py_SLOT_COMPAT_VALUE(4, 87); -pub const Py_bf_getbuffer: c_int = _Py_SLOT_COMPAT_VALUE(1, 88); -pub const Py_bf_releasebuffer: c_int = _Py_SLOT_COMPAT_VALUE(2, 89); -pub const Py_mp_ass_subscript: c_int = _Py_SLOT_COMPAT_VALUE(3, 90); -pub const Py_mp_length: c_int = _Py_SLOT_COMPAT_VALUE(4, 91); -#[cfg(Py_3_15)] -pub const Py_slot_subslots: c_int = 92; -#[cfg(Py_3_15)] -pub const Py_tp_slots: c_int = 93; -#[cfg(Py_3_15)] -pub const Py_mod_slots: c_int = 94; -#[cfg(Py_3_15)] -pub const Py_tp_name: c_int = 95; -#[cfg(Py_3_15)] -pub const Py_tp_basicsize: c_int = 96; -#[cfg(Py_3_15)] -pub const Py_tp_extra_basicsize: c_int = 97; -#[cfg(Py_3_15)] -pub const Py_tp_itemsize: c_int = 98; -#[cfg(Py_3_15)] -pub const Py_tp_flags: c_int = 99; -#[cfg(Py_3_15)] -pub const Py_mod_name: c_int = 100; -#[cfg(Py_3_15)] -pub const Py_mod_doc: c_int = 101; -#[cfg(Py_3_15)] -pub const Py_mod_state_size: c_int = 102; -#[cfg(Py_3_15)] -pub const Py_mod_methods: c_int = 103; -#[cfg(Py_3_15)] -pub const Py_mod_state_traverse: c_int = 104; -#[cfg(Py_3_15)] -pub const Py_mod_state_clear: c_int = 105; -#[cfg(Py_3_15)] -pub const Py_mod_state_free: c_int = 106; -#[cfg(Py_3_15)] -pub const Py_tp_metaclass: c_int = 107; -#[cfg(Py_3_15)] -pub const Py_tp_module: c_int = 108; -#[cfg(Py_3_15)] -pub const Py_mod_abi: c_int = 109; -#[cfg(Py_3_15)] -pub const Py_mod_token: c_int = 110; From 43ce64887e243ac28ec4e61dba5ce59f96fbaf47 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 May 2026 14:44:24 -0600 Subject: [PATCH 10/15] Apply comments from David --- pyo3-ffi/examples/sequential/src/module.rs | 11 ++++------- pyo3-ffi/src/slots.rs | 7 +++---- pyo3-ffi/src/slots_generated.rs | 2 +- src/impl_/pymodule.rs | 5 +---- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index abb08f25f9d..b71d8b3fbcc 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -32,13 +32,10 @@ pub static mut SEQUENTIAL_SLOTS: [PySlot; SEQUENTIAL_SLOTS_LEN] = [ Py_mod_state_size, mem::size_of::() as Py_ssize_t, ), - PySlot_FUNC( - Py_mod_state_traverse, - Some(sequential_traverse as *mut c_void), - ), - PySlot_FUNC(Py_mod_state_clear, Some(sequential_clear as *mut c_void)), - PySlot_FUNC(Py_mod_state_free, Some(sequential_free as *mut c_void)), - PySlot_FUNC(Py_mod_exec, Some(sequential_exec as *mut c_void)), + PySlot_FUNC(Py_mod_state_traverse, sequential_traverse as *mut c_void), + PySlot_FUNC(Py_mod_state_clear, sequential_clear as *mut c_void), + PySlot_FUNC(Py_mod_state_free, sequential_free as *mut c_void), + PySlot_FUNC(Py_mod_exec, sequential_exec as *mut c_void), PySlot_DATA( Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index ecfd1f4cf3b..d0fc83a6944 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -69,15 +69,14 @@ pub const fn PySlot_DATA(NAME: c_int, VALUE: *mut c_void) -> PySlot { } #[cfg(Py_3_15)] -pub const fn PySlot_FUNC(NAME: c_int, VALUE: Option<*mut c_void>) -> PySlot { +pub const fn PySlot_FUNC(NAME: c_int, VALUE: *mut c_void) -> PySlot { + assert!(!VALUE.is_null(), "value may not be null"); PySlot { sl_id: safe_cast_c_int_to_u16(NAME), sl_flags: 0, anon1: _anon_union_32b { sl_reserved: 0 }, anon2: _anon_union_64b { - sl_func: Some(unsafe { - std::mem::transmute::<*mut c_void, _Py_funcptr_t>(VALUE.unwrap()) - }), + sl_func: Some(unsafe { std::mem::transmute::<*mut c_void, _Py_funcptr_t>(VALUE) }), }, } } diff --git a/pyo3-ffi/src/slots_generated.rs b/pyo3-ffi/src/slots_generated.rs index 1b2df5cf3c0..b6af4beaae6 100644 --- a/pyo3-ffi/src/slots_generated.rs +++ b/pyo3-ffi/src/slots_generated.rs @@ -10,7 +10,7 @@ const fn _Py_SLOT_COMPAT_VALUE(OLD: c_int, NEW: c_int) -> c_int { } #[cfg(Py_3_15)] -pub const Py_slot_end: u16 = 0; +pub const Py_slot_end: c_int = 0; pub const Py_mp_subscript: c_int = 5; pub const Py_nb_absolute: c_int = 6; pub const Py_nb_add: c_int = 7; diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index a4c83869008..18f3e327f09 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -306,10 +306,7 @@ impl PyModuleSlotsBuilder { } #[cfg(Py_3_15)] { - self.push_value(ffi::PySlot_FUNC( - ffi::Py_mod_exec, - Some(exec as *mut c_void), - )) + self.push_value(ffi::PySlot_FUNC(ffi::Py_mod_exec, exec as *mut c_void)) } } From 67d947e15b9f35b60868e7ca9ada2312ed9ba710 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 May 2026 15:55:45 -0600 Subject: [PATCH 11/15] gate on rustc version --- pyo3-build-config/src/lib.rs | 1 + pyo3-ffi/src/slots.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 9bc9a9c8cc9..beac716a9bc 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -247,6 +247,7 @@ fn print_feature_cfg(minor_version_required: u32, cfg: &str) { /// so this function is unstable. #[doc(hidden)] pub fn print_feature_cfgs() { + print_feature_cfg(84, "const_is_null"); print_feature_cfg(85, "fn_ptr_eq"); print_feature_cfg(86, "from_bytes_with_nul_error"); } diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index d0fc83a6944..fe9eaa6547e 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -70,6 +70,7 @@ pub const fn PySlot_DATA(NAME: c_int, VALUE: *mut c_void) -> PySlot { #[cfg(Py_3_15)] pub const fn PySlot_FUNC(NAME: c_int, VALUE: *mut c_void) -> PySlot { + #[cfg(const_is_null)] assert!(!VALUE.is_null(), "value may not be null"); PySlot { sl_id: safe_cast_c_int_to_u16(NAME), From 651cf3731b1ebb10315f5a6ede5e78dd6b91bda6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 May 2026 16:17:03 -0600 Subject: [PATCH 12/15] fix clippy --- pyo3-ffi/examples/sequential/src/module.rs | 9 +++++---- pyo3-ffi/src/slots.rs | 3 ++- src/impl_/pymodule.rs | 3 ++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index b71d8b3fbcc..7f562a28725 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -32,10 +32,11 @@ pub static mut SEQUENTIAL_SLOTS: [PySlot; SEQUENTIAL_SLOTS_LEN] = [ Py_mod_state_size, mem::size_of::() as Py_ssize_t, ), - PySlot_FUNC(Py_mod_state_traverse, sequential_traverse as *mut c_void), - PySlot_FUNC(Py_mod_state_clear, sequential_clear as *mut c_void), - PySlot_FUNC(Py_mod_state_free, sequential_free as *mut c_void), - PySlot_FUNC(Py_mod_exec, sequential_exec as *mut c_void), + // safety: all these funciton pointers are non-null by construction + unsafe { PySlot_FUNC(Py_mod_state_traverse, sequential_traverse as *mut c_void) }, + unsafe { PySlot_FUNC(Py_mod_state_clear, sequential_clear as *mut c_void) }, + unsafe { PySlot_FUNC(Py_mod_state_free, sequential_free as *mut c_void) }, + unsafe { PySlot_FUNC(Py_mod_exec, sequential_exec as *mut c_void) }, PySlot_DATA( Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index fe9eaa6547e..2bb214ad7ea 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -68,8 +68,9 @@ pub const fn PySlot_DATA(NAME: c_int, VALUE: *mut c_void) -> PySlot { } } +#[expect(clippy::incompatible_msrv, reason = "guarded by cfg(const_is_null)")] #[cfg(Py_3_15)] -pub const fn PySlot_FUNC(NAME: c_int, VALUE: *mut c_void) -> PySlot { +pub const unsafe fn PySlot_FUNC(NAME: c_int, VALUE: *mut c_void) -> PySlot { #[cfg(const_is_null)] assert!(!VALUE.is_null(), "value may not be null"); PySlot { diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 18f3e327f09..ff3bc60252e 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -306,7 +306,8 @@ impl PyModuleSlotsBuilder { } #[cfg(Py_3_15)] { - self.push_value(ffi::PySlot_FUNC(ffi::Py_mod_exec, exec as *mut c_void)) + // safety: exce is not NULL + self.push_value(unsafe { ffi::PySlot_FUNC(ffi::Py_mod_exec, exec as *mut c_void) }) } } From e87f566f0b8adb86421f1b98ad98221f53bd5f00 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 May 2026 16:28:35 -0600 Subject: [PATCH 13/15] spelling --- pyo3-ffi/examples/sequential/src/module.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index 7f562a28725..4d95af2d9ac 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -32,7 +32,7 @@ pub static mut SEQUENTIAL_SLOTS: [PySlot; SEQUENTIAL_SLOTS_LEN] = [ Py_mod_state_size, mem::size_of::() as Py_ssize_t, ), - // safety: all these funciton pointers are non-null by construction + // safety: all these function pointers are non-null by construction unsafe { PySlot_FUNC(Py_mod_state_traverse, sequential_traverse as *mut c_void) }, unsafe { PySlot_FUNC(Py_mod_state_clear, sequential_clear as *mut c_void) }, unsafe { PySlot_FUNC(Py_mod_state_free, sequential_free as *mut c_void) }, From 56c87b087db49f9ee286b7e1d23e9636f749dd73 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 May 2026 17:32:48 -0600 Subject: [PATCH 14/15] fix ffi-check --- pyo3-ffi-check/macro/src/lib.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index 3a6b3923f06..7151e938c2a 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -359,6 +359,15 @@ const MACRO_EXCLUSIONS: &[(&str, &str)] = &[ ("PySet_CheckExact", "not(PyPy)"), ("PySet_GET_SIZE", ""), ("PySlice_Check", ""), + ("PySlot_DATA", ""), + ("PySlot_END", ""), + ("PySlot_FUNC", ""), + ("PySlot_INT64", ""), + ("PySlot_PTR", ""), + ("PySlot_PTR_STATIC", ""), + ("PySlot_SIZE", ""), + ("PySlot_STATIC_DATA", ""), + ("PySlot_UINT64", ""), ("PyStructSequence_GET_ITEM", ""), ("PyStructSequence_SET_ITEM", ""), ("PyTZInfo_Check", "not(PyPy)"), From 26809b9c2682a0cd1b930530dd7146db108e5b60 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 May 2026 17:42:24 -0600 Subject: [PATCH 15/15] fix FFI doctest on Python 3.8 --- pyo3-ffi/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index fa67614a38e..535d3e2c8aa 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -166,6 +166,7 @@ //! //! #[cfg(not(Py_3_15))] //! static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ +//! #[cfg(Py_3_12)] //! PyModuleDef_Slot { //! slot: Py_mod_multiple_interpreters, //! value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED,