Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Declare free-threaded support for PyModule #4588

Merged
merged 41 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b120d45
WIP: declare free-threaded support in pymodule macro
ngoldbaum Oct 1, 2024
5218108
ignore ruff lint about unused import
ngoldbaum Oct 21, 2024
1a52a05
eliminate gil re-enabling in pytests
ngoldbaum Oct 21, 2024
cabf707
fix clippy nit
ngoldbaum Oct 21, 2024
fed9ebd
fix return type of PyUnstable_Module_SetGIL binding
ngoldbaum Oct 21, 2024
67c54b8
add a way to declare free-threaded support without macros
ngoldbaum Oct 21, 2024
d36b088
fix ruff
ngoldbaum Oct 21, 2024
7145100
fix changed ui test answer
ngoldbaum Oct 21, 2024
faf3aa0
fix build issues on old python versions
ngoldbaum Oct 21, 2024
698250b
fix runtime warnings in examples
ngoldbaum Oct 21, 2024
7ec3c15
ensure that the GIL does not get re-enabled in the pytests
ngoldbaum Oct 21, 2024
78db4a4
add changelog entry
ngoldbaum Oct 21, 2024
447231f
fix ruff
ngoldbaum Oct 21, 2024
5940fb8
fix compiler error on older pythons
ngoldbaum Oct 21, 2024
ebdcf27
fix clippy
ngoldbaum Oct 21, 2024
a51ca89
really fix clippy and expose supports_free_threaded on all builds
ngoldbaum Oct 22, 2024
4a6005c
fix clippy and msrv
ngoldbaum Oct 22, 2024
72c3032
fix examples on gil-disabled python
ngoldbaum Oct 22, 2024
33fb8cb
fix free-threaded clippy
ngoldbaum Oct 22, 2024
04a48d8
fix unused import in example
ngoldbaum Oct 22, 2024
d2d3cb4
Add pyo3-build-config as a build dependency to examples that need it
ngoldbaum Oct 23, 2024
9f0688d
add docs
ngoldbaum Oct 23, 2024
003aa36
add rust tests so coverage picks up the new code
ngoldbaum Oct 23, 2024
1def33b
fix some formatting issues
ngoldbaum Oct 23, 2024
d3ca9a2
Apply cleanups
ngoldbaum Oct 23, 2024
c272082
fix cargo fmt --check
ngoldbaum Oct 23, 2024
244a32c
revert changes to non-FFI examples
ngoldbaum Oct 28, 2024
1868e21
apply David's suggestion for the guide
ngoldbaum Oct 28, 2024
7495e8e
link to raw FFI examples in the guide
ngoldbaum Oct 28, 2024
3fa6f52
fix config guards in moduleobject.rs
ngoldbaum Oct 28, 2024
c34853b
rename supports_free_threaded to gil_used
ngoldbaum Oct 28, 2024
042e2bf
remove ensure_gil_enabled from pyo3-ffi/build.rs
ngoldbaum Oct 28, 2024
db912bf
update docs for PyModule::gil_used
ngoldbaum Oct 28, 2024
371ce0c
remove UNSAFE_PYO3_BUILD_FREE_THREADED from the CI config
ngoldbaum Oct 31, 2024
e38676c
fix merge conflict screwup
ngoldbaum Nov 4, 2024
f9dae80
fix nox -s test-py
ngoldbaum Nov 4, 2024
bd5473c
fix guide links
ngoldbaum Nov 4, 2024
45ba422
remove redundant pytest test
ngoldbaum Nov 4, 2024
f40dbc9
fix issue with wrap_pymodule not respecting user choice for GIL support
ngoldbaum Nov 4, 2024
1c42dbe
replace map.unwrap_or with map_or
ngoldbaum Nov 4, 2024
ff891f6
fix refcounting error in ffi example
ngoldbaum Nov 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 79 additions & 7 deletions guide/src/free-threading.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,83 @@ 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, for example by declaring the module with
`#[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
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 `gil_used = false` as a parameter to the
`pymodule` procedural macro declaring the module or call
`PyModule::gil_used` 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(gil_used = false)]
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.gil_used(false)?;
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
parent_module.add_submodule(&child_module)
}

```

See the
[`string-sum`](https://github.com/PyO3/pyo3/tree/main/pyo3-ffi/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/pyo3-ffi/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
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
Expand Down Expand Up @@ -83,7 +155,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
Expand All @@ -99,7 +171,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]
Expand Down Expand Up @@ -206,7 +278,7 @@ Python::with_gil(|py| {
# }
```

## `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
Expand Down
3 changes: 3 additions & 0 deletions newsfragments/4588.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* It is now possible to declare that a module supports the free-threaded build
by either calling `PyModule::gil_used` or passing
`gil_used = false` as a parameter to the `pymodule` proc macro.
8 changes: 0 additions & 8 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
37 changes: 11 additions & 26 deletions pyo3-ffi/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use pyo3_build_config::{
},
warn, BuildFlag, PythonImplementation,
};
use std::ops::Not;

/// Minimum Python version PyO3 supports.
struct SupportedVersions {
Expand Down Expand Up @@ -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."
Expand All @@ -121,29 +130,6 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> {
Ok(())
}

fn ensure_gil_enabled(interpreter_config: &InterpreterConfig) -> Result<()> {
let gil_enabled = interpreter_config
.build_flags
.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."
)
}

Ok(())
}

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
Expand Down Expand Up @@ -209,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()?;
Expand Down
3 changes: 3 additions & 0 deletions pyo3-ffi/examples/sequential/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
3 changes: 3 additions & 0 deletions pyo3-ffi/examples/sequential/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
pyo3_build_config::use_pyo3_cfgs();
}
5 changes: 5 additions & 0 deletions pyo3-ffi/examples/sequential/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ 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,
},
PyModuleDef_Slot {
slot: 0,
value: ptr::null_mut(),
Expand Down
3 changes: 3 additions & 0 deletions pyo3-ffi/examples/string-sum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ crate-type = ["cdylib"]
[dependencies]
pyo3-ffi = { path = "../../", features = ["extension-module"] }

[build-dependencies]
pyo3-build-config = { path = "../../../pyo3-build-config" }

[workspace]
3 changes: 3 additions & 0 deletions pyo3-ffi/examples/string-sum/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
pyo3_build_config::use_pyo3_cfgs();
}
13 changes: 12 additions & 1 deletion pyo3-ffi/examples/string-sum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,18 @@ 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;
}
#[cfg(Py_GIL_DISABLED)]
{
if PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) < 0 {
Py_DECREF(module);
return std::ptr::null_mut();
ngoldbaum marked this conversation as resolved.
Show resolved Hide resolved
}
}
module
}

/// A helper to parse function arguments
Expand Down
14 changes: 13 additions & 1 deletion pyo3-ffi/src/moduleobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ pub const Py_mod_create: c_int = 1;
pub const Py_mod_exec: c_int = 2;
#[cfg(Py_3_12)]
pub const Py_mod_multiple_interpreters: c_int = 3;
#[cfg(Py_3_13)]
pub const Py_mod_gil: c_int = 4;

// skipped private _Py_mod_LAST_SLOT

#[cfg(Py_3_12)]
pub const Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED: *mut c_void = 0 as *mut c_void;
Expand All @@ -96,7 +100,15 @@ pub const Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED: *mut c_void = 1 as *mut c_void
#[cfg(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(Py_3_13)]
pub const Py_MOD_GIL_USED: *mut c_void = 0 as *mut c_void;
#[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))]
ngoldbaum marked this conversation as resolved.
Show resolved Hide resolved
extern "C" {
pub fn PyUnstable_Module_SetGIL(module: *mut PyObject, gil: *mut c_void) -> c_int;
}

#[repr(C)]
pub struct PyModuleDef {
Expand Down
4 changes: 3 additions & 1 deletion pyo3-macros-backend/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -44,6 +44,7 @@ pub mod kw {
syn::custom_keyword!(transparent);
syn::custom_keyword!(unsendable);
syn::custom_keyword!(weakref);
syn::custom_keyword!(gil_used);
}

fn take_int(read: &mut &str, tracker: &mut usize) -> String {
Expand Down Expand Up @@ -308,6 +309,7 @@ pub type RenameAllAttribute = KeywordAttribute<kw::rename_all, RenamingRuleLitSt
pub type StrFormatterAttribute = OptionalKeywordAttribute<kw::str, StringFormatter>;
pub type TextSignatureAttribute = KeywordAttribute<kw::text_signature, TextSignatureAttributeValue>;
pub type SubmoduleAttribute = kw::submodule;
pub type GILUsedAttribute = KeywordAttribute<kw::gil_used, LitBool>;

impl<K: Parse + std::fmt::Debug, V: Parse> Parse for KeywordAttribute<K, V> {
fn parse(input: ParseStream<'_>) -> Result<Self> {
Expand Down
Loading
Loading