Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eb53c2d
Use PyModExport and PyABIInfo APIs in pymodule implementation
ngoldbaum Jan 26, 2026
e41b509
Add PyModExport function
ngoldbaum Jan 26, 2026
91becaf
DNM: temporarily disable append_to_inittab doctest
ngoldbaum Jan 26, 2026
1020688
fix issues seen on older pythons in CI
ngoldbaum Jan 26, 2026
3afa9ae
fix incorrect ModuleDef setup on 3.15
ngoldbaum Jan 26, 2026
f8d6cae
Expose both the PyInit and PyModExport initialization hooks
ngoldbaum Jan 27, 2026
a590874
fix clippy
ngoldbaum Jan 27, 2026
a96219f
add changelog entry
ngoldbaum Jan 27, 2026
e7ac9c0
try use only slots for both init hooks on 3.15
ngoldbaum Jan 29, 2026
d981be7
Always pass m_name and m_doc, following cpython-gh-144340
ngoldbaum Jan 30, 2026
55b6acd
WIP: opaque pyobject support (without Py_GIL_DISABLED)
ngoldbaum Feb 13, 2026
733aa82
delete debug prints
ngoldbaum Feb 13, 2026
c43061e
WIP: fix segfault
ngoldbaum Feb 13, 2026
3812a64
disable append_to_inittab tests
ngoldbaum Feb 13, 2026
25a65a6
fix clippy
ngoldbaum Feb 13, 2026
4a83024
fix ruff
ngoldbaum Feb 13, 2026
9d0e2ed
implement David's suggestion for pyobject_subclassable_native_type
ngoldbaum Feb 13, 2026
a78b5df
replace skipped test with real test
ngoldbaum Feb 13, 2026
42a73e1
fix check-feature-powerset
ngoldbaum Feb 13, 2026
060c3ca
fix clippy-all
ngoldbaum Feb 13, 2026
c1bd2c7
skip test that depend on struct layout on opaque pyobject builds
ngoldbaum Feb 13, 2026
ba8b09a
Expose PyModuleDef as an opaque pointer on opaque PyObject builds
ngoldbaum Feb 16, 2026
f15a7fc
add comments about location of opaque pointers in CPython headers
ngoldbaum Feb 16, 2026
3fa17d0
fix test_inherited_size
ngoldbaum Feb 16, 2026
1970421
Fix doctest on _Py_OPAQUE_PYOBJECT builds
ngoldbaum Feb 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ abi3-py310 = ["abi3-py311", "pyo3-build-config/abi3-py310", "pyo3-ffi/abi3-py310
abi3-py311 = ["abi3-py312", "pyo3-build-config/abi3-py311", "pyo3-ffi/abi3-py311"]
abi3-py312 = ["abi3-py313", "pyo3-build-config/abi3-py312", "pyo3-ffi/abi3-py312"]
abi3-py313 = ["abi3-py314", "pyo3-build-config/abi3-py313", "pyo3-ffi/abi3-py313"]
abi3-py314 = ["abi3", "pyo3-build-config/abi3-py314", "pyo3-ffi/abi3-py314"]
abi3-py314 = ["abi3-py315", "pyo3-build-config/abi3-py314", "pyo3-ffi/abi3-py314"]
abi3-py315 = ["abi3", "pyo3-build-config/abi3-py315", "pyo3-ffi/abi3-py315"]

# Automatically generates `python3.dll` import libraries for Windows targets.
generate-import-lib = ["pyo3-ffi/generate-import-lib"]
Expand Down
3 changes: 3 additions & 0 deletions guide/src/python-from-rust/calling-existing-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,13 @@ mod foo {
}
}

# #[cfg(not(_Py_OPAQUE_PYOBJECT))]
fn main() -> PyResult<()> {
pyo3::append_to_inittab!(foo);
Python::attach(|py| Python::run(py, c"import foo; foo.add_one(6)", None, None))
}
# #[cfg(_Py_OPAQUE_PYOBJECT)]
# fn main() -> () {}
```

If `append_to_inittab` cannot be used due to constraints in the program, an alternative is to create a module using [`PyModule::new`] and insert it manually into `sys.modules`:
Expand Down
1 change: 1 addition & 0 deletions newsfragments/5753.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Module initialization uses the PyModExport and PyABIInfo APIs on python 3.15 and newer.
10 changes: 6 additions & 4 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,7 @@ def _supported_interpreter_versions(


PY_VERSIONS = _supported_interpreter_versions("cpython")
# We don't yet support abi3-py315 but do support cp315 and cp315t
# version-specific builds
ABI3_PY_VERSIONS = [p for p in PY_VERSIONS if not p.endswith("t")]
ABI3_PY_VERSIONS.remove("3.15")
PYPY_VERSIONS = _supported_interpreter_versions("pypy")


Expand Down Expand Up @@ -124,7 +121,12 @@ def test_rust(session: nox.Session):
# We need to pass the feature set to the test command
# so that it can be used in the test code
# (e.g. for `#[cfg(feature = "abi3-py37")]`)
if feature_set and "abi3" in feature_set and FREE_THREADED_BUILD:
if (
feature_set
and "abi3" in feature_set
and FREE_THREADED_BUILD
and sys.version_info < (3, 15)
):
# free-threaded builds don't support abi3 yet
continue

Expand Down
3 changes: 2 additions & 1 deletion pyo3-build-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ abi3-py310 = ["abi3-py311"]
abi3-py311 = ["abi3-py312"]
abi3-py312 = ["abi3-py313"]
abi3-py313 = ["abi3-py314"]
abi3-py314 = ["abi3"]
abi3-py314 = ["abi3-py315"]
abi3-py315 = ["abi3"]

[package.metadata.docs.rs]
features = ["resolve-config"]
32 changes: 30 additions & 2 deletions pyo3-build-config/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion {
};

/// Maximum Python version that can be used as minimum required Python version with abi3.
pub(crate) const ABI3_MAX_MINOR: u8 = 14;
pub(crate) const ABI3_MAX_MINOR: u8 = 15;

#[cfg(test)]
thread_local! {
Expand Down Expand Up @@ -190,8 +190,11 @@ impl InterpreterConfig {
}

// If Py_GIL_DISABLED is set, do not build with limited API support
if self.abi3 && !self.is_free_threaded() {
if self.abi3 && !(self.is_free_threaded()) {
out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned());
if self.version.minor >= 15 {
out.push("cargo:rustc-cfg=_Py_OPAQUE_PYOBJECT".to_owned());
}
}

for flag in &self.build_flags.0 {
Expand Down Expand Up @@ -3203,6 +3206,31 @@ mod tests {
"cargo:rustc-cfg=Py_LIMITED_API".to_owned(),
]
);

let interpreter_config = InterpreterConfig {
implementation: PythonImplementation::CPython,
version: PythonVersion {
major: 3,
minor: 15,
},
..interpreter_config
};
assert_eq!(
interpreter_config.build_script_outputs(),
[
"cargo:rustc-cfg=Py_3_7".to_owned(),
"cargo:rustc-cfg=Py_3_8".to_owned(),
"cargo:rustc-cfg=Py_3_9".to_owned(),
"cargo:rustc-cfg=Py_3_10".to_owned(),
"cargo:rustc-cfg=Py_3_11".to_owned(),
"cargo:rustc-cfg=Py_3_12".to_owned(),
"cargo:rustc-cfg=Py_3_13".to_owned(),
"cargo:rustc-cfg=Py_3_14".to_owned(),
"cargo:rustc-cfg=Py_3_15".to_owned(),
"cargo:rustc-cfg=Py_LIMITED_API".to_owned(),
"cargo:rustc-cfg=_Py_OPAQUE_PYOBJECT".to_owned(),
]
);
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions pyo3-build-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ pub fn print_expected_cfgs() {

println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)");
println!("cargo:rustc-check-cfg=cfg(Py_GIL_DISABLED)");
println!("cargo:rustc-check-cfg=cfg(_Py_OPAQUE_PYOBJECT)");
println!("cargo:rustc-check-cfg=cfg(PyPy)");
println!("cargo:rustc-check-cfg=cfg(GraalPy)");
println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))");
Expand Down
3 changes: 2 additions & 1 deletion pyo3-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ abi3-py310 = ["abi3-py311", "pyo3-build-config/abi3-py310"]
abi3-py311 = ["abi3-py312", "pyo3-build-config/abi3-py311"]
abi3-py312 = ["abi3-py313", "pyo3-build-config/abi3-py312"]
abi3-py313 = ["abi3-py314", "pyo3-build-config/abi3-py313"]
abi3-py314 = ["abi3", "pyo3-build-config/abi3-py314"]
abi3-py314 = ["abi3-py315", "pyo3-build-config/abi3-py314"]
abi3-py315 = ["abi3", "pyo3-build-config/abi3-py315"]

# Automatically generates `python3.dll` import libraries for Windows targets.
generate-import-lib = ["pyo3-build-config/generate-import-lib"]
Expand Down
8 changes: 8 additions & 0 deletions pyo3-ffi/src/moduleobject.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#[cfg(not(_Py_OPAQUE_PYOBJECT))]
use crate::methodobject::PyMethodDef;
use crate::object::*;
use crate::pyport::Py_ssize_t;
Expand Down Expand Up @@ -52,6 +53,7 @@ extern "C" {
pub static mut PyModuleDef_Type: PyTypeObject;
}

#[cfg(not(_Py_OPAQUE_PYOBJECT))]
#[repr(C)]
pub struct PyModuleDef_Base {
pub ob_base: PyObject,
Expand All @@ -61,6 +63,7 @@ pub struct PyModuleDef_Base {
pub m_copy: *mut PyObject,
}

#[cfg(not(_Py_OPAQUE_PYOBJECT))]
#[allow(
clippy::declare_interior_mutable_const,
reason = "contains atomic refcount on free-threaded builds"
Expand Down Expand Up @@ -151,6 +154,7 @@ extern "C" {
pub fn PyModule_GetToken(module: *mut PyObject, result: *mut *mut c_void) -> c_int;
}

#[cfg(not(_Py_OPAQUE_PYOBJECT))]
#[repr(C)]
pub struct PyModuleDef {
pub m_base: PyModuleDef_Base,
Expand All @@ -164,3 +168,7 @@ pub struct PyModuleDef {
pub m_clear: Option<inquiry>,
pub m_free: Option<freefunc>,
}

// from pytypedefs.h
#[cfg(_Py_OPAQUE_PYOBJECT)]
opaque_struct!(pub PyModuleDef);
27 changes: 27 additions & 0 deletions pyo3-ffi/src/object.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use crate::pyport::{Py_hash_t, Py_ssize_t};
#[cfg(not(_Py_OPAQUE_PYOBJECT))]
#[cfg(Py_GIL_DISABLED)]
use crate::refcount;
#[cfg(Py_GIL_DISABLED)]
use crate::PyMutex;
use std::ffi::{c_char, c_int, c_uint, c_ulong, c_void};
use std::mem;
use std::ptr;
#[cfg(not(_Py_OPAQUE_PYOBJECT))]
#[cfg(Py_GIL_DISABLED)]
use std::sync::atomic::{AtomicIsize, AtomicU32};

// from pytypedefs.h
#[cfg(Py_LIMITED_API)]
opaque_struct!(pub PyTypeObject);

Expand Down Expand Up @@ -92,6 +95,7 @@ const _PyObject_MIN_ALIGNMENT: usize = 4;
// not currently possible to use constant variables with repr(align()), see
// https://github.com/rust-lang/rust/issues/52840

#[cfg(not(_Py_OPAQUE_PYOBJECT))]
#[cfg_attr(not(all(Py_3_15, Py_GIL_DISABLED)), repr(C))]
#[cfg_attr(all(Py_3_15, Py_GIL_DISABLED), repr(C, align(4)))]
#[derive(Debug)]
Expand Down Expand Up @@ -121,8 +125,10 @@ pub struct PyObject {
pub ob_type: *mut PyTypeObject,
}

#[cfg(not(_Py_OPAQUE_PYOBJECT))]
const _: () = assert!(std::mem::align_of::<PyObject>() >= _PyObject_MIN_ALIGNMENT);

#[cfg(not(_Py_OPAQUE_PYOBJECT))]
#[allow(
clippy::declare_interior_mutable_const,
reason = "contains atomic refcount on free-threaded builds"
Expand Down Expand Up @@ -157,10 +163,15 @@ pub const PyObject_HEAD_INIT: PyObject = PyObject {
ob_type: std::ptr::null_mut(),
};

// from pytypedefs.h
#[cfg(_Py_OPAQUE_PYOBJECT)]
opaque_struct!(pub PyObject);

// skipped _Py_UNOWNED_TID

// skipped _PyObject_CAST

#[cfg(not(_Py_OPAQUE_PYOBJECT))]
#[repr(C)]
#[derive(Debug)]
pub struct PyVarObject {
Expand All @@ -172,6 +183,10 @@ pub struct PyVarObject {
pub _ob_size_graalpy: Py_ssize_t,
}

// from pytypedefs.h
#[cfg(_Py_OPAQUE_PYOBJECT)]
opaque_struct!(pub PyVarObject);

// skipped private _PyVarObject_CAST

#[inline]
Expand Down Expand Up @@ -219,6 +234,16 @@ extern "C" {
pub fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject;
}

#[cfg_attr(windows, link(name = "pythonXY"))]
#[cfg(all(Py_LIMITED_API, Py_3_15))]
extern "C" {
#[cfg_attr(PyPy, link_name = "PyPy_SIZE")]
pub fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t;
#[cfg_attr(PyPy, link_name = "PyPy_IS_TYPE")]
pub fn Py_IS_TYPE(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int;
// skipped Py_SET_SIZE
}

// skip _Py_TYPE compat shim

#[cfg_attr(windows, link(name = "pythonXY"))]
Expand All @@ -229,6 +254,7 @@ extern "C" {
pub static mut PyBool_Type: PyTypeObject;
}

#[cfg(not(all(Py_LIMITED_API, Py_3_15)))]
#[inline]
pub unsafe fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t {
#[cfg(not(GraalPy))]
Expand All @@ -241,6 +267,7 @@ pub unsafe fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t {
_Py_SIZE(ob)
}

#[cfg(not(all(Py_LIMITED_API, Py_3_15)))]
#[inline]
pub unsafe fn Py_IS_TYPE(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int {
(Py_TYPE(ob) == tp) as c_int
Expand Down
3 changes: 2 additions & 1 deletion pyo3-ffi/src/refcount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::ffi::c_uint;
#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
use std::ffi::c_ulong;
use std::ptr;
#[cfg(Py_GIL_DISABLED)]
#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))]
use std::sync::atomic::Ordering::Relaxed;

#[cfg(all(Py_3_14, not(Py_3_15)))]
Expand Down Expand Up @@ -116,6 +116,7 @@ pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t {
}
}

#[cfg(not(_Py_OPAQUE_PYOBJECT))]
#[cfg(Py_3_12)]
#[inline(always)]
unsafe fn _Py_IsImmortal(op: *mut PyObject) -> c_int {
Expand Down
20 changes: 18 additions & 2 deletions pyo3-macros-backend/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ fn module_initialization(
) -> Result<TokenStream> {
let Ctx { pyo3_path, .. } = ctx;
let pyinit_symbol = format!("PyInit_{name}");
let pymodexport_symbol = format!("PyModExport_{name}");
let pyo3_name = LitCStr::new(&CString::new(full_name).unwrap(), Span::call_site());
let doc = if let Some(doc) = doc {
doc.to_cstr_stream(ctx)?
Expand All @@ -544,24 +545,39 @@ fn module_initialization(
#pyo3_path::impl_::trampoline::module_exec(module, #module_exec)
}

static SLOTS: impl_::PyModuleSlots<4> = impl_::PyModuleSlotsBuilder::new()
// The full slots, used for the PyModExport initializaiton
static SLOTS: impl_::PyModuleSlots = impl_::PyModuleSlotsBuilder::new()
.with_mod_exec(__pyo3_module_exec)
.with_gil_used(#gil_used)
.with_name(__PYO3_NAME)
.with_doc(#doc)
.build();

// Since the macros need to be written agnostic to the Python version
// we need to explicitly pass the name and docstring for PyModuleDef
// initializaiton.
impl_::ModuleDef::new(__PYO3_NAME, #doc, &SLOTS)
};
};
if !is_submodule {
result.extend(quote! {
/// This autogenerated function is called by the python interpreter when importing
/// the module.
/// the module on Python 3.14 and older.
#[doc(hidden)]
#[export_name = #pyinit_symbol]
pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject {
_PYO3_DEF.init_multi_phase()
}
});
result.extend(quote! {
/// 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()
}
});
}
Ok(result)
}
Expand Down
17 changes: 10 additions & 7 deletions src/impl_/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1458,6 +1458,7 @@ pub trait ExtractPyClassWithClone {}
#[cfg(test)]
#[cfg(feature = "macros")]
mod tests {
#[cfg(not(_Py_OPAQUE_PYOBJECT))]
use crate::pycell::impl_::PyClassObjectContents;

use super::*;
Expand Down Expand Up @@ -1486,17 +1487,19 @@ mod tests {
Some(PyMethodDefType::StructMember(member)) => {
assert_eq!(unsafe { CStr::from_ptr(member.name) }, c"value");
assert_eq!(member.type_code, ffi::Py_T_OBJECT_EX);
#[cfg(not(_Py_OPAQUE_PYOBJECT))]
#[repr(C)]
struct ExpectedLayout {
ob_base: ffi::PyObject,
contents: PyClassObjectContents<FrozenClass>,
}
#[cfg(not(_Py_OPAQUE_PYOBJECT))]
assert_eq!(
member.offset,
(offset_of!(ExpectedLayout, contents) + offset_of!(FrozenClass, value))
as ffi::Py_ssize_t
);
assert_eq!(member.flags, ffi::Py_READONLY);
assert_eq!(member.flags & ffi::Py_READONLY, ffi::Py_READONLY);
}
_ => panic!("Expected a StructMember"),
}
Expand Down Expand Up @@ -1608,17 +1611,17 @@ mod tests {
// SAFETY: def.doc originated from a CStr
assert_eq!(unsafe { CStr::from_ptr(def.doc) }, c"My field doc");
assert_eq!(def.type_code, ffi::Py_T_OBJECT_EX);
#[allow(irrefutable_let_patterns)]
let PyObjectOffset::Absolute(contents_offset) =
<MyClass as PyClassImpl>::Layout::CONTENTS_OFFSET
else {
panic!()
#[allow(clippy::infallible_destructuring_match)]
let contents_offset = match <MyClass as PyClassImpl>::Layout::CONTENTS_OFFSET {
PyObjectOffset::Absolute(contents_offset) => contents_offset,
#[cfg(Py_3_12)]
PyObjectOffset::Relative(contents_offset) => contents_offset,
};
assert_eq!(
def.offset,
contents_offset + FIELD_OFFSET as ffi::Py_ssize_t
);
assert_eq!(def.flags, ffi::Py_READONLY);
assert_eq!(def.flags & ffi::Py_READONLY, ffi::Py_READONLY);
}

#[test]
Expand Down
Loading
Loading