From a1694a34ae4147246eba739c2108a6d4f2f4dffc Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 19 Jan 2026 10:10:03 -0700 Subject: [PATCH 01/13] add wrappers for the PEP 793 PyModSupport API --- pyo3-ffi/src/modsupport.rs | 26 ++++++++++++++------------ pyo3-ffi/src/moduleobject.rs | 26 ++++++++++++++++++++++++++ pyo3-ffi/src/object.rs | 3 +++ 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/pyo3-ffi/src/modsupport.rs b/pyo3-ffi/src/modsupport.rs index 8cda23a8c2d..fecea61a772 100644 --- a/pyo3-ffi/src/modsupport.rs +++ b/pyo3-ffi/src/modsupport.rs @@ -86,7 +86,21 @@ extern "C" { #[cfg(not(py_sys_config = "Py_TRACE_REFS"))] #[cfg_attr(PyPy, link_name = "PyPyModule_Create2")] pub fn PyModule_Create2(module: *mut PyModuleDef, apiver: c_int) -> *mut PyObject; +} + +#[inline] +pub unsafe fn PyModule_Create(module: *mut PyModuleDef) -> *mut PyObject { + PyModule_Create2( + module, + if cfg!(Py_LIMITED_API) { + PYTHON_ABI_VERSION + } else { + PYTHON_API_VERSION + }, + ) +} +extern "C" { #[cfg(py_sys_config = "Py_TRACE_REFS")] fn PyModule_Create2TraceRefs(module: *mut PyModuleDef, apiver: c_int) -> *mut PyObject; @@ -122,18 +136,6 @@ pub unsafe fn PyModule_FromDefAndSpec2( PyModule_FromDefAndSpec2TraceRefs(def, spec, module_api_version) } -#[inline] -pub unsafe fn PyModule_Create(module: *mut PyModuleDef) -> *mut PyObject { - PyModule_Create2( - module, - if cfg!(Py_LIMITED_API) { - PYTHON_ABI_VERSION - } else { - PYTHON_API_VERSION - }, - ) -} - #[inline] pub unsafe fn PyModule_FromDefAndSpec(def: *mut PyModuleDef, spec: *mut PyObject) -> *mut PyObject { PyModule_FromDefAndSpec2( diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index 3be0e44f3fc..c087a155576 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -93,6 +93,24 @@ pub const Py_mod_exec: c_int = 2; 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 @@ -121,6 +139,14 @@ extern "C" { pub fn PyUnstable_Module_SetGIL(module: *mut PyObject, gil: *mut c_void) -> c_int; } +#[cfg(Py_3_15)] +extern "C" { + pub fn PyModule_FromSlotsAndSpec(slots: *const PyModuleDef_Slot, spec: *mut PyObject); + pub fn PyModule_Exec(_mod: *mut PyObject); + pub fn PyModule_GetStateSize(_mod: *mut PyObject, result: *mut Py_ssize_t); + pub fn PyModule_GetToken(module: *mut PyObject, result: *mut *mut c_void); +} + #[repr(C)] pub struct PyModuleDef { pub m_base: PyModuleDef_Base, diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index c1eddeb2a97..505001edf2d 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -740,4 +740,7 @@ extern "C" { #[cfg(Py_3_14)] pub fn PyType_Freeze(tp: *mut crate::PyTypeObject) -> c_int; + + #[cfg(Py_3_15)] + pub fn PyType_GetModuleByToken(_type: *mut PyTypeObject, token: *const c_void); } From b1d71013d5abc4bf692dae21333b397e90ce0f9c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 19 Jan 2026 10:16:01 -0700 Subject: [PATCH 02/13] add wrappers for the PyABIInfo API --- pyo3-ffi/src/modsupport.rs | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/pyo3-ffi/src/modsupport.rs b/pyo3-ffi/src/modsupport.rs index fecea61a772..2b9acadf3b7 100644 --- a/pyo3-ffi/src/modsupport.rs +++ b/pyo3-ffi/src/modsupport.rs @@ -148,3 +148,58 @@ pub unsafe fn PyModule_FromDefAndSpec(def: *mut PyModuleDef, spec: *mut PyObject }, ) } + +#[cfg(Py_3_15)] +#[repr(C)] +pub struct PyABIInfo { + pub abiinfo_major_version: u8, + pub abiinfo_minor_version: u8, + pub flags: u16, + pub build_version: u32, + pub abi_version: u32, +} +#[cfg(Py_3_15)] +pub const PyABIInfo_STABLE: u16 = 0x0001; +#[cfg(Py_3_15)] +pub const PyABIInfo_GIL: u16 = 0x0002; +#[cfg(Py_3_15)] +pub const PyABIInfo_FREETHREADED: u16 = 0x0004; +#[cfg(Py_3_15)] +pub const PyABIInfo_INTERNAL: u16 = 0x0008; + +#[cfg(Py_3_15)] +pub const PyABIInfo_FREETHREADING_AGNOSTIC: u16 = PyABIInfo_GIL | PyABIInfo_FREETHREADED; + +#[cfg(Py_3_15)] +extern "C" { + pub fn PyABIInfo_Check(info: *mut PyABIInfo, module_name: *const c_char); +} + +#[cfg(all(Py_LIMITED_API, Py_3_15))] +const _PyABIInfo_DEFAULT_FLAG_STABLE: u16 = PyABIInfo_STABLE; +#[cfg(all(Py_3_15, not(Py_LIMITED_API)))] +const _PyABIInfo_DEFAULT_FLAG_STABLE: u16 = 0; + +// skipped PyABIInfo_DEFAULT_ABI_VERSION: depends on Py_VERSION_HEX + +#[cfg(all(Py_3_15, Py_GIL_DISABLED))] +const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_FREETHREADED; +#[cfg(all(Py_3_15, not(Py_GIL_DISABLED)))] +const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_GIL; + +#[cfg(Py_3_15)] +// has an alternate definition if Py_BUILD_CORE is set, ignore that +const _PyABIInfo_DEFAULT_FLAG_INTERNAL: u16 = 0; + +#[cfg(Py_3_15)] +pub const PyABIInfo_DEFAULT_FLAGS: u16 = + _PyABIInfo_DEFAULT_FLAG_STABLE | _PyABIInfo_DEFAULT_FLAG_FT | _PyABIInfo_DEFAULT_FLAG_INTERNAL; + +#[cfg(Py_3_15)] +const _PyABIInfo_DEFAULT: PyABIInfo = PyABIInfo { + abiinfo_major_version: 1, + abiinfo_minor_version: 0, + flags: PyABIInfo_DEFAULT_FLAGS, + build_version: 0, + abi_version: 0, +}; From 0103458017cf4fb1c13b24c66a9eb55de74f2b38 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 19 Jan 2026 10:16:19 -0700 Subject: [PATCH 03/13] Fix error in PyModuleDef_Base definition --- pyo3-ffi/src/moduleobject.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index c087a155576..eb79a5146bf 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -55,7 +55,7 @@ extern "C" { #[repr(C)] pub struct PyModuleDef_Base { pub ob_base: PyObject, - pub m_init: Option *mut PyObject>, + pub m_init: *mut PyObject, pub m_index: Py_ssize_t, pub m_copy: *mut PyObject, } @@ -66,7 +66,7 @@ pub struct PyModuleDef_Base { )] pub const PyModuleDef_HEAD_INIT: PyModuleDef_Base = PyModuleDef_Base { ob_base: PyObject_HEAD_INIT, - m_init: None, + m_init: std::ptr::null_mut(), m_index: 0, m_copy: std::ptr::null_mut(), }; From 5fa28074abbcb6f654721be56d9f57aa079b06f8 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 19 Jan 2026 10:27:12 -0700 Subject: [PATCH 04/13] add changelog entry --- newsfragments/5746.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/5746.added.md diff --git a/newsfragments/5746.added.md b/newsfragments/5746.added.md new file mode 100644 index 00000000000..3613d4c82c6 --- /dev/null +++ b/newsfragments/5746.added.md @@ -0,0 +1 @@ +Added FFI wrappers for the PyABIInfo and PyModExport ABIs available in Python 3.15. \ No newline at end of file From eb869733d57888c291e9a6578943b6999708e9c2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 19 Jan 2026 13:15:56 -0700 Subject: [PATCH 05/13] expose PyABIInfo_VAR --- pyo3-ffi/src/modsupport.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pyo3-ffi/src/modsupport.rs b/pyo3-ffi/src/modsupport.rs index 2b9acadf3b7..cc583a0b424 100644 --- a/pyo3-ffi/src/modsupport.rs +++ b/pyo3-ffi/src/modsupport.rs @@ -196,10 +196,19 @@ pub const PyABIInfo_DEFAULT_FLAGS: u16 = _PyABIInfo_DEFAULT_FLAG_STABLE | _PyABIInfo_DEFAULT_FLAG_FT | _PyABIInfo_DEFAULT_FLAG_INTERNAL; #[cfg(Py_3_15)] -const _PyABIInfo_DEFAULT: PyABIInfo = PyABIInfo { +// must be pub because it is used by PyABIInfo_VAR +pub const _PyABIInfo_DEFAULT: PyABIInfo = PyABIInfo { abiinfo_major_version: 1, abiinfo_minor_version: 0, flags: PyABIInfo_DEFAULT_FLAGS, build_version: 0, abi_version: 0, }; + +#[cfg(Py_3_15)] +#[macro_export] +macro_rules! PyABIInfo_VAR { + ($name:ident) => { + static mut $name: PyABIInfo = _PyABIInfo_DEFAULT; + }; +} From f3c8778c1f9d1905cbf1ffc29b4d7c1b042de7e5 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 19 Jan 2026 13:16:10 -0700 Subject: [PATCH 06/13] use the new APIs in the pyo3-ffi examples --- pyo3-ffi/examples/sequential/src/lib.rs | 11 +++++ pyo3-ffi/examples/sequential/src/module.rs | 49 ++++++++++++++++++++-- pyo3-ffi/examples/string-sum/src/lib.rs | 42 +++++++++++++++++-- 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/pyo3-ffi/examples/sequential/src/lib.rs b/pyo3-ffi/examples/sequential/src/lib.rs index 452c0eb8784..dd90e0df85f 100644 --- a/pyo3-ffi/examples/sequential/src/lib.rs +++ b/pyo3-ffi/examples/sequential/src/lib.rs @@ -4,10 +4,21 @@ use pyo3_ffi::*; mod id; mod module; +#[cfg(not(Py_3_15))] use crate::module::MODULE_DEF; +#[cfg(Py_3_15)] +use crate::module::SEQUENTIAL_SLOTS; +#[cfg(not(Py_3_15))] #[allow(non_snake_case, reason = "must be named `PyInit_`")] #[no_mangle] pub unsafe extern "C" fn PyInit_sequential() -> *mut PyObject { PyModuleDef_Init(ptr::addr_of_mut!(MODULE_DEF)) } + +#[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 { + std::ptr::addr_of_mut!(SEQUENTIAL_SLOTS).cast() +} diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index 99b573d4154..168fffbdb00 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -2,6 +2,7 @@ use core::{mem, ptr}; use pyo3_ffi::*; use std::ffi::{c_int, c_void}; +#[cfg(not(Py_3_15))] pub static mut MODULE_DEF: PyModuleDef = PyModuleDef { m_base: PyModuleDef_HEAD_INIT, m_name: c"sequential".as_ptr(), @@ -14,9 +15,51 @@ pub static mut MODULE_DEF: PyModuleDef = PyModuleDef { m_free: Some(sequential_free), }; -const SEQUENTIAL_SLOTS_LEN: usize = - 2 + if cfg!(Py_3_12) { 1 } else { 0 } + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 }; -static mut SEQUENTIAL_SLOTS: [PyModuleDef_Slot; SEQUENTIAL_SLOTS_LEN] = [ +#[cfg(Py_3_15)] +PyABIInfo_VAR!(abi_info); + +const SEQUENTIAL_SLOTS_LEN: usize = 2 + + if cfg!(Py_3_12) { 1 } else { 0 } + + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 } + + if cfg!(Py_3_15) { 7 } else { 0 }; +pub static mut SEQUENTIAL_SLOTS: [PyModuleDef_Slot; SEQUENTIAL_SLOTS_LEN] = [ + #[cfg(Py_3_15)] + PyModuleDef_Slot { + slot: Py_mod_abi, + value: std::ptr::addr_of_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 658b798c523..c947e8a782e 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -1,8 +1,9 @@ -use std::ffi::{c_char, c_long}; +use std::ffi::{c_char, c_long, c_void}; use std::ptr; use pyo3_ffi::*; +#[cfg(not(Py_3_15))] static mut MODULE_DEF: PyModuleDef = PyModuleDef { m_base: PyModuleDef_HEAD_INIT, m_name: c"string_sum".as_ptr(), @@ -28,9 +29,36 @@ static mut METHODS: [PyMethodDef; 2] = [ PyMethodDef::zeroed(), ]; -const SLOTS_LEN: usize = - 1 + if cfg!(Py_3_12) { 1 } else { 0 } + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 }; +#[cfg(Py_3_15)] +PyABIInfo_VAR!(abi_info); + +const SLOTS_LEN: usize = 1 + + if cfg!(Py_3_12) { 1 } else { 0 } + + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 } + + if cfg!(Py_3_15) { 4 } else { 0 }; static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ + #[cfg(Py_3_15)] + PyModuleDef_Slot { + slot: Py_mod_abi, + value: std::ptr::addr_of_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: std::ptr::addr_of_mut!(METHODS).cast(), + }, #[cfg(Py_3_12)] PyModuleDef_Slot { slot: Py_mod_multiple_interpreters, @@ -48,12 +76,20 @@ static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ ]; // The module initialization function +#[cfg(not(Py_3_15))] #[allow(non_snake_case, reason = "must be named `PyInit_`")] #[no_mangle] pub unsafe extern "C" fn PyInit_string_sum() -> *mut PyObject { PyModuleDef_Init(ptr::addr_of_mut!(MODULE_DEF)) } +#[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 { + std::ptr::addr_of_mut!(SLOTS).cast() +} + /// A helper to parse function arguments /// If we used PyO3's proc macros they'd handle all of this boilerplate for us :) unsafe fn parse_arg_as_i32(obj: *mut PyObject, n_arg: usize) -> Option { From 96c90d9cba094adc6c02dad0629e7338ddf999dd Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 19 Jan 2026 13:27:10 -0700 Subject: [PATCH 07/13] fix python < 3.15 conditional compilation --- pyo3-ffi/examples/string-sum/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index c947e8a782e..aec48fe251e 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -1,4 +1,6 @@ -use std::ffi::{c_char, c_long, c_void}; +#[cfg(Py_3_15)] +use std::ffi::c_void; +use std::ffi::{c_char, c_long}; use std::ptr; use pyo3_ffi::*; From a3c01fabfbfe77f957deb0095e3cee8d4a0ae1be Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 19 Jan 2026 13:27:21 -0700 Subject: [PATCH 08/13] update code in pyo3-ffi README --- pyo3-ffi/README.md | 58 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index badab23b070..d38a4f57549 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -65,11 +65,14 @@ fn main() { **`src/lib.rs`** ```rust,no_run +#[cfg(Py_3_15)] +use std::ffi::c_void; use std::ffi::{c_char, c_long}; use std::ptr; use pyo3_ffi::*; +#[cfg(not(Py_3_15))] static mut MODULE_DEF: PyModuleDef = PyModuleDef { m_base: PyModuleDef_HEAD_INIT, m_name: c"string_sum".as_ptr(), @@ -95,16 +98,41 @@ static mut METHODS: [PyMethodDef; 2] = [ PyMethodDef::zeroed(), ]; -const SLOTS_LEN: usize = 1 + if cfg!(Py_3_12) { 1 } else { 0 } + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 }; +#[cfg(Py_3_15)] +PyABIInfo_VAR!(abi_info); + +const SLOTS_LEN: usize = 1 + + if cfg!(Py_3_12) { 1 } else { 0 } + + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 } + + if cfg!(Py_3_15) { 4 } else { 0 }; static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ - // NB: only include this slot if the module does not store any global state in `static` variables - // or other data which could cross between subinterpreters + #[cfg(Py_3_15)] + PyModuleDef_Slot { + slot: Py_mod_abi, + value: std::ptr::addr_of_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: std::ptr::addr_of_mut!(METHODS).cast(), + }, #[cfg(Py_3_12)] PyModuleDef_Slot { slot: Py_mod_multiple_interpreters, value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, }, - // NB: only include this slot if the module does not depend on the GIL for thread safety #[cfg(Py_GIL_DISABLED)] PyModuleDef_Slot { slot: Py_mod_gil, @@ -116,13 +144,21 @@ static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ }, ]; -// The module initialization function, which must be named `PyInit_`. -#[allow(non_snake_case)] +// The module initialization function +#[cfg(not(Py_3_15))] +#[allow(non_snake_case, reason = "must be named `PyInit_`")] #[no_mangle] pub unsafe extern "C" fn PyInit_string_sum() -> *mut PyObject { PyModuleDef_Init(ptr::addr_of_mut!(MODULE_DEF)) } +#[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 { + std::ptr::addr_of_mut!(SLOTS).cast() +} + /// A helper to parse function arguments /// If we used PyO3's proc macros they'd handle all of this boilerplate for us :) unsafe fn parse_arg_as_i32(obj: *mut PyObject, n_arg: usize) -> Option { @@ -140,7 +176,10 @@ unsafe fn parse_arg_as_i32(obj: *mut PyObject, n_arg: usize) -> Option { let mut overflow = 0; let i_long: c_long = PyLong_AsLongAndOverflow(obj, &mut overflow); - #[allow(irrefutable_let_patterns)] // some platforms have c_long equal to i32 + #[allow( + irrefutable_let_patterns, + reason = "some platforms have c_long equal to i32" + )] if overflow != 0 { raise_overflowerror(obj); None @@ -200,10 +239,7 @@ pub unsafe extern "C" fn sum_as_string( PyUnicode_FromStringAndSize(string.as_ptr().cast::(), string.len() as isize) } None => { - PyErr_SetString( - PyExc_OverflowError, - c"arguments too large to add".as_ptr(), - ); + PyErr_SetString(PyExc_OverflowError, c"arguments too large to add".as_ptr()); std::ptr::null_mut() } } From 49b20338afda42510c4af4ee754626e57e4d4dbe Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 19 Jan 2026 14:01:24 -0700 Subject: [PATCH 09/13] fix issues caught during windowd 3.15t tests --- pyo3-ffi/examples/sequential/src/lib.rs | 2 +- pyo3-ffi/examples/sequential/src/module.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyo3-ffi/examples/sequential/src/lib.rs b/pyo3-ffi/examples/sequential/src/lib.rs index dd90e0df85f..a77ec9da94d 100644 --- a/pyo3-ffi/examples/sequential/src/lib.rs +++ b/pyo3-ffi/examples/sequential/src/lib.rs @@ -20,5 +20,5 @@ pub unsafe extern "C" fn PyInit_sequential() -> *mut PyObject { #[allow(non_snake_case, reason = "must be named `PyModExport_`")] #[no_mangle] pub unsafe extern "C" fn PyModExport_sequential() -> *mut PyModuleDef_Slot { - std::ptr::addr_of_mut!(SEQUENTIAL_SLOTS).cast() + ptr::addr_of_mut!(SEQUENTIAL_SLOTS).cast() } diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index 168fffbdb00..1812e737b68 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -16,7 +16,7 @@ pub static mut MODULE_DEF: PyModuleDef = PyModuleDef { }; #[cfg(Py_3_15)] -PyABIInfo_VAR!(abi_info); +PyABIInfo_VAR!(ABI_INFO); const SEQUENTIAL_SLOTS_LEN: usize = 2 + if cfg!(Py_3_12) { 1 } else { 0 } @@ -26,7 +26,7 @@ pub static mut SEQUENTIAL_SLOTS: [PyModuleDef_Slot; SEQUENTIAL_SLOTS_LEN] = [ #[cfg(Py_3_15)] PyModuleDef_Slot { slot: Py_mod_abi, - value: std::ptr::addr_of_mut!(abi_info).cast(), + value: std::ptr::addr_of_mut!(ABI_INFO).cast(), }, #[cfg(Py_3_15)] PyModuleDef_Slot { From ad29df8b2f0c6dc1eb73aa5ebd2f24449f02ee55 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 19 Jan 2026 14:01:42 -0700 Subject: [PATCH 10/13] Update more out-of-date copies of string_sum sources --- pyo3-ffi/README.md | 4 +- pyo3-ffi/examples/string-sum/src/lib.rs | 4 +- pyo3-ffi/src/lib.rs | 88 +++++++++++++++++++------ 3 files changed, 71 insertions(+), 25 deletions(-) diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index d38a4f57549..af409d1809e 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -99,7 +99,7 @@ static mut METHODS: [PyMethodDef; 2] = [ ]; #[cfg(Py_3_15)] -PyABIInfo_VAR!(abi_info); +PyABIInfo_VAR!(ABI_INFO); const SLOTS_LEN: usize = 1 + if cfg!(Py_3_12) { 1 } else { 0 } @@ -109,7 +109,7 @@ static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ #[cfg(Py_3_15)] PyModuleDef_Slot { slot: Py_mod_abi, - value: std::ptr::addr_of_mut!(abi_info).cast(), + value: std::ptr::addr_of_mut!(ABI_INFO).cast(), }, #[cfg(Py_3_15)] PyModuleDef_Slot { diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index aec48fe251e..7e401b84898 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -32,7 +32,7 @@ static mut METHODS: [PyMethodDef; 2] = [ ]; #[cfg(Py_3_15)] -PyABIInfo_VAR!(abi_info); +PyABIInfo_VAR!(ABI_INFO); const SLOTS_LEN: usize = 1 + if cfg!(Py_3_12) { 1 } else { 0 } @@ -42,7 +42,7 @@ static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ #[cfg(Py_3_15)] PyModuleDef_Slot { slot: Py_mod_abi, - value: std::ptr::addr_of_mut!(abi_info).cast(), + value: std::ptr::addr_of_mut!(ABI_INFO).cast(), }, #[cfg(Py_3_15)] PyModuleDef_Slot { diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index d9992a444df..e2c589986cd 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -126,24 +126,27 @@ //! //! **`src/lib.rs`** //! ```rust,no_run +//! #[cfg(Py_3_15)] +//! use std::ffi::c_void; //! use std::ffi::{c_char, c_long}; //! use std::ptr; //! //! use pyo3_ffi::*; //! +//! #[cfg(not(Py_3_15))] //! static mut MODULE_DEF: PyModuleDef = PyModuleDef { //! m_base: PyModuleDef_HEAD_INIT, //! m_name: c"string_sum".as_ptr(), //! m_doc: c"A Python module written in Rust.".as_ptr(), //! m_size: 0, -//! m_methods: unsafe { METHODS as *const [PyMethodDef] as *mut PyMethodDef }, -//! m_slots: std::ptr::null_mut(), +//! m_methods: std::ptr::addr_of_mut!(METHODS).cast(), +//! m_slots: std::ptr::addr_of_mut!(SLOTS).cast(), //! m_traverse: None, //! m_clear: None, //! m_free: None, //! }; //! -//! static mut METHODS: &[PyMethodDef] = &[ +//! static mut METHODS: [PyMethodDef; 2] = [ //! PyMethodDef { //! ml_name: c"sum_as_string".as_ptr(), //! ml_meth: PyMethodDefPointer { @@ -156,22 +159,65 @@ //! PyMethodDef::zeroed(), //! ]; //! -//! // The module initialization function. +//! #[cfg(Py_3_15)] +//! PyABIInfo_VAR!(ABI_INFO); +//! +//! const SLOTS_LEN: usize = 1 +//! + if cfg!(Py_3_12) { 1 } else { 0 } +//! + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 } +//! + if cfg!(Py_3_15) { 4 } else { 0 }; +//! static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ +//! #[cfg(Py_3_15)] +//! PyModuleDef_Slot { +//! slot: Py_mod_abi, +//! value: std::ptr::addr_of_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: std::ptr::addr_of_mut!(METHODS).cast(), +//! }, +//! #[cfg(Py_3_12)] +//! PyModuleDef_Slot { +//! slot: Py_mod_multiple_interpreters, +//! value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, +//! }, +//! #[cfg(Py_GIL_DISABLED)] +//! PyModuleDef_Slot { +//! slot: Py_mod_gil, +//! value: Py_MOD_GIL_NOT_USED, +//! }, +//! PyModuleDef_Slot { +//! slot: 0, +//! value: ptr::null_mut(), +//! }, +//! ]; +//! +//! // The module initialization function +//! #[cfg(not(Py_3_15))] //! #[allow(non_snake_case, reason = "must be named `PyInit_`")] //! #[no_mangle] //! pub unsafe extern "C" fn PyInit_string_sum() -> *mut PyObject { -//! let module = PyModule_Create(ptr::addr_of_mut!(MODULE_DEF)); -//! if module.is_null() { -//! return module; -//! } -//! #[cfg(Py_GIL_DISABLED)] -//! { -//! if PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) < 0 { -//! Py_DECREF(module); -//! return std::ptr::null_mut(); -//! } -//! } -//! module +//! PyModuleDef_Init(ptr::addr_of_mut!(MODULE_DEF)) +//! } +//! +//! #[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 { +//! std::ptr::addr_of_mut!(SLOTS).cast() //! } //! //! /// A helper to parse function arguments @@ -191,7 +237,10 @@ //! let mut overflow = 0; //! let i_long: c_long = PyLong_AsLongAndOverflow(obj, &mut overflow); //! -//! #[allow(irrefutable_let_patterns, reason = "some platforms have c_long equal to i32")] +//! #[allow( +//! irrefutable_let_patterns, +//! reason = "some platforms have c_long equal to i32" +//! )] //! if overflow != 0 { //! raise_overflowerror(obj); //! None @@ -251,10 +300,7 @@ //! PyUnicode_FromStringAndSize(string.as_ptr().cast::(), string.len() as isize) //! } //! None => { -//! PyErr_SetString( -//! PyExc_OverflowError, -//! c"arguments too large to add".as_ptr(), -//! ); +//! PyErr_SetString(PyExc_OverflowError, c"arguments too large to add".as_ptr()); //! std::ptr::null_mut() //! } //! } From bd41b5b802d52f06120e857a83258b00dabd6f3d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 19 Jan 2026 14:58:42 -0700 Subject: [PATCH 11/13] code golf the number of slots --- pyo3-ffi/README.md | 6 ++---- pyo3-ffi/examples/sequential/src/module.rs | 6 ++---- pyo3-ffi/examples/string-sum/src/lib.rs | 6 ++---- pyo3-ffi/src/lib.rs | 6 ++---- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index af409d1809e..478e456e026 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -101,10 +101,8 @@ static mut METHODS: [PyMethodDef; 2] = [ #[cfg(Py_3_15)] PyABIInfo_VAR!(ABI_INFO); -const SLOTS_LEN: usize = 1 - + if cfg!(Py_3_12) { 1 } else { 0 } - + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 } - + if cfg!(Py_3_15) { 4 } else { 0 }; +const SLOTS_LEN: usize = + 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 4 * (cfg!(Py_3_15) as usize); static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ #[cfg(Py_3_15)] PyModuleDef_Slot { diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index 1812e737b68..6b927c23a4d 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -18,10 +18,8 @@ pub static mut MODULE_DEF: PyModuleDef = PyModuleDef { #[cfg(Py_3_15)] PyABIInfo_VAR!(ABI_INFO); -const SEQUENTIAL_SLOTS_LEN: usize = 2 - + if cfg!(Py_3_12) { 1 } else { 0 } - + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 } - + if cfg!(Py_3_15) { 7 } else { 0 }; +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); pub static mut SEQUENTIAL_SLOTS: [PyModuleDef_Slot; SEQUENTIAL_SLOTS_LEN] = [ #[cfg(Py_3_15)] PyModuleDef_Slot { diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index 7e401b84898..8ff6fb2ad37 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -34,10 +34,8 @@ static mut METHODS: [PyMethodDef; 2] = [ #[cfg(Py_3_15)] PyABIInfo_VAR!(ABI_INFO); -const SLOTS_LEN: usize = 1 - + if cfg!(Py_3_12) { 1 } else { 0 } - + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 } - + if cfg!(Py_3_15) { 4 } else { 0 }; +const SLOTS_LEN: usize = + 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 4 * (cfg!(Py_3_15) as usize); static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ #[cfg(Py_3_15)] PyModuleDef_Slot { diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index e2c589986cd..9a3b1db01e1 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -162,10 +162,8 @@ //! #[cfg(Py_3_15)] //! PyABIInfo_VAR!(ABI_INFO); //! -//! const SLOTS_LEN: usize = 1 -//! + if cfg!(Py_3_12) { 1 } else { 0 } -//! + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 } -//! + if cfg!(Py_3_15) { 4 } else { 0 }; +//! const SLOTS_LEN: usize = +//! 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 4 * (cfg!(Py_3_15) as usize); //! static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ //! #[cfg(Py_3_15)] //! PyModuleDef_Slot { From 2358c1690b2f3f51b715a687b73db8251b98ff84 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 23 Jan 2026 08:28:32 -0700 Subject: [PATCH 12/13] Revert incorrect change to m_init type and add comments --- pyo3-ffi/src/moduleobject.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index eb79a5146bf..88d6ccd71bb 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -55,7 +55,8 @@ extern "C" { #[repr(C)] pub struct PyModuleDef_Base { pub ob_base: PyObject, - pub m_init: *mut PyObject, + // Rust function pointers are non-null so an Option is needed here. + pub m_init: Option *mut PyObject>, pub m_index: Py_ssize_t, pub m_copy: *mut PyObject, } @@ -66,7 +67,7 @@ pub struct PyModuleDef_Base { )] pub const PyModuleDef_HEAD_INIT: PyModuleDef_Base = PyModuleDef_Base { ob_base: PyObject_HEAD_INIT, - m_init: std::ptr::null_mut(), + m_init: None, m_index: 0, m_copy: std::ptr::null_mut(), }; @@ -155,6 +156,7 @@ pub struct PyModuleDef { pub m_size: Py_ssize_t, pub m_methods: *mut PyMethodDef, pub m_slots: *mut PyModuleDef_Slot, + // Rust function pointers are non-null so an Option is needed here. pub m_traverse: Option, pub m_clear: Option, pub m_free: Option, From 1ec713dab2b53e93c7fcd3cdd3ec3b897241cc67 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 23 Jan 2026 10:12:55 -0700 Subject: [PATCH 13/13] Fix typo in release note fragment --- newsfragments/5746.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/5746.added.md b/newsfragments/5746.added.md index 3613d4c82c6..384fa01ef25 100644 --- a/newsfragments/5746.added.md +++ b/newsfragments/5746.added.md @@ -1 +1 @@ -Added FFI wrappers for the PyABIInfo and PyModExport ABIs available in Python 3.15. \ No newline at end of file +Added FFI wrappers for the PyABIInfo and PyModExport APIs available in Python 3.15. \ No newline at end of file