Skip to content
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
82 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
57c2045
Merge branch 'main' into opaque-pyobject
ngoldbaum Mar 3, 2026
f80849e
fix build error on non-opaque builds
ngoldbaum Mar 3, 2026
c0805a9
mark BaseWithoutData as subclassable
ngoldbaum Mar 3, 2026
719cef5
relax assert for Windows
ngoldbaum Mar 3, 2026
072ef0a
Make PyAny a PyVarObject only on the opaque PyObject build
ngoldbaum Mar 4, 2026
0b743b6
Merge branch 'main' into opaque-pyobject
ngoldbaum Mar 24, 2026
264307f
fix merge conflict resolution error
ngoldbaum Mar 24, 2026
ed48e75
fix buggy assertion
ngoldbaum Mar 24, 2026
b4138c4
Expose critical section in the limited API starting in Python 3.15
ngoldbaum Mar 24, 2026
f3ee6af
expose critical section API in limited API
ngoldbaum Mar 24, 2026
ba47f50
disable warning in pyo3-ffi build script on sufficiently new Pythons
ngoldbaum Mar 25, 2026
e52cb59
fixup features
ngoldbaum Mar 26, 2026
3cf60b1
Add missing error handling for `PyModule_FromSlotsAndSpec`
ngoldbaum Mar 26, 2026
607e4b0
Merge branch 'main' into expose-critical-section
ngoldbaum Mar 26, 2026
449eb8a
passes cargo tests with the abi3t feature enabled
ngoldbaum Mar 26, 2026
5371fd8
passes unit tests on GIL-enabled build with abi3t feature
ngoldbaum Mar 26, 2026
19d78d2
fix merge mistake
ngoldbaum Mar 26, 2026
14bf4d1
rustfmt
ngoldbaum Mar 27, 2026
91a0419
fix FIXME
ngoldbaum Mar 27, 2026
285e79c
Merge branch 'main' into opaque-pyobject
ngoldbaum Apr 3, 2026
9f23720
replace extern "C" with extern_libpython!
ngoldbaum Apr 3, 2026
d38c274
fix test
ngoldbaum Apr 3, 2026
e2d8f8c
Merge branch 'opaque-pyobject' into expose-critical-section
ngoldbaum Apr 6, 2026
15ff21c
fix merge error
ngoldbaum Apr 6, 2026
930e1a9
Allow abi3t builds without critical section bindings
ngoldbaum Apr 6, 2026
a609f8c
add FIXME
ngoldbaum Apr 6, 2026
dd5b3a9
remove default feature
ngoldbaum Apr 6, 2026
7419b86
fix syntax error in noxfile
ngoldbaum Apr 6, 2026
603aaf9
fix issues spotted by clippy
ngoldbaum Apr 6, 2026
c9b9157
bring over refactoring from PyO3 PR
ngoldbaum Apr 7, 2026
7560348
working!
ngoldbaum Apr 7, 2026
becc8af
Fix memory leak of iterator
ngoldbaum Apr 8, 2026
b734e77
fix size hints
ngoldbaum Apr 8, 2026
a6c8710
delete debug statement
ngoldbaum Apr 8, 2026
a827be9
Use Py_TARGET_ABI3T
ngoldbaum Apr 8, 2026
0ceda48
run formatter
ngoldbaum Apr 8, 2026
d1ef669
fix check-feature-powerset
ngoldbaum Apr 8, 2026
26c7dc0
increment SUPPORTED_VERSIONS_CPYTHON.max
ngoldbaum Apr 8, 2026
95ca7d2
fix conditional compilation for critical section API
ngoldbaum Apr 8, 2026
b596885
fix ruff
ngoldbaum Apr 8, 2026
22a5332
fix incorrect conditional compilation guargs
ngoldbaum Apr 8, 2026
ec1269a
fix noxfile and ban abi3t builds on 3.14 and older
ngoldbaum Apr 8, 2026
04eb8bb
ruff format
ngoldbaum Apr 8, 2026
101949c
attempt to fix semver-checks
ngoldbaum Apr 8, 2026
828afdd
fix test-version-limits
ngoldbaum Apr 8, 2026
cd78153
fix issues spotted by claude
ngoldbaum Apr 8, 2026
5e5671e
strip 't' for version parsing
ngoldbaum Apr 8, 2026
266527f
Adjust comments and error messages
ngoldbaum Apr 8, 2026
294a4f0
fix compiler warning
ngoldbaum Apr 8, 2026
b37445d
more noxfile fixes
ngoldbaum Apr 8, 2026
a2584f5
fix ruff
ngoldbaum Apr 8, 2026
1b5ec7a
use a 3.15 interpreter to run the feature-powerset tests
ngoldbaum Apr 8, 2026
553ef54
fix a few more issues caught by claude
ngoldbaum Apr 8, 2026
2f755ae
add comment
ngoldbaum Apr 8, 2026
f326c74
Merge branch 'main' into opaque-pyobject
ngoldbaum Apr 15, 2026
710e835
use correct windows library name for abi3t
ngoldbaum Apr 16, 2026
861edd8
try to fix linking
ngoldbaum Apr 16, 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"]

# deprecated: no longer needed, raw-dylib is used instead
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
10 changes: 6 additions & 4 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,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 @@ -126,7 +123,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 @@ -39,7 +39,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 @@ -44,7 +44,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 @@ -194,8 +194,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 @@ -3151,6 +3154,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 @@ -264,6 +264,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"]

# deprecated: no longer needed, raw-dylib is used instead
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 @@ -49,6 +50,7 @@ extern_libpython! {
pub static mut PyModuleDef_Type: PyTypeObject;
}

#[cfg(not(_Py_OPAQUE_PYOBJECT))]
#[repr(C)]
pub struct PyModuleDef_Base {
pub ob_base: PyObject,
Expand All @@ -58,6 +60,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 @@ -148,6 +151,7 @@ extern_libpython! {
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 @@ -161,3 +165,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,13 +1,16 @@
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;
#[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 @@ -91,6 +94,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 All @@ -116,8 +120,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 @@ -148,10 +154,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 @@ -163,6 +174,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 @@ -209,6 +224,16 @@ extern_libpython! {
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" {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to replace this extern "C" block with extern_libpython! - should fix the windows link issues.

(Consequence of #5866)

#[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

extern_libpython! {
Expand All @@ -218,6 +243,7 @@ extern_libpython! {
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 @@ -230,6 +256,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
17 changes: 10 additions & 7 deletions src/impl_/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1455,6 +1455,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 @@ -1483,17 +1484,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 @@ -1605,17 +1608,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