diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3dd03c8d90..7bc7c89b93c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -206,6 +206,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-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 495a0a174a8..fb56f848b94 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -181,6 +181,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-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index 4ad5d387ded..7151e938c2a 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -187,6 +187,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() }; @@ -352,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)"), 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..4d95af2d9ac 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -20,44 +20,33 @@ 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, + ), + // 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) }, + unsafe { PySlot_FUNC(Py_mod_exec, sequential_exec as *mut c_void) }, + 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 823f4e47ddf..535d3e2c8aa 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,34 +161,11 @@ //! PyMethodDef::zeroed(), //! ]; //! -//! #[cfg(Py_3_15)] -//! 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(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, @@ -203,6 +182,21 @@ //! }, //! ]; //! +//! #[cfg(Py_3_15)] +//! PyABIInfo_VAR!(ABI_INFO); +//! +//! #[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 +208,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() //! } //! @@ -440,6 +434,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::*; @@ -482,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::*; @@ -507,6 +505,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; @@ -574,12 +573,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 94f4e9286f5..cfc7bbd0f35 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}; #[cfg(not(RustPython))] @@ -92,33 +94,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, @@ -142,17 +117,13 @@ pub const Py_MOD_GIL_NOT_USED: *mut c_void = 1 as *mut c_void; extern_libpython! { #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] pub fn PyUnstable_Module_SetGIL(module: *mut PyObject, gil: *mut c_void) -> c_int; +} - #[cfg(Py_3_15)] - pub fn PyModule_FromSlotsAndSpec( - slots: *const PyModuleDef_Slot, - spec: *mut PyObject, - ) -> *mut PyObject; - #[cfg(Py_3_15)] +#[cfg(Py_3_15)] +extern_libpython! { + pub fn PyModule_FromSlotsAndSpec(slots: *const PySlot, spec: *mut PyObject) -> *mut PyObject; pub fn PyModule_Exec(_mod: *mut PyObject) -> c_int; - #[cfg(Py_3_15)] pub fn PyModule_GetStateSize(_mod: *mut PyObject, result: *mut Py_ssize_t) -> c_int; - #[cfg(Py_3_15)] 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 2e4f82e9e28..bc4a1a138b8 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)] @@ -279,6 +281,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; @@ -381,6 +391,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..2bb214ad7ea --- /dev/null +++ b/pyo3-ffi/src/slots.rs @@ -0,0 +1,149 @@ +#[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: Option<_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; + +// 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"); + } +} + +#[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 }, + } +} + +#[expect(clippy::incompatible_msrv, reason = "guarded by cfg(const_is_null)")] +#[cfg(Py_3_15)] +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 { + 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)] +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/typeslots.rs b/pyo3-ffi/src/slots_generated.rs similarity index 60% rename from pyo3-ffi/src/typeslots.rs rename to pyo3-ffi/src/slots_generated.rs index fd0c3b77a74..b6af4beaae6 100644 --- a/pyo3-ffi/src/typeslots.rs +++ b/pyo3-ffi/src/slots_generated.rs @@ -1,9 +1,16 @@ 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 { + if cfg!(Py_3_15) { + NEW + } else { + OLD + } +} + +#[cfg(Py_3_15)] +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; @@ -80,3 +87,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..ff3bc60252e 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,19 @@ 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)] + { + // safety: exce is not NULL + self.push_value(unsafe { ffi::PySlot_FUNC(ffi::Py_mod_exec, exec as *mut c_void) }) + } } 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 +324,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 +347,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 +365,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, + (&raw mut ABI_INFO).cast(), + )) } #[cfg(not(Py_3_15))] @@ -324,7 +380,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 +398,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 +410,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 +582,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 +593,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());