Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions guide/src/python-from-rust/calling-existing-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ 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))
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
2 changes: 2 additions & 0 deletions pyo3-build-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use target_lexicon::OperatingSystem;
/// | ---- | ----------- |
/// | `#[cfg(Py_3_7)]`, `#[cfg(Py_3_8)]`, `#[cfg(Py_3_9)]`, `#[cfg(Py_3_10)]` | These attributes mark code only for a given Python version and up. For example, `#[cfg(Py_3_7)]` marks code which can run on Python 3.7 **and newer**. |
/// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. |
/// | `#[cfg(Py_GIL_DISABLED)]` | This marks code which is run on the free-threaded interpreter. |
/// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. |
/// | `#[cfg(GraalPy)]` | This marks code which is run when compiling for GraalPy. |
///
Expand Down Expand Up @@ -253,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
1 change: 1 addition & 0 deletions pyo3-ffi/src/moduleobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,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
24 changes: 24 additions & 0 deletions pyo3-ffi/src/object.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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};

Expand Down Expand Up @@ -92,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 Down Expand Up @@ -121,8 +124,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 +162,14 @@ pub const PyObject_HEAD_INIT: PyObject = PyObject {
ob_type: std::ptr::null_mut(),
};

#[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 +181,9 @@ pub struct PyVarObject {
pub _ob_size_graalpy: Py_ssize_t,
}

#[cfg(_Py_OPAQUE_PYOBJECT)]
opaque_struct!(pub PyVarObject);

// skipped private _PyVarObject_CAST

#[inline]
Expand Down Expand Up @@ -219,6 +231,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 +251,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 +264,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
5 changes: 3 additions & 2 deletions pyo3-ffi/src/refcount.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::pyport::Py_ssize_t;
use crate::PyObject;
#[cfg(py_sys_config = "Py_REF_DEBUG")]
#[cfg(all(not(Py_LIMITED_API), py_sys_config = "Py_REF_DEBUG"))]
use std::ffi::c_char;
#[cfg(Py_3_12)]
use std::ffi::c_int;
Expand All @@ -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 @@ -511,6 +511,7 @@ fn module_initialization(
) -> 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 mut result = quote! {
Expand All @@ -531,24 +532,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()
}
});
}
result
}
Expand Down
14 changes: 7 additions & 7 deletions src/impl_/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1484,7 +1484,7 @@ mod tests {
(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 @@ -1593,17 +1593,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