From d1cf4cbe832b3def86213a56a40e5d7bfca0b356 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 1 Oct 2024 16:36:08 -0600 Subject: [PATCH 01/34] WIP: declare free-threaded support in pymodule macro --- noxfile.py | 8 ------- pyo3-ffi/build.rs | 8 ------- pyo3-ffi/src/moduleobject.rs | 22 +++++++++++++---- pyo3-macros-backend/src/attributes.rs | 4 +++- pyo3-macros-backend/src/module.rs | 34 +++++++++++++++++++++++---- pytests/src/free_threaded_mod.rs | 12 ++++++++++ pytests/src/lib.rs | 2 ++ pytests/tests/test_free_threaded.py | 1 + src/impl_/pymodule.rs | 26 ++++++++++++++++---- src/macros.rs | 3 ++- src/types/module.rs | 1 + 11 files changed, 89 insertions(+), 32 deletions(-) create mode 100644 pytests/src/free_threaded_mod.rs create mode 100644 pytests/tests/test_free_threaded.py diff --git a/noxfile.py b/noxfile.py index 32176240f59..25cb8d1eb92 100644 --- a/noxfile.py +++ b/noxfile.py @@ -676,14 +676,6 @@ def test_version_limits(session: nox.Session): config_file.set("PyPy", "3.11") _run_cargo(session, "check", env=env, expect_error=True) - # Python build with GIL disabled should fail building - config_file.set("CPython", "3.13", build_flags=["Py_GIL_DISABLED"]) - _run_cargo(session, "check", env=env, expect_error=True) - - # Python build with GIL disabled should pass with env flag on - env["UNSAFE_PYO3_BUILD_FREE_THREADED"] = "1" - _run_cargo(session, "check", env=env) - @nox.session(name="check-feature-powerset", venv_backend="none") def check_feature_powerset(session: nox.Session): diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 622c2707110..64f951723ba 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -127,14 +127,6 @@ fn ensure_gil_enabled(interpreter_config: &InterpreterConfig) -> Result<()> { .0 .contains(&BuildFlag::Py_GIL_DISABLED) .not(); - ensure!( - gil_enabled || std::env::var("UNSAFE_PYO3_BUILD_FREE_THREADED").map_or(false, |os_str| os_str == "1"), - "the Python interpreter was built with the GIL disabled, which is not yet supported by PyO3\n\ - = help: see https://github.com/PyO3/pyo3/issues/4265 for more information\n\ - = help: please check if an updated version of PyO3 is available. Current version: {}\n\ - = help: set UNSAFE_PYO3_BUILD_FREE_THREADED=1 to suppress this check and build anyway for free-threaded Python", - std::env::var("CARGO_PKG_VERSION").unwrap() - ); if !gil_enabled && interpreter_config.abi3 { warn!( "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index ff6458f4b15..e5d54631c2e 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -86,17 +86,29 @@ 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)] +#[cfg(all(not(Py_LIMITED_API), Py_3_12))] pub const Py_mod_multiple_interpreters: c_int = 3; +#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +pub const Py_mod_gil: c_int = 4; -#[cfg(Py_3_12)] +// skipped private _Py_mod_LAST_SLOT + +#[cfg(all(not(Py_LIMITED_API), Py_3_12))] pub const Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED: *mut c_void = 0 as *mut c_void; -#[cfg(Py_3_12)] +#[cfg(all(not(Py_LIMITED_API), Py_3_12))] pub const Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED: *mut c_void = 1 as *mut c_void; -#[cfg(Py_3_12)] +#[cfg(all(not(Py_LIMITED_API), Py_3_12))] pub const Py_MOD_PER_INTERPRETER_GIL_SUPPORTED: *mut c_void = 2 as *mut c_void; -// skipped non-limited _Py_mod_LAST_SLOT +#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +pub const Py_MOD_GIL_USED: *mut c_void = 0 as *mut c_void; +#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +pub const Py_MOD_GIL_NOT_USED: *mut c_void = 1 as *mut c_void; + +#[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] +extern "C" { + pub fn PyUnstable_Module_SetGIL(module: *mut PyObject, gil: *mut c_void); +} #[repr(C)] pub struct PyModuleDef { diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 94526e7dafc..1c9c5b1ec53 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -7,7 +7,7 @@ use syn::{ punctuated::Punctuated, spanned::Spanned, token::Comma, - Attribute, Expr, ExprPath, Ident, Index, LitStr, Member, Path, Result, Token, + Attribute, Expr, ExprPath, Ident, Index, LitBool, LitStr, Member, Path, Result, Token, }; pub mod kw { @@ -44,6 +44,7 @@ pub mod kw { syn::custom_keyword!(transparent); syn::custom_keyword!(unsendable); syn::custom_keyword!(weakref); + syn::custom_keyword!(supports_free_threaded); } fn take_int(read: &mut &str, tracker: &mut usize) -> String { @@ -308,6 +309,7 @@ pub type RenameAllAttribute = KeywordAttribute; pub type TextSignatureAttribute = KeywordAttribute; pub type SubmoduleAttribute = kw::submodule; +pub type FreeThreadedAttribute = KeywordAttribute; impl Parse for KeywordAttribute { fn parse(input: ParseStream<'_>) -> Result { diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 7d2c72dbdfb..9537d66ac41 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -2,8 +2,8 @@ use crate::{ attributes::{ - self, kw, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute, - NameAttribute, SubmoduleAttribute, + self, kw, take_attributes, take_pyo3_options, CrateAttribute, FreeThreadedAttribute, + ModuleAttribute, NameAttribute, SubmoduleAttribute, }, get_doc, pyclass::PyClassPyO3Option, @@ -29,6 +29,7 @@ pub struct PyModuleOptions { name: Option, module: Option, submodule: Option, + supports_free_threaded: Option, } impl Parse for PyModuleOptions { @@ -72,6 +73,9 @@ impl PyModuleOptions { submodule, " (it is implicitly always specified for nested modules)" ), + PyModulePyO3Option::SupportsFreeThreaded(supports_free_threaded) => { + set_option!(supports_free_threaded) + } } } Ok(()) @@ -344,7 +348,15 @@ pub fn pymodule_module_impl( ) } }}; - let initialization = module_initialization(&name, ctx, module_def, options.submodule.is_some()); + let initialization = module_initialization( + &name, + ctx, + module_def, + options.submodule.is_some(), + options + .supports_free_threaded + .is_some_and(|op| op.value.value), + ); Ok(quote!( #(#attrs)* @@ -383,7 +395,15 @@ pub fn pymodule_function_impl( let vis = &function.vis; let doc = get_doc(&function.attrs, None, ctx); - let initialization = module_initialization(&name, ctx, quote! { MakeDef::make_def() }, false); + let initialization = module_initialization( + &name, + ctx, + quote! { MakeDef::make_def() }, + false, + options + .supports_free_threaded + .is_some_and(|op| op.value.value), + ); // Module function called with optional Python<'_> marker as first arg, followed by the module. let mut module_args = Vec::new(); @@ -428,6 +448,7 @@ fn module_initialization( ctx: &Ctx, module_def: TokenStream, is_submodule: bool, + supports_free_threaded: bool, ) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let pyinit_symbol = format!("PyInit_{}", name); @@ -449,7 +470,7 @@ fn module_initialization( #[doc(hidden)] #[export_name = #pyinit_symbol] pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject { - #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py)) + #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py, #supports_free_threaded)) } }); } @@ -596,6 +617,7 @@ enum PyModulePyO3Option { Crate(CrateAttribute), Name(NameAttribute), Module(ModuleAttribute), + SupportsFreeThreaded(FreeThreadedAttribute), } impl Parse for PyModulePyO3Option { @@ -609,6 +631,8 @@ impl Parse for PyModulePyO3Option { input.parse().map(PyModulePyO3Option::Module) } else if lookahead.peek(attributes::kw::submodule) { input.parse().map(PyModulePyO3Option::Submodule) + } else if lookahead.peek(attributes::kw::supports_free_threaded) { + input.parse().map(PyModulePyO3Option::SupportsFreeThreaded) } else { Err(lookahead.error()) } diff --git a/pytests/src/free_threaded_mod.rs b/pytests/src/free_threaded_mod.rs new file mode 100644 index 00000000000..7bf9763db14 --- /dev/null +++ b/pytests/src/free_threaded_mod.rs @@ -0,0 +1,12 @@ +use pyo3::prelude::*; + +#[pyfunction] +fn add_two(x: usize) -> usize { + x + 2 +} + +#[pymodule] +pub fn free_threaded_mod(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(add_two, m)?)?; + Ok(()) +} diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index 72f5feaa0f4..32c4b329d75 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -8,6 +8,7 @@ pub mod comparisons; pub mod datetime; pub mod dict_iter; pub mod enums; +pub mod free_threaded_mod; pub mod misc; pub mod objstore; pub mod othermod; @@ -35,6 +36,7 @@ fn pyo3_pytests(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(pyfunctions::pyfunctions))?; m.add_wrapped(wrap_pymodule!(sequence::sequence))?; m.add_wrapped(wrap_pymodule!(subclassing::subclassing))?; + m.add_wrapped(wrap_pymodule!(free_threaded_mod::free_threaded_mod))?; // Inserting to sys.modules allows importing submodules nicely from Python // e.g. import pyo3_pytests.buf_and_str as bas diff --git a/pytests/tests/test_free_threaded.py b/pytests/tests/test_free_threaded.py new file mode 100644 index 00000000000..584a739a98c --- /dev/null +++ b/pytests/tests/test_free_threaded.py @@ -0,0 +1 @@ +from pyo3_pytests import free_threaded_mod diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index da17fe4bbdc..92949a30108 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -15,7 +15,7 @@ use portable_atomic::{AtomicI64, Ordering}; not(all(windows, Py_LIMITED_API, not(Py_3_10))), target_has_atomic = "64", ))] -use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; #[cfg(not(any(PyPy, GraalPy)))] use crate::exceptions::PyImportError; @@ -41,6 +41,8 @@ pub struct ModuleDef { interpreter: AtomicI64, /// Initialized module object, cached to avoid reinitialization. module: GILOnceCell>, + /// Whether or not the module supports running without the GIL + supports_free_threaded: AtomicBool, } /// Wrapper to enable initializer to be used in const fns. @@ -85,10 +87,15 @@ impl ModuleDef { ))] interpreter: AtomicI64::new(-1), module: GILOnceCell::new(), + supports_free_threaded: AtomicBool::new(false), } } /// Builds a module using user given initializer. Used for [`#[pymodule]`][crate::pymodule]. - pub fn make_module(&'static self, py: Python<'_>) -> PyResult> { + pub fn make_module( + &'static self, + py: Python<'_>, + supports_free_threaded: bool, + ) -> PyResult> { // Check the interpreter ID has not changed, since we currently have no way to guarantee // that static data is not reused across interpreters. // @@ -134,6 +141,11 @@ impl ModuleDef { ffi::PyModule_Create(self.ffi_def.get()), )? }; + if supports_free_threaded { + unsafe { + ffi::PyUnstable_Module_SetGIL(module.as_ptr(), ffi::Py_MOD_GIL_NOT_USED) + }; + } self.initializer.0(module.bind(py))?; Ok(module) }) @@ -190,7 +202,13 @@ impl PyAddToModule for PyMethodDef { /// For adding a module to a module. impl PyAddToModule for ModuleDef { fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { - module.add_submodule(self.make_module(module.py())?.bind(module.py())) + module.add_submodule( + self.make_module( + module.py(), + self.supports_free_threaded.load(Ordering::Relaxed), + )? + .bind(module.py()), + ) } } @@ -223,7 +241,7 @@ mod tests { ) }; Python::with_gil(|py| { - let module = MODULE_DEF.make_module(py).unwrap().into_bound(py); + let module = MODULE_DEF.make_module(py, false).unwrap().into_bound(py); assert_eq!( module .getattr("__name__") diff --git a/src/macros.rs b/src/macros.rs index d2fa6f31ada..2cd00f79aa6 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -176,7 +176,8 @@ macro_rules! wrap_pymodule { &|py| { use $module as wrapped_pymodule; wrapped_pymodule::_PYO3_DEF - .make_module(py) + // corrected in add_wrapped later based on the parent module's settings + .make_module(py, false) .expect("failed to wrap pymodule") } }; diff --git a/src/types/module.rs b/src/types/module.rs index f3490385721..89e00e0874f 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -494,6 +494,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { T: IntoPyCallbackOutput<'py, PyObject>, { fn inner(module: &Bound<'_, PyModule>, object: Bound<'_, PyAny>) -> PyResult<()> { + if object.is_instance_of::() {} let name = object.getattr(__name__(module.py()))?; module.add(name.downcast_into::()?, object) } From 70abf673941ee74590e21c3ce60df1db6ab88d83 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 10:47:29 -0600 Subject: [PATCH 02/34] ignore ruff lint about unused import --- pytests/tests/test_free_threaded.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytests/tests/test_free_threaded.py b/pytests/tests/test_free_threaded.py index 584a739a98c..e3bd2a1c639 100644 --- a/pytests/tests/test_free_threaded.py +++ b/pytests/tests/test_free_threaded.py @@ -1 +1 @@ -from pyo3_pytests import free_threaded_mod +from pyo3_pytests import free_threaded_mod # NOQA From 1ba9806df74bdd39ab9b548e30e8c4bbc0b29e12 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 12:39:34 -0600 Subject: [PATCH 03/34] eliminate gil re-enabling in pytests --- pytests/src/awaitable.rs | 2 +- pytests/src/buf_and_str.rs | 2 +- pytests/src/comparisons.rs | 2 +- pytests/src/datetime.rs | 2 +- pytests/src/enums.rs | 2 +- pytests/src/free_threaded_mod.rs | 2 +- pytests/src/lib.rs | 2 +- pytests/src/misc.rs | 2 +- pytests/src/objstore.rs | 2 +- pytests/src/othermod.rs | 2 +- pytests/src/path.rs | 2 +- pytests/src/pyclasses.rs | 2 +- pytests/src/pyfunctions.rs | 2 +- pytests/src/sequence.rs | 2 +- pytests/src/subclassing.rs | 2 +- pytests/tests/test_free_threaded.py | 11 +++++++++++ 16 files changed, 26 insertions(+), 15 deletions(-) diff --git a/pytests/src/awaitable.rs b/pytests/src/awaitable.rs index 5e3b98e14ea..4dd0313c56c 100644 --- a/pytests/src/awaitable.rs +++ b/pytests/src/awaitable.rs @@ -78,7 +78,7 @@ impl FutureAwaitable { } } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn awaitable(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/buf_and_str.rs b/pytests/src/buf_and_str.rs index bbaad40f312..3442c121a78 100644 --- a/pytests/src/buf_and_str.rs +++ b/pytests/src/buf_and_str.rs @@ -47,7 +47,7 @@ fn return_memoryview(py: Python<'_>) -> PyResult> { PyMemoryView::from(&bytes) } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn buf_and_str(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(return_memoryview, m)?)?; diff --git a/pytests/src/comparisons.rs b/pytests/src/comparisons.rs index 5c7f659c9b3..8cc1b728a5e 100644 --- a/pytests/src/comparisons.rs +++ b/pytests/src/comparisons.rs @@ -112,7 +112,7 @@ impl OrderedDefaultNe { } } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn comparisons(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/datetime.rs b/pytests/src/datetime.rs index 3bdf103b62e..1fc8beb42e8 100644 --- a/pytests/src/datetime.rs +++ b/pytests/src/datetime.rs @@ -203,7 +203,7 @@ impl TzClass { } } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn datetime(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(make_date, m)?)?; m.add_function(wrap_pyfunction!(get_date_tuple, m)?)?; diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs index fb96c0a9366..c18a839deb1 100644 --- a/pytests/src/enums.rs +++ b/pytests/src/enums.rs @@ -4,7 +4,7 @@ use pyo3::{ wrap_pyfunction, Bound, PyResult, }; -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn enums(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/free_threaded_mod.rs b/pytests/src/free_threaded_mod.rs index 7bf9763db14..5ef7715775b 100644 --- a/pytests/src/free_threaded_mod.rs +++ b/pytests/src/free_threaded_mod.rs @@ -5,7 +5,7 @@ fn add_two(x: usize) -> usize { x + 2 } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn free_threaded_mod(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(add_two, m)?)?; Ok(()) diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index 32c4b329d75..736d870f6cf 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -18,7 +18,7 @@ pub mod pyfunctions; pub mod sequence; pub mod subclassing; -#[pymodule] +#[pymodule(supports_free_threaded = true)] fn pyo3_pytests(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(awaitable::awaitable))?; #[cfg(not(Py_LIMITED_API))] diff --git a/pytests/src/misc.rs b/pytests/src/misc.rs index ed9c9333ec2..bc42ee81db8 100644 --- a/pytests/src/misc.rs +++ b/pytests/src/misc.rs @@ -32,7 +32,7 @@ fn get_item_and_run_callback(dict: Bound<'_, PyDict>, callback: Bound<'_, PyAny> Ok(()) } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn misc(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(issue_219, m)?)?; m.add_function(wrap_pyfunction!(get_type_fully_qualified_name, m)?)?; diff --git a/pytests/src/objstore.rs b/pytests/src/objstore.rs index 844cee946ad..acf288e36b0 100644 --- a/pytests/src/objstore.rs +++ b/pytests/src/objstore.rs @@ -18,7 +18,7 @@ impl ObjStore { } } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn objstore(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::() } diff --git a/pytests/src/othermod.rs b/pytests/src/othermod.rs index 36ad4b5e23e..394e9e013da 100644 --- a/pytests/src/othermod.rs +++ b/pytests/src/othermod.rs @@ -28,7 +28,7 @@ fn double(x: i32) -> i32 { x * 2 } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn othermod(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; diff --git a/pytests/src/path.rs b/pytests/src/path.rs index 0675e56d13a..d3f875ba165 100644 --- a/pytests/src/path.rs +++ b/pytests/src/path.rs @@ -11,7 +11,7 @@ fn take_pathbuf(path: PathBuf) -> PathBuf { path } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn path(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(make_path, m)?)?; m.add_function(wrap_pyfunction!(take_pathbuf, m)?)?; diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 4e52dbc8712..ed9b78c5d4e 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -104,7 +104,7 @@ impl ClassWithDict { } } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/pyfunctions.rs b/pytests/src/pyfunctions.rs index 77496198bb9..abb3cc739d0 100644 --- a/pytests/src/pyfunctions.rs +++ b/pytests/src/pyfunctions.rs @@ -67,7 +67,7 @@ fn args_kwargs<'py>( (args, kwargs) } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn pyfunctions(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(none, m)?)?; m.add_function(wrap_pyfunction!(simple, m)?)?; diff --git a/pytests/src/sequence.rs b/pytests/src/sequence.rs index f552b4048b8..b3f38f7db58 100644 --- a/pytests/src/sequence.rs +++ b/pytests/src/sequence.rs @@ -16,7 +16,7 @@ fn vec_to_vec_pystring(vec: Vec>) -> Vec vec } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn sequence(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(vec_to_vec_i32, m)?)?; m.add_function(wrap_pyfunction!(array_to_array_i32, m)?)?; diff --git a/pytests/src/subclassing.rs b/pytests/src/subclassing.rs index 8e451cd9183..842d11a2151 100644 --- a/pytests/src/subclassing.rs +++ b/pytests/src/subclassing.rs @@ -17,7 +17,7 @@ impl Subclassable { } } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn subclassing(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) diff --git a/pytests/tests/test_free_threaded.py b/pytests/tests/test_free_threaded.py index e3bd2a1c639..41900aefcb7 100644 --- a/pytests/tests/test_free_threaded.py +++ b/pytests/tests/test_free_threaded.py @@ -1 +1,12 @@ +import pytest +import sys +import sysconfig + from pyo3_pytests import free_threaded_mod # NOQA + +GIL_DISABLED_BUILD = bool(sysconfig.get_config_var('Py_GIL_DISABLED')) + +@pytest.mark.skipif(not GIL_DISABLED_BUILD, + reason="test is not meaningful on GIL-enabled build") +def test_gil_disabled(): + assert not sys._is_gil_enabled() From ecac64097aecf538cdc787ad15de41444922041f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 12:47:35 -0600 Subject: [PATCH 04/34] fix clippy nit --- pyo3-ffi/build.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 64f951723ba..75e7f0eb01c 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -121,7 +121,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { Ok(()) } -fn ensure_gil_enabled(interpreter_config: &InterpreterConfig) -> Result<()> { +fn ensure_gil_enabled(interpreter_config: &InterpreterConfig) { let gil_enabled = interpreter_config .build_flags .0 @@ -132,8 +132,6 @@ fn ensure_gil_enabled(interpreter_config: &InterpreterConfig) -> Result<()> { "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." ) } - - Ok(()) } fn ensure_target_pointer_width(interpreter_config: &InterpreterConfig) -> Result<()> { @@ -201,7 +199,7 @@ fn configure_pyo3() -> Result<()> { ensure_python_version(&interpreter_config)?; ensure_target_pointer_width(&interpreter_config)?; - ensure_gil_enabled(&interpreter_config)?; + ensure_gil_enabled(&interpreter_config); // Serialize the whole interpreter config into DEP_PYTHON_PYO3_CONFIG env var. interpreter_config.to_cargo_dep_env()?; From b5d919bcdddf98ae7fce8803149a2584e70f326d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 12:48:10 -0600 Subject: [PATCH 05/34] fix return type of PyUnstable_Module_SetGIL binding --- pyo3-ffi/src/moduleobject.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index e5d54631c2e..b3203157682 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -107,7 +107,7 @@ pub const Py_MOD_GIL_NOT_USED: *mut c_void = 1 as *mut c_void; #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] extern "C" { - pub fn PyUnstable_Module_SetGIL(module: *mut PyObject, gil: *mut c_void); + pub fn PyUnstable_Module_SetGIL(module: *mut PyObject, gil: *mut c_void) -> c_int; } #[repr(C)] From f837b1435480bb249f69a605f8e04ce77d95e550 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 12:48:27 -0600 Subject: [PATCH 06/34] add a way to declare free-threaded support without macros --- src/impl_/pymodule.rs | 20 ++++++++++++------- src/types/module.rs | 45 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 92949a30108..cdbe18d5e5e 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -1,6 +1,6 @@ //! Implementation details of `#[pymodule]` which need to be accessible from proc-macro generated code. -use std::{cell::UnsafeCell, ffi::CStr, marker::PhantomData}; +use std::{cell::UnsafeCell, ffi::CStr, marker::PhantomData, os::raw::c_int}; #[cfg(all( not(any(PyPy, GraalPy)), @@ -24,7 +24,7 @@ use crate::{ impl_::pymethods::PyMethodDef, sync::GILOnceCell, types::{PyCFunction, PyModule, PyModuleMethods}, - Bound, Py, PyClass, PyResult, PyTypeInfo, Python, + Bound, Py, PyClass, PyErr, PyResult, PyTypeInfo, Python, }; /// `Sync` wrapper of `ffi::PyModuleDef`. @@ -141,11 +141,17 @@ impl ModuleDef { ffi::PyModule_Create(self.ffi_def.get()), )? }; - if supports_free_threaded { - unsafe { - ffi::PyUnstable_Module_SetGIL(module.as_ptr(), ffi::Py_MOD_GIL_NOT_USED) - }; - } + let gil_used = { + if supports_free_threaded { + ffi::Py_MOD_GIL_NOT_USED + } else { + ffi::Py_MOD_GIL_USED + } + }; + match unsafe { ffi::PyUnstable_Module_SetGIL(module.as_ptr(), gil_used) } { + c_int::MIN..=-1 => return Err(PyErr::fetch(py)), + 0..=c_int::MAX => {} + }; self.initializer.0(module.bind(py))?; Ok(module) }) diff --git a/src/types/module.rs b/src/types/module.rs index 89e00e0874f..c39cbc57a4f 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -9,6 +9,7 @@ use crate::types::{ }; use crate::{exceptions, ffi, Borrowed, Bound, BoundObject, Py, PyObject, Python}; use std::ffi::{CStr, CString}; +use std::os::raw::c_int; use std::str; /// Represents a Python [`module`][1] object. @@ -385,6 +386,38 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// [1]: crate::prelude::pyfunction /// [2]: crate::wrap_pyfunction fn add_function(&self, fun: Bound<'_, PyCFunction>) -> PyResult<()>; + + /// Declare whether or not this module supports running with the GIL disabled + /// + /// If the module does not rely on the GIL for thread safety, you can pass True + /// to this function so that when the module is imported the interpreter will + /// not enable the GIL at runtime on the free-threaded interpreter. + /// + /// This function sets the [`Py_MOD_GIL` + /// slot](https://docs.python.org/3/c-api/module.html#c.Py_mod_gil) on the + /// module object. The default is `Py_MOD_GIL_USED`, so passing `false` to + /// this function is a no-op unless you have already set `Py_MOD_GIL` to + /// `Py_MOD_GIL_NOT_USED` elsewhere. + /// + /// # Examples + /// + /// ```rust + /// use pyo3::prelude::*; + /// + /// #[pymodule(supports_free_threaded = true)] + /// fn my_module(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { + /// let submodule = PyModule::new(py, "submodule")?; + /// submodule.supports_free_threaded(true)?; + /// module.add_submodule(&submodule)?; + /// Ok(()) + /// } + /// ``` + /// + /// The resulting module will not print a `RuntimeWarning` and re-enable the + /// GIL when Python imports it on the free-threaded build, since all module + /// objects defined in the extension have `Py_MOD_GIL` set to + /// `Py_MOD_GIL_NOT_USED`. + fn supports_free_threaded(&self, supports_free_threaded: bool) -> PyResult<()>; } impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { @@ -494,7 +527,6 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { T: IntoPyCallbackOutput<'py, PyObject>, { fn inner(module: &Bound<'_, PyModule>, object: Bound<'_, PyAny>) -> PyResult<()> { - if object.is_instance_of::() {} let name = object.getattr(__name__(module.py()))?; module.add(name.downcast_into::()?, object) } @@ -512,6 +544,17 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { let name = fun.getattr(__name__(self.py()))?; self.add(name.downcast_into::()?, fun) } + + fn supports_free_threaded(&self, supports_free_threaded: bool) -> PyResult<()> { + let gil_used = match supports_free_threaded { + true => ffi::Py_MOD_GIL_NOT_USED, + false => ffi::Py_MOD_GIL_USED, + }; + match unsafe { ffi::PyUnstable_Module_SetGIL(self.as_ptr(), gil_used) } { + c_int::MIN..=-1 => Err(PyErr::fetch(self.py())), + 0..=c_int::MAX => Ok(()), + } + } } fn __all__(py: Python<'_>) -> &Bound<'_, PyString> { From f85e1a2012b411069c14feb7331b05312b7b3ce2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 15:55:51 -0600 Subject: [PATCH 07/34] fix ruff --- pytests/tests/test_free_threaded.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pytests/tests/test_free_threaded.py b/pytests/tests/test_free_threaded.py index 41900aefcb7..f87c10d392c 100644 --- a/pytests/tests/test_free_threaded.py +++ b/pytests/tests/test_free_threaded.py @@ -4,9 +4,11 @@ from pyo3_pytests import free_threaded_mod # NOQA -GIL_DISABLED_BUILD = bool(sysconfig.get_config_var('Py_GIL_DISABLED')) +GIL_DISABLED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) -@pytest.mark.skipif(not GIL_DISABLED_BUILD, - reason="test is not meaningful on GIL-enabled build") + +@pytest.mark.skipif( + not GIL_DISABLED_BUILD, reason="test is not meaningful on GIL-enabled build" +) def test_gil_disabled(): assert not sys._is_gil_enabled() From 9ce99eb5e3dde5a7bd3171c18072ecf86fbbf8b2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 16:20:36 -0600 Subject: [PATCH 08/34] fix changed ui test answer --- tests/ui/invalid_pymodule_args.stderr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/invalid_pymodule_args.stderr b/tests/ui/invalid_pymodule_args.stderr index 261d8115e15..1144bbf8d8d 100644 --- a/tests/ui/invalid_pymodule_args.stderr +++ b/tests/ui/invalid_pymodule_args.stderr @@ -1,4 +1,4 @@ -error: expected one of: `name`, `crate`, `module`, `submodule` +error: expected one of: `name`, `crate`, `module`, `submodule`, `supports_free_threaded` --> tests/ui/invalid_pymodule_args.rs:3:12 | 3 | #[pymodule(some_arg)] From cb95dc226de9122ba249e1f590af1ae025c59d4b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 16:50:43 -0600 Subject: [PATCH 09/34] fix build issues on old python versions --- src/impl_/pymodule.rs | 34 +++++++++++++++++++++------------- src/types/module.rs | 5 +++++ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index cdbe18d5e5e..c940722d286 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -1,6 +1,8 @@ //! Implementation details of `#[pymodule]` which need to be accessible from proc-macro generated code. -use std::{cell::UnsafeCell, ffi::CStr, marker::PhantomData, os::raw::c_int}; +#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +use std::os::raw::c_int; +use std::{cell::UnsafeCell, ffi::CStr, marker::PhantomData}; #[cfg(all( not(any(PyPy, GraalPy)), @@ -19,12 +21,14 @@ use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; #[cfg(not(any(PyPy, GraalPy)))] use crate::exceptions::PyImportError; +#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +use crate::PyErr; use crate::{ ffi, impl_::pymethods::PyMethodDef, sync::GILOnceCell, types::{PyCFunction, PyModule, PyModuleMethods}, - Bound, Py, PyClass, PyErr, PyResult, PyTypeInfo, Python, + Bound, Py, PyClass, PyResult, PyTypeInfo, Python, }; /// `Sync` wrapper of `ffi::PyModuleDef`. @@ -91,6 +95,7 @@ impl ModuleDef { } } /// Builds a module using user given initializer. Used for [`#[pymodule]`][crate::pymodule]. + #[cfg_attr(any(Py_LIMITED_API, not(Py_3_13)), allow(unused_variables))] pub fn make_module( &'static self, py: Python<'_>, @@ -141,17 +146,20 @@ impl ModuleDef { ffi::PyModule_Create(self.ffi_def.get()), )? }; - let gil_used = { - if supports_free_threaded { - ffi::Py_MOD_GIL_NOT_USED - } else { - ffi::Py_MOD_GIL_USED - } - }; - match unsafe { ffi::PyUnstable_Module_SetGIL(module.as_ptr(), gil_used) } { - c_int::MIN..=-1 => return Err(PyErr::fetch(py)), - 0..=c_int::MAX => {} - }; + #[cfg(all(not(Py_LIMITED_API), Py_3_13))] + { + let gil_used = { + if supports_free_threaded { + ffi::Py_MOD_GIL_NOT_USED + } else { + ffi::Py_MOD_GIL_USED + } + }; + match unsafe { ffi::PyUnstable_Module_SetGIL(module.as_ptr(), gil_used) } { + c_int::MIN..=-1 => return Err(PyErr::fetch(py)), + 0..=c_int::MAX => {} + }; + } self.initializer.0(module.bind(py))?; Ok(module) }) diff --git a/src/types/module.rs b/src/types/module.rs index c39cbc57a4f..4316db42624 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -9,6 +9,7 @@ use crate::types::{ }; use crate::{exceptions, ffi, Borrowed, Bound, BoundObject, Py, PyObject, Python}; use std::ffi::{CStr, CString}; +#[cfg(all(not(Py_LIMITED_API), Py_3_13))] use std::os::raw::c_int; use std::str; @@ -417,6 +418,8 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// GIL when Python imports it on the free-threaded build, since all module /// objects defined in the extension have `Py_MOD_GIL` set to /// `Py_MOD_GIL_NOT_USED`. + #[cfg(all(not(Py_LIMITED_API), Py_3_13))] + #[cfg_attr(docsrs, doc(cfg(all())))] fn supports_free_threaded(&self, supports_free_threaded: bool) -> PyResult<()>; } @@ -545,6 +548,8 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { self.add(name.downcast_into::()?, fun) } + #[cfg(all(not(Py_LIMITED_API), Py_3_13))] + #[cfg_attr(docsrs, doc(cfg(all())))] fn supports_free_threaded(&self, supports_free_threaded: bool) -> PyResult<()> { let gil_used = match supports_free_threaded { true => ffi::Py_MOD_GIL_NOT_USED, From 81d467b374c86db50df841bc92e2605ffa0cba53 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 16:50:53 -0600 Subject: [PATCH 10/34] fix runtime warnings in examples --- examples/decorator/src/lib.rs | 14 ++++++-------- examples/getitem/src/lib.rs | 2 +- examples/maturin-starter/src/lib.rs | 2 +- examples/maturin-starter/src/submodule.rs | 2 +- examples/plugin/plugin_api/src/lib.rs | 2 +- examples/setuptools-rust-starter/src/lib.rs | 2 +- examples/setuptools-rust-starter/src/submodule.rs | 2 +- examples/word-count/src/lib.rs | 2 +- pyo3-ffi/examples/sequential/src/lib.rs | 1 + pyo3-ffi/examples/sequential/src/module.rs | 4 ++++ pyo3-ffi/examples/string-sum/src/lib.rs | 9 ++++++++- 11 files changed, 26 insertions(+), 16 deletions(-) diff --git a/examples/decorator/src/lib.rs b/examples/decorator/src/lib.rs index 8d257aecb2e..eef2c3984aa 100644 --- a/examples/decorator/src/lib.rs +++ b/examples/decorator/src/lib.rs @@ -1,6 +1,6 @@ use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; -use std::cell::Cell; +use std::sync::atomic::{AtomicU64, Ordering}; /// A function decorator that keeps track how often it is called. /// @@ -10,7 +10,7 @@ pub struct PyCounter { // Keeps track of how many calls have gone through. // // See the discussion at the end for why `Cell` is used. - count: Cell, + count: AtomicU64, // This is the actual function being wrapped. wraps: Py, @@ -26,14 +26,14 @@ impl PyCounter { #[new] fn __new__(wraps: Py) -> Self { PyCounter { - count: Cell::new(0), + count: AtomicU64::new(0), wraps, } } #[getter] fn count(&self) -> u64 { - self.count.get() + self.count.load(Ordering::Acquire) } #[pyo3(signature = (*args, **kwargs))] @@ -43,9 +43,7 @@ impl PyCounter { args: &Bound<'_, PyTuple>, kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { - let old_count = self.count.get(); - let new_count = old_count + 1; - self.count.set(new_count); + let new_count = self.count.fetch_add(1, Ordering::Relaxed) + 1; let name = self.wraps.getattr(py, "__name__")?; println!("{} has been called {} time(s).", name, new_count); @@ -59,7 +57,7 @@ impl PyCounter { } } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn decorator(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; Ok(()) diff --git a/examples/getitem/src/lib.rs b/examples/getitem/src/lib.rs index ba850a06b8d..b494b3faeb1 100644 --- a/examples/getitem/src/lib.rs +++ b/examples/getitem/src/lib.rs @@ -75,7 +75,7 @@ impl ExampleContainer { } } -#[pymodule(name = "getitem")] +#[pymodule(name = "getitem", supports_free_threaded = true)] fn example(m: &Bound<'_, PyModule>) -> PyResult<()> { // ? -https://github.com/PyO3/maturin/issues/475 m.add_class::()?; diff --git a/examples/maturin-starter/src/lib.rs b/examples/maturin-starter/src/lib.rs index 4c2a30d3a5d..75e122529f3 100644 --- a/examples/maturin-starter/src/lib.rs +++ b/examples/maturin-starter/src/lib.rs @@ -19,7 +19,7 @@ impl ExampleClass { } /// An example module implemented in Rust using PyO3. -#[pymodule] +#[pymodule(supports_free_threaded = true)] fn maturin_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_wrapped(wrap_pymodule!(submodule::submodule))?; diff --git a/examples/maturin-starter/src/submodule.rs b/examples/maturin-starter/src/submodule.rs index f3eb174100b..2d6ea86fa1d 100644 --- a/examples/maturin-starter/src/submodule.rs +++ b/examples/maturin-starter/src/submodule.rs @@ -15,7 +15,7 @@ impl SubmoduleClass { } } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn submodule(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) diff --git a/examples/plugin/plugin_api/src/lib.rs b/examples/plugin/plugin_api/src/lib.rs index 580c85a8c8e..98f118523af 100644 --- a/examples/plugin/plugin_api/src/lib.rs +++ b/examples/plugin/plugin_api/src/lib.rs @@ -25,7 +25,7 @@ impl Gadget { } /// A Python module for plugin interface types -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn plugin_api(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) diff --git a/examples/setuptools-rust-starter/src/lib.rs b/examples/setuptools-rust-starter/src/lib.rs index a26623bc044..52e5f3b5a0c 100644 --- a/examples/setuptools-rust-starter/src/lib.rs +++ b/examples/setuptools-rust-starter/src/lib.rs @@ -19,7 +19,7 @@ impl ExampleClass { } /// An example module implemented in Rust using PyO3. -#[pymodule] +#[pymodule(supports_free_threaded = true)] fn _setuptools_rust_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_wrapped(wrap_pymodule!(submodule::submodule))?; diff --git a/examples/setuptools-rust-starter/src/submodule.rs b/examples/setuptools-rust-starter/src/submodule.rs index f3eb174100b..2d6ea86fa1d 100644 --- a/examples/setuptools-rust-starter/src/submodule.rs +++ b/examples/setuptools-rust-starter/src/submodule.rs @@ -15,7 +15,7 @@ impl SubmoduleClass { } } -#[pymodule] +#[pymodule(supports_free_threaded = true)] pub fn submodule(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) diff --git a/examples/word-count/src/lib.rs b/examples/word-count/src/lib.rs index 5bc73df97a4..5db250a6e5d 100644 --- a/examples/word-count/src/lib.rs +++ b/examples/word-count/src/lib.rs @@ -32,7 +32,7 @@ fn count_line(line: &str, needle: &str) -> usize { total } -#[pymodule] +#[pymodule(supports_free_threaded = true)] fn word_count(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(search, m)?)?; m.add_function(wrap_pyfunction!(search_sequential, m)?)?; diff --git a/pyo3-ffi/examples/sequential/src/lib.rs b/pyo3-ffi/examples/sequential/src/lib.rs index 7e77064de4d..c61ae5a581e 100644 --- a/pyo3-ffi/examples/sequential/src/lib.rs +++ b/pyo3-ffi/examples/sequential/src/lib.rs @@ -1,3 +1,4 @@ +use std::os::raw::c_int; use std::ptr; use pyo3_ffi::*; diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index 5e71f07a865..1e88cb32279 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -23,6 +23,10 @@ static mut SEQUENTIAL_SLOTS: &[PyModuleDef_Slot] = &[ slot: Py_mod_multiple_interpreters, value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, }, + PyModuleDef_Slot { + slot: Py_mod_gil, + value: Py_MOD_GIL_NOT_USED, + }, PyModuleDef_Slot { slot: 0, value: ptr::null_mut(), diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index 9f0d6c6435f..907afd81506 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -32,7 +32,14 @@ static mut METHODS: &[PyMethodDef] = &[ #[allow(non_snake_case)] #[no_mangle] pub unsafe extern "C" fn PyInit_string_sum() -> *mut PyObject { - PyModule_Create(ptr::addr_of_mut!(MODULE_DEF)) + let module = PyModule_Create(ptr::addr_of_mut!(MODULE_DEF)); + if module.is_null() { + return module; + } + if PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) < 0 { + return std::ptr::null_mut(); + } + module } /// A helper to parse function arguments From 389f28b7d3770da4a2e85dd2048adbf854382728 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 17:27:10 -0600 Subject: [PATCH 11/34] ensure that the GIL does not get re-enabled in the pytests --- pytests/conftest.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 pytests/conftest.py diff --git a/pytests/conftest.py b/pytests/conftest.py new file mode 100644 index 00000000000..47112163ab0 --- /dev/null +++ b/pytests/conftest.py @@ -0,0 +1,22 @@ +import sysconfig +import sys +import pytest + +FREE_THREADED_BUILD = bool(sysconfig.get_config_var('Py_GIL_DISABLED')) + +gil_enabled_at_start = True +if FREE_THREADED_BUILD: + gil_enabled_at_start = sys._is_gil_enabled() + + +def pytest_terminal_summary(terminalreporter, exitstatus, config): + if FREE_THREADED_BUILD and not gil_enabled_at_start and sys._is_gil_enabled(): + tr = terminalreporter + tr.ensure_newline() + tr.section("GIL re-enabled", sep="=", red=True, bold=True) + tr.line("The GIL was re-enabled at runtime during the tests.") + tr.line("") + tr.line("Please ensure all new modules declare support for running") + tr.line("without the GIL. Any new tests that intentionally imports ") + tr.line("code that re-enables the GIL should do so in a subprocess.") + pytest.exit("GIL re-enabled during tests", returncode=1) From ce14ddb39872d8e8c547fe61aa09f1d0044bf6a9 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 17:29:18 -0600 Subject: [PATCH 12/34] add changelog entry --- newsfragments/4588.added.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 newsfragments/4588.added.md diff --git a/newsfragments/4588.added.md b/newsfragments/4588.added.md new file mode 100644 index 00000000000..f42453d33d9 --- /dev/null +++ b/newsfragments/4588.added.md @@ -0,0 +1,3 @@ +* It is now possible to declare that a module supports the free-threaded build + by either calling `PyModule::supports_free_threaded` or passing + `supports_free_threaded = true` as a parameter to the `pymodule` proc macro. From f5792470fcb981988e58ac59b2c8b053ec02462a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 17:30:11 -0600 Subject: [PATCH 13/34] fix ruff --- pytests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytests/conftest.py b/pytests/conftest.py index 47112163ab0..ce729689355 100644 --- a/pytests/conftest.py +++ b/pytests/conftest.py @@ -2,7 +2,7 @@ import sys import pytest -FREE_THREADED_BUILD = bool(sysconfig.get_config_var('Py_GIL_DISABLED')) +FREE_THREADED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) gil_enabled_at_start = True if FREE_THREADED_BUILD: From 26acf5687715017945258c4df648343d30d3c430 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 17:35:45 -0600 Subject: [PATCH 14/34] fix compiler error on older pythons --- src/impl_/pymodule.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index c940722d286..28672721cf4 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -11,13 +11,14 @@ use std::{cell::UnsafeCell, ffi::CStr, marker::PhantomData}; not(target_has_atomic = "64"), ))] use portable_atomic::{AtomicI64, Ordering}; +use std::sync::atomic::AtomicBool; #[cfg(all( not(any(PyPy, GraalPy)), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10))), target_has_atomic = "64", ))] -use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; +use std::sync::atomic::{AtomicI64, Ordering}; #[cfg(not(any(PyPy, GraalPy)))] use crate::exceptions::PyImportError; From 2cc90939b4b166a7b5b769296a09c07650c6743b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 17:46:25 -0600 Subject: [PATCH 15/34] fix clippy --- src/impl_/pymodule.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 28672721cf4..3865810ca23 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -10,15 +10,15 @@ use std::{cell::UnsafeCell, ffi::CStr, marker::PhantomData}; not(all(windows, Py_LIMITED_API, not(Py_3_10))), not(target_has_atomic = "64"), ))] -use portable_atomic::{AtomicI64, Ordering}; -use std::sync::atomic::AtomicBool; +use portable_atomic::AtomicI64; #[cfg(all( not(any(PyPy, GraalPy)), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10))), target_has_atomic = "64", ))] -use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::atomic::AtomicI64; +use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(not(any(PyPy, GraalPy)))] use crate::exceptions::PyImportError; From 1e891085e8a0a58e319babedf3bf4517dcb72e80 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 18:23:43 -0600 Subject: [PATCH 16/34] really fix clippy and expose supports_free_threaded on all builds --- src/impl_/pymodule.rs | 8 ++++---- src/types/module.rs | 27 +++++++++++++++------------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 3865810ca23..babcecfb36f 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -1,6 +1,6 @@ //! Implementation details of `#[pymodule]` which need to be accessible from proc-macro generated code. -#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +#[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] use std::os::raw::c_int; use std::{cell::UnsafeCell, ffi::CStr, marker::PhantomData}; @@ -22,7 +22,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(not(any(PyPy, GraalPy)))] use crate::exceptions::PyImportError; -#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +#[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] use crate::PyErr; use crate::{ ffi, @@ -96,7 +96,7 @@ impl ModuleDef { } } /// Builds a module using user given initializer. Used for [`#[pymodule]`][crate::pymodule]. - #[cfg_attr(any(Py_LIMITED_API, not(Py_3_13)), allow(unused_variables))] + #[cfg_attr(any(Py_LIMITED_API, not(Py_GIL_DISABLED)), allow(unused_variables))] pub fn make_module( &'static self, py: Python<'_>, @@ -147,7 +147,7 @@ impl ModuleDef { ffi::PyModule_Create(self.ffi_def.get()), )? }; - #[cfg(all(not(Py_LIMITED_API), Py_3_13))] + #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] { let gil_used = { if supports_free_threaded { diff --git a/src/types/module.rs b/src/types/module.rs index 4316db42624..67bcd2e400a 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -9,7 +9,7 @@ use crate::types::{ }; use crate::{exceptions, ffi, Borrowed, Bound, BoundObject, Py, PyObject, Python}; use std::ffi::{CStr, CString}; -#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +#[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] use std::os::raw::c_int; use std::str; @@ -418,8 +418,8 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// GIL when Python imports it on the free-threaded build, since all module /// objects defined in the extension have `Py_MOD_GIL` set to /// `Py_MOD_GIL_NOT_USED`. - #[cfg(all(not(Py_LIMITED_API), Py_3_13))] - #[cfg_attr(docsrs, doc(cfg(all())))] + /// + /// This is a no-op on the GIL-enabled build. fn supports_free_threaded(&self, supports_free_threaded: bool) -> PyResult<()>; } @@ -548,17 +548,20 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { self.add(name.downcast_into::()?, fun) } - #[cfg(all(not(Py_LIMITED_API), Py_3_13))] - #[cfg_attr(docsrs, doc(cfg(all())))] + #[cfg_attr(any(Py_LIMITED_API, not(Py_GIL_DISABLED)), allow(unused_variables))] fn supports_free_threaded(&self, supports_free_threaded: bool) -> PyResult<()> { - let gil_used = match supports_free_threaded { - true => ffi::Py_MOD_GIL_NOT_USED, - false => ffi::Py_MOD_GIL_USED, - }; - match unsafe { ffi::PyUnstable_Module_SetGIL(self.as_ptr(), gil_used) } { - c_int::MIN..=-1 => Err(PyErr::fetch(self.py())), - 0..=c_int::MAX => Ok(()), + #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] + { + let gil_used = match supports_free_threaded { + true => ffi::Py_MOD_GIL_NOT_USED, + false => ffi::Py_MOD_GIL_USED, + }; + match unsafe { ffi::PyUnstable_Module_SetGIL(self.as_ptr(), gil_used) } { + c_int::MIN..=-1 => Err(PyErr::fetch(self.py())), + 0..=c_int::MAX => Ok(()), + } } + Ok(()) } } From a8759184a5938d66ad1cdb835e21479206071d51 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 18:36:12 -0600 Subject: [PATCH 17/34] fix clippy and msrv --- pyo3-macros-backend/src/module.rs | 6 ++++-- src/types/module.rs | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 9537d66ac41..8fd52582e26 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -355,7 +355,8 @@ pub fn pymodule_module_impl( options.submodule.is_some(), options .supports_free_threaded - .is_some_and(|op| op.value.value), + .map(|op| op.value.value) + .unwrap_or(false), ); Ok(quote!( @@ -402,7 +403,8 @@ pub fn pymodule_function_impl( false, options .supports_free_threaded - .is_some_and(|op| op.value.value), + .map(|op| op.value.value) + .unwrap_or(false), ); // Module function called with optional Python<'_> marker as first arg, followed by the module. diff --git a/src/types/module.rs b/src/types/module.rs index 67bcd2e400a..06524bc4326 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -561,6 +561,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { 0..=c_int::MAX => Ok(()), } } + #[cfg(any(Py_LIMITED_API, not(Py_GIL_DISABLED)))] Ok(()) } } From 555421f706310062f06181fefc79eeb1b4ce378e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 19:23:00 -0600 Subject: [PATCH 18/34] fix examples on gil-disabled python --- pyo3-ffi/examples/sequential/src/module.rs | 1 + pyo3-ffi/examples/string-sum/src/lib.rs | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index 1e88cb32279..baa7c66f206 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -23,6 +23,7 @@ static mut SEQUENTIAL_SLOTS: &[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, diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index 907afd81506..c5979835de5 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -36,8 +36,11 @@ pub unsafe extern "C" fn PyInit_string_sum() -> *mut PyObject { if module.is_null() { return module; } - if PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) < 0 { - return std::ptr::null_mut(); + #[cfg(Py_GIL_DISABLED)] + { + if PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) < 0 { + return std::ptr::null_mut(); + } } module } From 4d1873cd10a03a0ff2abccaa584990f96237649a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 19:23:07 -0600 Subject: [PATCH 19/34] fix free-threaded clippy --- src/impl_/pymodule.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index babcecfb36f..30c72c3e998 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -1,7 +1,5 @@ //! Implementation details of `#[pymodule]` which need to be accessible from proc-macro generated code. -#[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] -use std::os::raw::c_int; use std::{cell::UnsafeCell, ffi::CStr, marker::PhantomData}; #[cfg(all( @@ -156,10 +154,9 @@ impl ModuleDef { ffi::Py_MOD_GIL_USED } }; - match unsafe { ffi::PyUnstable_Module_SetGIL(module.as_ptr(), gil_used) } { - c_int::MIN..=-1 => return Err(PyErr::fetch(py)), - 0..=c_int::MAX => {} - }; + if unsafe { ffi::PyUnstable_Module_SetGIL(module.as_ptr(), gil_used) } < 0 { + return Err(PyErr::fetch(py)); + } } self.initializer.0(module.bind(py))?; Ok(module) From 160cee3b5267913e28def99fb55a39bfb0aa0cb6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 19:25:11 -0600 Subject: [PATCH 20/34] fix unused import in example --- pyo3-ffi/examples/sequential/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/pyo3-ffi/examples/sequential/src/lib.rs b/pyo3-ffi/examples/sequential/src/lib.rs index c61ae5a581e..7e77064de4d 100644 --- a/pyo3-ffi/examples/sequential/src/lib.rs +++ b/pyo3-ffi/examples/sequential/src/lib.rs @@ -1,4 +1,3 @@ -use std::os::raw::c_int; use std::ptr; use pyo3_ffi::*; From e3aa6deadede9d18ff8328c9c7c62bc9f47c4b7f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 23 Oct 2024 10:09:11 -0600 Subject: [PATCH 21/34] Add pyo3-build-config as a build dependency to examples that need it --- pyo3-ffi/examples/sequential/Cargo.toml | 3 +++ pyo3-ffi/examples/sequential/build.rs | 3 +++ pyo3-ffi/examples/string-sum/Cargo.toml | 3 +++ pyo3-ffi/examples/string-sum/build.rs | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 pyo3-ffi/examples/sequential/build.rs create mode 100644 pyo3-ffi/examples/string-sum/build.rs diff --git a/pyo3-ffi/examples/sequential/Cargo.toml b/pyo3-ffi/examples/sequential/Cargo.toml index 3348595b4e9..6e32d1d2952 100644 --- a/pyo3-ffi/examples/sequential/Cargo.toml +++ b/pyo3-ffi/examples/sequential/Cargo.toml @@ -10,4 +10,7 @@ crate-type = ["cdylib", "lib"] [dependencies] pyo3-ffi = { path = "../../", features = ["extension-module"] } +[build-dependencies] +pyo3-build-config = { path = "../../pyo3-build-config" } + [workspace] diff --git a/pyo3-ffi/examples/sequential/build.rs b/pyo3-ffi/examples/sequential/build.rs new file mode 100644 index 00000000000..0475124bb4e --- /dev/null +++ b/pyo3-ffi/examples/sequential/build.rs @@ -0,0 +1,3 @@ +fn main() { + pyo3_build_config::use_pyo3_cfgs(); +} diff --git a/pyo3-ffi/examples/string-sum/Cargo.toml b/pyo3-ffi/examples/string-sum/Cargo.toml index 6fb72141cdc..b9a17239334 100644 --- a/pyo3-ffi/examples/string-sum/Cargo.toml +++ b/pyo3-ffi/examples/string-sum/Cargo.toml @@ -10,4 +10,7 @@ crate-type = ["cdylib"] [dependencies] pyo3-ffi = { path = "../../", features = ["extension-module"] } +[build-dependencies] +pyo3-build-config = { path = "../../pyo3-build-config" } + [workspace] diff --git a/pyo3-ffi/examples/string-sum/build.rs b/pyo3-ffi/examples/string-sum/build.rs new file mode 100644 index 00000000000..0475124bb4e --- /dev/null +++ b/pyo3-ffi/examples/string-sum/build.rs @@ -0,0 +1,3 @@ +fn main() { + pyo3_build_config::use_pyo3_cfgs(); +} From 82516bbe14077ced8dbc79e7e3c575a719921441 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 23 Oct 2024 15:50:54 -0600 Subject: [PATCH 22/34] add docs --- guide/src/free-threading.md | 76 ++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 77b2ff327a2..4a2041868a7 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -35,11 +35,75 @@ concurrency"](https://doc.rust-lang.org/book/ch16-00-concurrency.html) in the native Python runtime by building on the Rust `Send` and `Sync` traits. This document provides advice for porting Rust code using PyO3 to run under -free-threaded Python. While many simple PyO3 uses, like defining an immutable -Python class, will likely work "out of the box", there are currently some -limitations. +free-threaded Python. + +## Supporting free-threaded Python with PyO3 + +Many simple uses of PyO3, like exposing bindings for a "pure" Rust function with +no side-effects or defining an immutable Python class, will likely work "out of +the box" on the free-threaded build. All that will be necessary is to +annotate Python modules declared by rust code in your project to declare that +they support free-threaded Python. + +At a low-level, annotating a module sets the `Py_MOD_GIL` slot on modules +defined by an extension to `Py_MOD_GIL_NOT_USED`, which allows the interpreter +to see at runtime that the author of the extension thinks the extension is +thread-safe. You should only do this if you know that your extension is +thread-safe. Because of Rust's guarantees, this is already true for many +extensions, however see below for more discussion about how to evaluate the +thread safety of existing Rust extensions and how to think about the PyO3 API +using a Python runtime with no GIL. + +If you do not explicitly mark that modules are thread-safe, the Python +interpreter will re-enable the GIL at runtime and print a `RuntimeWarning` +explaining which module caused it to re-enable the GIL. You can also force the +GIL to remain disabled by setting the `PYTHON_GIL=0` as an environment variable +or passing `-Xgil=0` when starting Python (`0` means the GIL is turned off). + +If you are sure that all data structures exposed in a [`PyModule`] are +thread-safe, then pass `supports_free_threaded = true` as a parameter to the +`pymodule` procedural macro declaring the module or call +[`PyModule::supports_free_threaded`] on a `PyModule` instance. For example: -## Many symbols exposed by PyO3 have `GIL` in the name +```rust +use pyo3::prelude::*; + +/// This module supports free-threaded Python +#[pymodule(supports_free_threaded = true)] +fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { + // add members to the module that you know are thread-safe + Ok(()) +} +``` + +Or for a module that is set up without using the `pymodule` macro: + +```rust +use pyo3::prelude::*; + +# #[allow(dead_code)] +fn register_child_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + let child_module = PyModule::new(parent_module.py(), "child_module")?; + child_module.supports_free_threaded(true)?; + parent_module.add_submodule(&child_module) +} + +``` + +## Special considerations for the free-threaded build + +The free-threaded interpreter does not have a GIL, and this can make interacting +with the PyO3 API confusing, since the API was originally designed around strong +assumptions about the GIL providing locking. Additionally, since the GIL +provided locking for operations on Python objects, many existing extensions that +provide mutable data structures relied on the GIL to make interior mutability +thread-safe. + +Working with PyO3 under the free-threaded interpreter therefore requires some +additional care and mental overhead compared with a GIL-enabled interpreter. We +discuss how to handle this below. + +### Many symbols exposed by PyO3 have `GIL` in the name We are aware that there are some naming issues in the PyO3 API that make it awkward to think about a runtime environment where there is no GIL. We plan to @@ -83,7 +147,7 @@ garbage collector can only run if all threads are detached from the runtime (in a stop-the-world state), so detaching from the runtime allows freeing unused memory. -## Exceptions and panics for multithreaded access of mutable `pyclass` instances +### Exceptions and panics for multithreaded access of mutable `pyclass` instances Data attached to `pyclass` instances is protected from concurrent access by a `RefCell`-like pattern of runtime borrow checking. Like a `RefCell`, PyO3 will @@ -152,7 +216,7 @@ We plan to allow user-selectable semantics for mutable pyclass definitions in PyO3 0.24, allowing some form of opt-in locking to emulate the GIL if that is needed. -## `GILProtected` is not exposed +### `GILProtected` is not exposed `GILProtected` is a PyO3 type that allows mutable access to static data by leveraging the GIL to lock concurrent access from other threads. In From 0172bf22ad22fe65e6e59adae57fe9f0c69914cd Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 23 Oct 2024 15:56:25 -0600 Subject: [PATCH 23/34] add rust tests so coverage picks up the new code --- tests/test_module.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_module.rs b/tests/test_module.rs index 7b97fb3a889..d69fe1d3f7b 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -37,7 +37,7 @@ fn double(x: usize) -> usize { } /// This module is implemented in Rust. -#[pymodule] +#[pymodule(supports_free_threaded = true)] fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m)] #[pyo3(name = "no_parameters")] @@ -182,6 +182,10 @@ fn test_module_from_code_bound() { .extract() .expect("The value should be able to be converted to an i32"); + adder_mod + .supports_free_threaded(true) + .expect("Disabling the GIL failed"); + assert_eq!(ret_value, 3); }); } From 2fdc38212d59ffd07be852fbf2c5e0a63f5ccb9a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 23 Oct 2024 16:20:27 -0600 Subject: [PATCH 24/34] fix some formatting issues --- guide/src/free-threading.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 4a2041868a7..e1c593ead29 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -60,10 +60,10 @@ explaining which module caused it to re-enable the GIL. You can also force the GIL to remain disabled by setting the `PYTHON_GIL=0` as an environment variable or passing `-Xgil=0` when starting Python (`0` means the GIL is turned off). -If you are sure that all data structures exposed in a [`PyModule`] are +If you are sure that all data structures exposed in a `PyModule` are thread-safe, then pass `supports_free_threaded = true` as a parameter to the `pymodule` procedural macro declaring the module or call -[`PyModule::supports_free_threaded`] on a `PyModule` instance. For example: +`PyModule::supports_free_threaded` on a `PyModule` instance. For example: ```rust use pyo3::prelude::*; @@ -163,7 +163,7 @@ The most straightforward way to trigger this problem to use the Python `threading` module to simultaneously call a rust function that mutably borrows a `pyclass`. For example, consider the following `PyClass` implementation: -``` +```rust # use pyo3::prelude::*; # fn main() { #[pyclass] From ac30f2e18066f3d8a86378c3f389a78a3e0a6128 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 23 Oct 2024 16:21:31 -0600 Subject: [PATCH 25/34] Apply cleanups --- src/macros.rs | 1 - src/types/module.rs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index 2cd00f79aa6..4d7d4616425 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -176,7 +176,6 @@ macro_rules! wrap_pymodule { &|py| { use $module as wrapped_pymodule; wrapped_pymodule::_PYO3_DEF - // corrected in add_wrapped later based on the parent module's settings .make_module(py, false) .expect("failed to wrap pymodule") } diff --git a/src/types/module.rs b/src/types/module.rs index 06524bc4326..1c856dac687 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -391,8 +391,8 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// Declare whether or not this module supports running with the GIL disabled /// /// If the module does not rely on the GIL for thread safety, you can pass True - /// to this function so that when the module is imported the interpreter will - /// not enable the GIL at runtime on the free-threaded interpreter. + /// to this function to indicate the module does not rely on the GIL for + /// thread-safety. /// /// This function sets the [`Py_MOD_GIL` /// slot](https://docs.python.org/3/c-api/module.html#c.Py_mod_gil) on the From ab60aac4f191ddb91fc6579883d13eed3495a75b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 23 Oct 2024 17:17:37 -0600 Subject: [PATCH 26/34] fix cargo fmt --check --- src/types/module.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/module.rs b/src/types/module.rs index 1c856dac687..86693a32fb1 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -391,7 +391,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// Declare whether or not this module supports running with the GIL disabled /// /// If the module does not rely on the GIL for thread safety, you can pass True - /// to this function to indicate the module does not rely on the GIL for + /// to this function to indicate the module does not rely on the GIL for /// thread-safety. /// /// This function sets the [`Py_MOD_GIL` From 2ef9e16a724d499abaed9d1d8318a70fc4d5eb1b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 28 Oct 2024 11:09:17 -0600 Subject: [PATCH 27/34] revert changes to non-FFI examples --- examples/decorator/src/lib.rs | 14 ++++++++------ examples/getitem/src/lib.rs | 2 +- examples/maturin-starter/src/lib.rs | 2 +- examples/maturin-starter/src/submodule.rs | 2 +- examples/plugin/plugin_api/src/lib.rs | 2 +- examples/setuptools-rust-starter/src/lib.rs | 2 +- examples/setuptools-rust-starter/src/submodule.rs | 2 +- examples/word-count/src/lib.rs | 2 +- 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/examples/decorator/src/lib.rs b/examples/decorator/src/lib.rs index eef2c3984aa..8d257aecb2e 100644 --- a/examples/decorator/src/lib.rs +++ b/examples/decorator/src/lib.rs @@ -1,6 +1,6 @@ use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::cell::Cell; /// A function decorator that keeps track how often it is called. /// @@ -10,7 +10,7 @@ pub struct PyCounter { // Keeps track of how many calls have gone through. // // See the discussion at the end for why `Cell` is used. - count: AtomicU64, + count: Cell, // This is the actual function being wrapped. wraps: Py, @@ -26,14 +26,14 @@ impl PyCounter { #[new] fn __new__(wraps: Py) -> Self { PyCounter { - count: AtomicU64::new(0), + count: Cell::new(0), wraps, } } #[getter] fn count(&self) -> u64 { - self.count.load(Ordering::Acquire) + self.count.get() } #[pyo3(signature = (*args, **kwargs))] @@ -43,7 +43,9 @@ impl PyCounter { args: &Bound<'_, PyTuple>, kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { - let new_count = self.count.fetch_add(1, Ordering::Relaxed) + 1; + let old_count = self.count.get(); + let new_count = old_count + 1; + self.count.set(new_count); let name = self.wraps.getattr(py, "__name__")?; println!("{} has been called {} time(s).", name, new_count); @@ -57,7 +59,7 @@ impl PyCounter { } } -#[pymodule(supports_free_threaded = true)] +#[pymodule] pub fn decorator(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; Ok(()) diff --git a/examples/getitem/src/lib.rs b/examples/getitem/src/lib.rs index b494b3faeb1..ba850a06b8d 100644 --- a/examples/getitem/src/lib.rs +++ b/examples/getitem/src/lib.rs @@ -75,7 +75,7 @@ impl ExampleContainer { } } -#[pymodule(name = "getitem", supports_free_threaded = true)] +#[pymodule(name = "getitem")] fn example(m: &Bound<'_, PyModule>) -> PyResult<()> { // ? -https://github.com/PyO3/maturin/issues/475 m.add_class::()?; diff --git a/examples/maturin-starter/src/lib.rs b/examples/maturin-starter/src/lib.rs index 75e122529f3..4c2a30d3a5d 100644 --- a/examples/maturin-starter/src/lib.rs +++ b/examples/maturin-starter/src/lib.rs @@ -19,7 +19,7 @@ impl ExampleClass { } /// An example module implemented in Rust using PyO3. -#[pymodule(supports_free_threaded = true)] +#[pymodule] fn maturin_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_wrapped(wrap_pymodule!(submodule::submodule))?; diff --git a/examples/maturin-starter/src/submodule.rs b/examples/maturin-starter/src/submodule.rs index 2d6ea86fa1d..f3eb174100b 100644 --- a/examples/maturin-starter/src/submodule.rs +++ b/examples/maturin-starter/src/submodule.rs @@ -15,7 +15,7 @@ impl SubmoduleClass { } } -#[pymodule(supports_free_threaded = true)] +#[pymodule] pub fn submodule(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) diff --git a/examples/plugin/plugin_api/src/lib.rs b/examples/plugin/plugin_api/src/lib.rs index 98f118523af..580c85a8c8e 100644 --- a/examples/plugin/plugin_api/src/lib.rs +++ b/examples/plugin/plugin_api/src/lib.rs @@ -25,7 +25,7 @@ impl Gadget { } /// A Python module for plugin interface types -#[pymodule(supports_free_threaded = true)] +#[pymodule] pub fn plugin_api(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) diff --git a/examples/setuptools-rust-starter/src/lib.rs b/examples/setuptools-rust-starter/src/lib.rs index 52e5f3b5a0c..a26623bc044 100644 --- a/examples/setuptools-rust-starter/src/lib.rs +++ b/examples/setuptools-rust-starter/src/lib.rs @@ -19,7 +19,7 @@ impl ExampleClass { } /// An example module implemented in Rust using PyO3. -#[pymodule(supports_free_threaded = true)] +#[pymodule] fn _setuptools_rust_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_wrapped(wrap_pymodule!(submodule::submodule))?; diff --git a/examples/setuptools-rust-starter/src/submodule.rs b/examples/setuptools-rust-starter/src/submodule.rs index 2d6ea86fa1d..f3eb174100b 100644 --- a/examples/setuptools-rust-starter/src/submodule.rs +++ b/examples/setuptools-rust-starter/src/submodule.rs @@ -15,7 +15,7 @@ impl SubmoduleClass { } } -#[pymodule(supports_free_threaded = true)] +#[pymodule] pub fn submodule(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) diff --git a/examples/word-count/src/lib.rs b/examples/word-count/src/lib.rs index 5db250a6e5d..5bc73df97a4 100644 --- a/examples/word-count/src/lib.rs +++ b/examples/word-count/src/lib.rs @@ -32,7 +32,7 @@ fn count_line(line: &str, needle: &str) -> usize { total } -#[pymodule(supports_free_threaded = true)] +#[pymodule] fn word_count(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(search, m)?)?; m.add_function(wrap_pyfunction!(search_sequential, m)?)?; From 19fca46cad9ec785556c17a2aa4e963a5cd17d78 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 28 Oct 2024 11:59:47 -0600 Subject: [PATCH 28/34] apply David's suggestion for the guide --- guide/src/free-threading.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index e1c593ead29..f00e5cf2073 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -39,11 +39,12 @@ free-threaded Python. ## Supporting free-threaded Python with PyO3 -Many simple uses of PyO3, like exposing bindings for a "pure" Rust function with -no side-effects or defining an immutable Python class, will likely work "out of -the box" on the free-threaded build. All that will be necessary is to +Many simple uses of PyO3, like exposing bindings for a "pure" Rust function +with no side-effects or defining an immutable Python class, will likely work +"out of the box" on the free-threaded build. All that will be necessary is to annotate Python modules declared by rust code in your project to declare that -they support free-threaded Python. +they support free-threaded Python, for example by declaring the module with +`#[pymodule(supports_free_threaded = true)]`. At a low-level, annotating a module sets the `Py_MOD_GIL` slot on modules defined by an extension to `Py_MOD_GIL_NOT_USED`, which allows the interpreter From 6586935ccffcd3f6a6a5f9574314841986aacd65 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 28 Oct 2024 12:00:21 -0600 Subject: [PATCH 29/34] link to raw FFI examples in the guide --- guide/src/free-threading.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index f00e5cf2073..2d96b018ad9 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -91,6 +91,13 @@ fn register_child_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { ``` +See the +[`string-sum`](https://github.com/PyO3/pyo3/tree/main/examples/string-sum) +example for how to declare free-threaded support using raw FFI calls for modules +using single-phase initialization and the +[`sequential`](https://github.com/PyO3/pyo3/tree/main/examples/sequential) +example for modules using multi-phase initialization. + ## Special considerations for the free-threaded build The free-threaded interpreter does not have a GIL, and this can make interacting From b8b7a6652ae0c54aa73d97eae954e9bfa5f60fc4 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 28 Oct 2024 13:29:04 -0600 Subject: [PATCH 30/34] fix config guards in moduleobject.rs --- pyo3-ffi/src/moduleobject.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index b3203157682..2417664a421 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -86,23 +86,23 @@ impl Default for PyModuleDef_Slot { pub const Py_mod_create: c_int = 1; pub const Py_mod_exec: c_int = 2; -#[cfg(all(not(Py_LIMITED_API), Py_3_12))] +#[cfg(Py_3_12)] pub const Py_mod_multiple_interpreters: c_int = 3; -#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +#[cfg(Py_3_13)] pub const Py_mod_gil: c_int = 4; // skipped private _Py_mod_LAST_SLOT -#[cfg(all(not(Py_LIMITED_API), Py_3_12))] +#[cfg(Py_3_12)] pub const Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED: *mut c_void = 0 as *mut c_void; -#[cfg(all(not(Py_LIMITED_API), Py_3_12))] +#[cfg(Py_3_12)] pub const Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED: *mut c_void = 1 as *mut c_void; -#[cfg(all(not(Py_LIMITED_API), Py_3_12))] +#[cfg(Py_3_12)] pub const Py_MOD_PER_INTERPRETER_GIL_SUPPORTED: *mut c_void = 2 as *mut c_void; -#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +#[cfg(Py_3_13)] pub const Py_MOD_GIL_USED: *mut c_void = 0 as *mut c_void; -#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +#[cfg(Py_3_13)] pub const Py_MOD_GIL_NOT_USED: *mut c_void = 1 as *mut c_void; #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] From 28073302270a6a126fb417e28a6537da82f21fb4 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 28 Oct 2024 13:50:00 -0600 Subject: [PATCH 31/34] rename supports_free_threaded to gil_used --- guide/src/free-threading.md | 10 +++++----- newsfragments/4588.added.md | 4 ++-- pyo3-macros-backend/src/attributes.rs | 4 ++-- pyo3-macros-backend/src/module.rs | 28 +++++++++++---------------- pytests/src/awaitable.rs | 2 +- pytests/src/buf_and_str.rs | 2 +- pytests/src/comparisons.rs | 2 +- pytests/src/datetime.rs | 2 +- pytests/src/enums.rs | 2 +- pytests/src/free_threaded_mod.rs | 2 +- pytests/src/lib.rs | 2 +- pytests/src/misc.rs | 2 +- pytests/src/objstore.rs | 2 +- pytests/src/othermod.rs | 2 +- pytests/src/path.rs | 2 +- pytests/src/pyclasses.rs | 2 +- pytests/src/pyfunctions.rs | 2 +- pytests/src/sequence.rs | 2 +- pytests/src/subclassing.rs | 2 +- src/impl_/pymodule.rs | 27 ++++++++++---------------- src/types/module.rs | 14 +++++++------- tests/test_module.rs | 6 ++---- tests/ui/invalid_pymodule_args.stderr | 2 +- 23 files changed, 55 insertions(+), 70 deletions(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 2d96b018ad9..fe364077cfb 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -44,7 +44,7 @@ with no side-effects or defining an immutable Python class, will likely work "out of the box" on the free-threaded build. All that will be necessary is to annotate Python modules declared by rust code in your project to declare that they support free-threaded Python, for example by declaring the module with -`#[pymodule(supports_free_threaded = true)]`. +`#[pymodule(gil_used = false)]`. At a low-level, annotating a module sets the `Py_MOD_GIL` slot on modules defined by an extension to `Py_MOD_GIL_NOT_USED`, which allows the interpreter @@ -62,15 +62,15 @@ GIL to remain disabled by setting the `PYTHON_GIL=0` as an environment variable or passing `-Xgil=0` when starting Python (`0` means the GIL is turned off). If you are sure that all data structures exposed in a `PyModule` are -thread-safe, then pass `supports_free_threaded = true` as a parameter to the +thread-safe, then pass `gil_used = false` as a parameter to the `pymodule` procedural macro declaring the module or call -`PyModule::supports_free_threaded` on a `PyModule` instance. For example: +`PyModule::gil_used` on a `PyModule` instance. For example: ```rust use pyo3::prelude::*; /// This module supports free-threaded Python -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { // add members to the module that you know are thread-safe Ok(()) @@ -85,7 +85,7 @@ use pyo3::prelude::*; # #[allow(dead_code)] fn register_child_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { let child_module = PyModule::new(parent_module.py(), "child_module")?; - child_module.supports_free_threaded(true)?; + child_module.gil_used(false)?; parent_module.add_submodule(&child_module) } diff --git a/newsfragments/4588.added.md b/newsfragments/4588.added.md index f42453d33d9..42b5b8e219a 100644 --- a/newsfragments/4588.added.md +++ b/newsfragments/4588.added.md @@ -1,3 +1,3 @@ * It is now possible to declare that a module supports the free-threaded build - by either calling `PyModule::supports_free_threaded` or passing - `supports_free_threaded = true` as a parameter to the `pymodule` proc macro. + by either calling `PyModule::gil_used` or passing + `gil_used = false` as a parameter to the `pymodule` proc macro. diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 1c9c5b1ec53..6fe75e44302 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -44,7 +44,7 @@ pub mod kw { syn::custom_keyword!(transparent); syn::custom_keyword!(unsendable); syn::custom_keyword!(weakref); - syn::custom_keyword!(supports_free_threaded); + syn::custom_keyword!(gil_used); } fn take_int(read: &mut &str, tracker: &mut usize) -> String { @@ -309,7 +309,7 @@ pub type RenameAllAttribute = KeywordAttribute; pub type TextSignatureAttribute = KeywordAttribute; pub type SubmoduleAttribute = kw::submodule; -pub type FreeThreadedAttribute = KeywordAttribute; +pub type GILUsedAttribute = KeywordAttribute; impl Parse for KeywordAttribute { fn parse(input: ParseStream<'_>) -> Result { diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 8fd52582e26..62ff30613a0 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -2,7 +2,7 @@ use crate::{ attributes::{ - self, kw, take_attributes, take_pyo3_options, CrateAttribute, FreeThreadedAttribute, + self, kw, take_attributes, take_pyo3_options, CrateAttribute, GILUsedAttribute, ModuleAttribute, NameAttribute, SubmoduleAttribute, }, get_doc, @@ -29,7 +29,7 @@ pub struct PyModuleOptions { name: Option, module: Option, submodule: Option, - supports_free_threaded: Option, + gil_used: Option, } impl Parse for PyModuleOptions { @@ -73,8 +73,8 @@ impl PyModuleOptions { submodule, " (it is implicitly always specified for nested modules)" ), - PyModulePyO3Option::SupportsFreeThreaded(supports_free_threaded) => { - set_option!(supports_free_threaded) + PyModulePyO3Option::GILUsed(gil_used) => { + set_option!(gil_used) } } } @@ -353,10 +353,7 @@ pub fn pymodule_module_impl( ctx, module_def, options.submodule.is_some(), - options - .supports_free_threaded - .map(|op| op.value.value) - .unwrap_or(false), + options.gil_used.map(|op| op.value.value).unwrap_or(true), ); Ok(quote!( @@ -401,10 +398,7 @@ pub fn pymodule_function_impl( ctx, quote! { MakeDef::make_def() }, false, - options - .supports_free_threaded - .map(|op| op.value.value) - .unwrap_or(false), + options.gil_used.map(|op| op.value.value).unwrap_or(true), ); // Module function called with optional Python<'_> marker as first arg, followed by the module. @@ -450,7 +444,7 @@ fn module_initialization( ctx: &Ctx, module_def: TokenStream, is_submodule: bool, - supports_free_threaded: bool, + gil_used: bool, ) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let pyinit_symbol = format!("PyInit_{}", name); @@ -472,7 +466,7 @@ fn module_initialization( #[doc(hidden)] #[export_name = #pyinit_symbol] pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject { - #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py, #supports_free_threaded)) + #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py, #gil_used)) } }); } @@ -619,7 +613,7 @@ enum PyModulePyO3Option { Crate(CrateAttribute), Name(NameAttribute), Module(ModuleAttribute), - SupportsFreeThreaded(FreeThreadedAttribute), + GILUsed(GILUsedAttribute), } impl Parse for PyModulePyO3Option { @@ -633,8 +627,8 @@ impl Parse for PyModulePyO3Option { input.parse().map(PyModulePyO3Option::Module) } else if lookahead.peek(attributes::kw::submodule) { input.parse().map(PyModulePyO3Option::Submodule) - } else if lookahead.peek(attributes::kw::supports_free_threaded) { - input.parse().map(PyModulePyO3Option::SupportsFreeThreaded) + } else if lookahead.peek(attributes::kw::gil_used) { + input.parse().map(PyModulePyO3Option::GILUsed) } else { Err(lookahead.error()) } diff --git a/pytests/src/awaitable.rs b/pytests/src/awaitable.rs index 4dd0313c56c..fb04c33ed05 100644 --- a/pytests/src/awaitable.rs +++ b/pytests/src/awaitable.rs @@ -78,7 +78,7 @@ impl FutureAwaitable { } } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn awaitable(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/buf_and_str.rs b/pytests/src/buf_and_str.rs index 3442c121a78..15230a5e153 100644 --- a/pytests/src/buf_and_str.rs +++ b/pytests/src/buf_and_str.rs @@ -47,7 +47,7 @@ fn return_memoryview(py: Python<'_>) -> PyResult> { PyMemoryView::from(&bytes) } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn buf_and_str(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(return_memoryview, m)?)?; diff --git a/pytests/src/comparisons.rs b/pytests/src/comparisons.rs index 8cc1b728a5e..4ed79e42790 100644 --- a/pytests/src/comparisons.rs +++ b/pytests/src/comparisons.rs @@ -112,7 +112,7 @@ impl OrderedDefaultNe { } } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn comparisons(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/datetime.rs b/pytests/src/datetime.rs index 1fc8beb42e8..5162b3508a5 100644 --- a/pytests/src/datetime.rs +++ b/pytests/src/datetime.rs @@ -203,7 +203,7 @@ impl TzClass { } } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn datetime(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(make_date, m)?)?; m.add_function(wrap_pyfunction!(get_date_tuple, m)?)?; diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs index c18a839deb1..8652321700a 100644 --- a/pytests/src/enums.rs +++ b/pytests/src/enums.rs @@ -4,7 +4,7 @@ use pyo3::{ wrap_pyfunction, Bound, PyResult, }; -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn enums(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/free_threaded_mod.rs b/pytests/src/free_threaded_mod.rs index 5ef7715775b..e0b44616749 100644 --- a/pytests/src/free_threaded_mod.rs +++ b/pytests/src/free_threaded_mod.rs @@ -5,7 +5,7 @@ fn add_two(x: usize) -> usize { x + 2 } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn free_threaded_mod(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(add_two, m)?)?; Ok(()) diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index 736d870f6cf..8a467fe3554 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -18,7 +18,7 @@ pub mod pyfunctions; pub mod sequence; pub mod subclassing; -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] fn pyo3_pytests(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(awaitable::awaitable))?; #[cfg(not(Py_LIMITED_API))] diff --git a/pytests/src/misc.rs b/pytests/src/misc.rs index bc42ee81db8..e44d1aa0ecf 100644 --- a/pytests/src/misc.rs +++ b/pytests/src/misc.rs @@ -32,7 +32,7 @@ fn get_item_and_run_callback(dict: Bound<'_, PyDict>, callback: Bound<'_, PyAny> Ok(()) } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn misc(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(issue_219, m)?)?; m.add_function(wrap_pyfunction!(get_type_fully_qualified_name, m)?)?; diff --git a/pytests/src/objstore.rs b/pytests/src/objstore.rs index acf288e36b0..8e729052992 100644 --- a/pytests/src/objstore.rs +++ b/pytests/src/objstore.rs @@ -18,7 +18,7 @@ impl ObjStore { } } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn objstore(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::() } diff --git a/pytests/src/othermod.rs b/pytests/src/othermod.rs index 394e9e013da..0de912d7d04 100644 --- a/pytests/src/othermod.rs +++ b/pytests/src/othermod.rs @@ -28,7 +28,7 @@ fn double(x: i32) -> i32 { x * 2 } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn othermod(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; diff --git a/pytests/src/path.rs b/pytests/src/path.rs index d3f875ba165..b52c038ed34 100644 --- a/pytests/src/path.rs +++ b/pytests/src/path.rs @@ -11,7 +11,7 @@ fn take_pathbuf(path: PathBuf) -> PathBuf { path } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn path(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(make_path, m)?)?; m.add_function(wrap_pyfunction!(take_pathbuf, m)?)?; diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index ed9b78c5d4e..3af08c053cc 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -104,7 +104,7 @@ impl ClassWithDict { } } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/pyfunctions.rs b/pytests/src/pyfunctions.rs index abb3cc739d0..024641d3d2e 100644 --- a/pytests/src/pyfunctions.rs +++ b/pytests/src/pyfunctions.rs @@ -67,7 +67,7 @@ fn args_kwargs<'py>( (args, kwargs) } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn pyfunctions(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(none, m)?)?; m.add_function(wrap_pyfunction!(simple, m)?)?; diff --git a/pytests/src/sequence.rs b/pytests/src/sequence.rs index b3f38f7db58..175f5fba8aa 100644 --- a/pytests/src/sequence.rs +++ b/pytests/src/sequence.rs @@ -16,7 +16,7 @@ fn vec_to_vec_pystring(vec: Vec>) -> Vec vec } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn sequence(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(vec_to_vec_i32, m)?)?; m.add_function(wrap_pyfunction!(array_to_array_i32, m)?)?; diff --git a/pytests/src/subclassing.rs b/pytests/src/subclassing.rs index 842d11a2151..0f00e74c19d 100644 --- a/pytests/src/subclassing.rs +++ b/pytests/src/subclassing.rs @@ -17,7 +17,7 @@ impl Subclassable { } } -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] pub fn subclassing(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 30c72c3e998..08b1ead7584 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -45,7 +45,7 @@ pub struct ModuleDef { /// Initialized module object, cached to avoid reinitialization. module: GILOnceCell>, /// Whether or not the module supports running without the GIL - supports_free_threaded: AtomicBool, + gil_used: AtomicBool, } /// Wrapper to enable initializer to be used in const fns. @@ -90,16 +90,12 @@ impl ModuleDef { ))] interpreter: AtomicI64::new(-1), module: GILOnceCell::new(), - supports_free_threaded: AtomicBool::new(false), + gil_used: AtomicBool::new(true), } } /// Builds a module using user given initializer. Used for [`#[pymodule]`][crate::pymodule]. #[cfg_attr(any(Py_LIMITED_API, not(Py_GIL_DISABLED)), allow(unused_variables))] - pub fn make_module( - &'static self, - py: Python<'_>, - supports_free_threaded: bool, - ) -> PyResult> { + pub fn make_module(&'static self, py: Python<'_>, gil_used: bool) -> PyResult> { // Check the interpreter ID has not changed, since we currently have no way to guarantee // that static data is not reused across interpreters. // @@ -147,14 +143,14 @@ impl ModuleDef { }; #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] { - let gil_used = { - if supports_free_threaded { - ffi::Py_MOD_GIL_NOT_USED - } else { + let gil_used_ptr = { + if gil_used { ffi::Py_MOD_GIL_USED + } else { + ffi::Py_MOD_GIL_NOT_USED } }; - if unsafe { ffi::PyUnstable_Module_SetGIL(module.as_ptr(), gil_used) } < 0 { + if unsafe { ffi::PyUnstable_Module_SetGIL(module.as_ptr(), gil_used_ptr) } < 0 { return Err(PyErr::fetch(py)); } } @@ -215,11 +211,8 @@ impl PyAddToModule for PyMethodDef { impl PyAddToModule for ModuleDef { fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_submodule( - self.make_module( - module.py(), - self.supports_free_threaded.load(Ordering::Relaxed), - )? - .bind(module.py()), + self.make_module(module.py(), self.gil_used.load(Ordering::Relaxed))? + .bind(module.py()), ) } } diff --git a/src/types/module.rs b/src/types/module.rs index 86693a32fb1..3ff5bb40691 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -405,10 +405,10 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// ```rust /// use pyo3::prelude::*; /// - /// #[pymodule(supports_free_threaded = true)] + /// #[pymodule(gil_used = false)] /// fn my_module(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { /// let submodule = PyModule::new(py, "submodule")?; - /// submodule.supports_free_threaded(true)?; + /// submodule.gil_used(false)?; /// module.add_submodule(&submodule)?; /// Ok(()) /// } @@ -420,7 +420,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// `Py_MOD_GIL_NOT_USED`. /// /// This is a no-op on the GIL-enabled build. - fn supports_free_threaded(&self, supports_free_threaded: bool) -> PyResult<()>; + fn gil_used(&self, gil_used: bool) -> PyResult<()>; } impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { @@ -549,12 +549,12 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { } #[cfg_attr(any(Py_LIMITED_API, not(Py_GIL_DISABLED)), allow(unused_variables))] - fn supports_free_threaded(&self, supports_free_threaded: bool) -> PyResult<()> { + fn gil_used(&self, gil_used: bool) -> PyResult<()> { #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] { - let gil_used = match supports_free_threaded { - true => ffi::Py_MOD_GIL_NOT_USED, - false => ffi::Py_MOD_GIL_USED, + let gil_used = match gil_used { + true => ffi::Py_MOD_GIL_USED, + false => ffi::Py_MOD_GIL_NOT_USED, }; match unsafe { ffi::PyUnstable_Module_SetGIL(self.as_ptr(), gil_used) } { c_int::MIN..=-1 => Err(PyErr::fetch(self.py())), diff --git a/tests/test_module.rs b/tests/test_module.rs index d69fe1d3f7b..36d21abe9b0 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -37,7 +37,7 @@ fn double(x: usize) -> usize { } /// This module is implemented in Rust. -#[pymodule(supports_free_threaded = true)] +#[pymodule(gil_used = false)] fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m)] #[pyo3(name = "no_parameters")] @@ -182,9 +182,7 @@ fn test_module_from_code_bound() { .extract() .expect("The value should be able to be converted to an i32"); - adder_mod - .supports_free_threaded(true) - .expect("Disabling the GIL failed"); + adder_mod.gil_used(false).expect("Disabling the GIL failed"); assert_eq!(ret_value, 3); }); diff --git a/tests/ui/invalid_pymodule_args.stderr b/tests/ui/invalid_pymodule_args.stderr index 1144bbf8d8d..23a5109b4cb 100644 --- a/tests/ui/invalid_pymodule_args.stderr +++ b/tests/ui/invalid_pymodule_args.stderr @@ -1,4 +1,4 @@ -error: expected one of: `name`, `crate`, `module`, `submodule`, `supports_free_threaded` +error: expected one of: `name`, `crate`, `module`, `submodule`, `gil_used` --> tests/ui/invalid_pymodule_args.rs:3:12 | 3 | #[pymodule(some_arg)] From a195ff0d7707a1f39e4e5eb98610d8ed8bfe7f1e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 28 Oct 2024 14:00:26 -0600 Subject: [PATCH 32/34] remove ensure_gil_enabled from pyo3-ffi/build.rs --- pyo3-ffi/build.rs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 75e7f0eb01c..931838b5e5d 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -6,7 +6,6 @@ use pyo3_build_config::{ }, warn, BuildFlag, PythonImplementation, }; -use std::ops::Not; /// Minimum Python version PyO3 supports. struct SupportedVersions { @@ -107,7 +106,17 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { if interpreter_config.abi3 { match interpreter_config.implementation { - PythonImplementation::CPython => {} + PythonImplementation::CPython => { + if interpreter_config + .build_flags + .0 + .contains(&BuildFlag::Py_GIL_DISABLED) + { + warn!( + "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." + ) + } + } PythonImplementation::PyPy => warn!( "PyPy does not yet support abi3 so the build artifacts will be version-specific. \ See https://github.com/pypy/pypy/issues/3397 for more information." @@ -121,19 +130,6 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { Ok(()) } -fn ensure_gil_enabled(interpreter_config: &InterpreterConfig) { - let gil_enabled = interpreter_config - .build_flags - .0 - .contains(&BuildFlag::Py_GIL_DISABLED) - .not(); - if !gil_enabled && interpreter_config.abi3 { - warn!( - "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." - ) - } -} - fn ensure_target_pointer_width(interpreter_config: &InterpreterConfig) -> Result<()> { if let Some(pointer_width) = interpreter_config.pointer_width { // Try to check whether the target architecture matches the python library @@ -199,7 +195,6 @@ fn configure_pyo3() -> Result<()> { ensure_python_version(&interpreter_config)?; ensure_target_pointer_width(&interpreter_config)?; - ensure_gil_enabled(&interpreter_config); // Serialize the whole interpreter config into DEP_PYTHON_PYO3_CONFIG env var. interpreter_config.to_cargo_dep_env()?; From b9f9417567156d9824d6071d8f54f2df0744ca4e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 28 Oct 2024 14:03:25 -0600 Subject: [PATCH 33/34] update docs for PyModule::gil_used --- src/types/module.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types/module.rs b/src/types/module.rs index 3ff5bb40691..d3e59c85198 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -390,13 +390,13 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// Declare whether or not this module supports running with the GIL disabled /// - /// If the module does not rely on the GIL for thread safety, you can pass True - /// to this function to indicate the module does not rely on the GIL for - /// thread-safety. + /// If the module does not rely on the GIL for thread safety, you can pass + /// `false` to this function to indicate the module does not rely on the GIL + /// for thread-safety. /// /// This function sets the [`Py_MOD_GIL` /// slot](https://docs.python.org/3/c-api/module.html#c.Py_mod_gil) on the - /// module object. The default is `Py_MOD_GIL_USED`, so passing `false` to + /// module object. The default is `Py_MOD_GIL_USED`, so passing `true` to /// this function is a no-op unless you have already set `Py_MOD_GIL` to /// `Py_MOD_GIL_NOT_USED` elsewhere. /// From a6f59f39b76843c4c9350e0208cec25a94bdf576 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 31 Oct 2024 13:16:11 -0600 Subject: [PATCH 34/34] remove UNSAFE_PYO3_BUILD_FREE_THREADED from the CI config --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcc4dd8bf0b..eba8676f01d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -553,8 +553,6 @@ jobs: test-free-threaded: needs: [fmt] runs-on: ubuntu-latest - env: - UNSAFE_PYO3_BUILD_FREE_THREADED: 1 steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2