Skip to content

Commit 59707f0

Browse files
committed
auto-initialize: new feature to control initializing Python
1 parent 42ca48a commit 59707f0

File tree

7 files changed

+128
-49
lines changed

7 files changed

+128
-49
lines changed

.github/workflows/ci.yml

+11-7
Original file line numberDiff line numberDiff line change
@@ -85,27 +85,28 @@ jobs:
8585
run: echo LD_LIBRARY_PATH=${pythonLocation}/lib >> $GITHUB_ENV
8686

8787
- name: Build docs
88-
run: cargo doc --features "num-bigint num-complex hashbrown" --verbose --target ${{ matrix.platform.rust-target }}
88+
run: cargo doc --no-default-features --features "macros num-bigint num-complex hashbrown" --verbose --target ${{ matrix.platform.rust-target }}
8989

90-
- name: Build without default features
90+
- name: Build (no features)
9191
run: cargo build --no-default-features --verbose --target ${{ matrix.platform.rust-target }}
9292

93-
- name: Build with default features
94-
run: cargo build --features "num-bigint num-complex hashbrown" --verbose --target ${{ matrix.platform.rust-target }}
93+
- name: Build (all additive features)
94+
run: cargo build --no-default-features --features "macros num-bigint num-complex hashbrown" --verbose --target ${{ matrix.platform.rust-target }}
9595

9696
# Run tests (except on PyPy, because no embedding API).
9797
- if: matrix.python-version != 'pypy-3.6'
9898
name: Test
99-
run: cargo test --features "num-bigint num-complex hashbrown" --target ${{ matrix.platform.rust-target }}
99+
run: cargo test --no-default-features --features "macros num-bigint num-complex hashbrown" --target ${{ matrix.platform.rust-target }}
100+
100101
# Run tests again, but in abi3 mode
101102
- if: matrix.python-version != 'pypy-3.6'
102103
name: Test (abi3)
103-
run: cargo test --no-default-features --features "abi3,macros" --target ${{ matrix.platform.rust-target }}
104+
run: cargo test --no-default-features --features "abi3 macros num-bigint num-complex hashbrown" --target ${{ matrix.platform.rust-target }}
104105

105106
# Run tests again, for abi3-py36 (the minimal Python version)
106107
- if: (matrix.python-version != 'pypy-3.6') && (matrix.python-version != '3.6')
107108
name: Test (abi3-py36)
108-
run: cargo test --no-default-features --features "abi3-py36,macros" --target ${{ matrix.platform.rust-target }}
109+
run: cargo test --no-default-features --features "abi3-py36 macros num-bigint num-complex hashbrown" --target ${{ matrix.platform.rust-target }}
109110

110111
- name: Test proc-macro code
111112
run: cargo test --manifest-path=pyo3-macros-backend/Cargo.toml --target ${{ matrix.platform.rust-target }}
@@ -125,6 +126,9 @@ jobs:
125126
env:
126127
RUST_BACKTRACE: 1
127128
RUSTFLAGS: "-D warnings"
129+
# TODO: this is a hack to workaround compile_error! warnings about auto-initialize on PyPy
130+
# Once cargo's `resolver = "2"` is stable (~ MSRV Rust 1.52), remove this.
131+
PYO3_CI: 1
128132

129133
coverage:
130134
needs: [fmt]

Cargo.toml

+17-7
Original file line numberDiff line numberDiff line change
@@ -33,26 +33,36 @@ assert_approx_eq = "1.1.0"
3333
trybuild = "1.0.23"
3434
rustversion = "1.0"
3535
proptest = { version = "0.10.1", default-features = false, features = ["std"] }
36+
# features needed to run the PyO3 test suite
37+
pyo3 = { path = ".", default-features = false, features = ["macros", "auto-initialize"] }
3638

3739
[features]
38-
default = ["macros"]
39-
macros = ["ctor", "indoc", "inventory", "paste", "pyo3-macros", "unindent"]
40+
default = ["macros", "auto-initialize"]
41+
42+
# Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc.
43+
macros = ["pyo3-macros", "ctor", "indoc", "inventory", "paste", "unindent"]
44+
45+
# Use this feature when building an extension module.
46+
# It tells the linker to keep the python symbols unresolved,
47+
# so that the module can also be used with statically linked python interpreters.
48+
extension-module = []
49+
4050
# Use the Python limited API. See https://www.python.org/dev/peps/pep-0384/ for more.
4151
abi3 = []
52+
4253
# With abi3, we can manually set the minimum Python version.
4354
abi3-py36 = ["abi3-py37"]
4455
abi3-py37 = ["abi3-py38"]
4556
abi3-py38 = ["abi3-py39"]
4657
abi3-py39 = ["abi3"]
4758

59+
# Changes `Python::with_gil` and `Python::acquire_gil` to automatically initialize the
60+
# Python interpreter if needed.
61+
auto-initialize = []
62+
4863
# Optimizes PyObject to Vec conversion and so on.
4964
nightly = []
5065

51-
# Use this feature when building an extension module.
52-
# It tells the linker to keep the python symbols unresolved,
53-
# so that the module can also be used with statically linked python interpreters.
54-
extension-module = []
55-
5666
[workspace]
5767
members = [
5868
"pyo3-macros",

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,9 @@ If you want your Rust application to create a Python interpreter internally and
104104
use it to run Python code, add `pyo3` to your `Cargo.toml` like this:
105105

106106
```toml
107-
[dependencies]
108-
pyo3 = "0.13.0"
107+
[dependencies.pyo3]
108+
version = "0.13.0"
109+
features = ["auto-initialize"]
109110
```
110111

111112
Example program displaying the value of `sys.version` and the current user name:

build.rs

+10
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,10 @@ fn configure(interpreter_config: &InterpreterConfig) -> Result<()> {
749749
}
750750
}
751751

752+
if interpreter_config.shared {
753+
println!("cargo:rustc-cfg=Py_SHARED");
754+
}
755+
752756
if interpreter_config.version.implementation == PythonInterpreterKind::PyPy {
753757
println!("cargo:rustc-cfg=PyPy");
754758
};
@@ -883,5 +887,11 @@ fn main() -> Result<()> {
883887
}
884888
}
885889

890+
// TODO: this is a hack to workaround compile_error! warnings about auto-initialize on PyPy
891+
// Once cargo's `resolver = "2"` is stable (~ MSRV Rust 1.52), remove this.
892+
if env::var_os("PYO3_CI").is_some() {
893+
println!("cargo:rustc-cfg=__pyo3_ci");
894+
}
895+
886896
Ok(())
887897
}

src/gil.rs

+67-24
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
//! Interaction with python's global interpreter lock
44
55
use crate::{ffi, internal_tricks::Unsendable, Python};
6-
use parking_lot::{const_mutex, Mutex};
6+
use parking_lot::{const_mutex, Mutex, Once};
77
use std::cell::{Cell, RefCell};
8-
use std::{mem::ManuallyDrop, ptr::NonNull, sync};
8+
use std::{mem::ManuallyDrop, ptr::NonNull};
99

10-
static START: sync::Once = sync::Once::new();
10+
static START: Once = Once::new();
1111

1212
thread_local! {
1313
/// This is a internal counter in pyo3 monitoring whether this thread has the GIL.
@@ -45,16 +45,20 @@ pub(crate) fn gil_is_acquired() -> bool {
4545
/// If both the Python interpreter and Python threading are already initialized,
4646
/// this function has no effect.
4747
///
48+
/// # Availability
49+
///
50+
/// This function is only available when linking against Python distributions that contain a
51+
/// shared library.
52+
///
53+
/// This function is not available on PyPy.
54+
///
4855
/// # Panic
4956
/// If the Python interpreter is initialized but Python threading is not,
5057
/// a panic occurs.
5158
/// It is not possible to safely access the Python runtime unless the main
5259
/// thread (the thread which originally initialized Python) also initializes
5360
/// threading.
54-
///
55-
/// When writing an extension module, the `#[pymodule]` macro
56-
/// will ensure that Python threading is initialized.
57-
///
61+
#[cfg(all(Py_SHARED, not(PyPy)))]
5862
pub fn prepare_freethreaded_python() {
5963
// Protect against race conditions when Python is not yet initialized
6064
// and multiple threads concurrently call 'prepare_freethreaded_python()'.
@@ -72,34 +76,29 @@ pub fn prepare_freethreaded_python() {
7276
// Note that the 'main thread' notion in Python isn't documented properly;
7377
// and running Python without one is not officially supported.
7478

75-
// PyPy does not support the embedding API
76-
#[cfg(not(PyPy))]
77-
{
78-
ffi::Py_InitializeEx(0);
79-
80-
// Make sure Py_Finalize will be called before exiting.
81-
extern "C" fn finalize() {
82-
unsafe {
83-
if ffi::Py_IsInitialized() != 0 {
84-
ffi::PyGILState_Ensure();
85-
ffi::Py_Finalize();
86-
}
79+
ffi::Py_InitializeEx(0);
80+
81+
// Make sure Py_Finalize will be called before exiting.
82+
extern "C" fn finalize() {
83+
unsafe {
84+
if ffi::Py_IsInitialized() != 0 {
85+
ffi::PyGILState_Ensure();
86+
ffi::Py_Finalize();
8787
}
8888
}
89-
libc::atexit(finalize);
9089
}
90+
libc::atexit(finalize);
9191

9292
// > Changed in version 3.7: This function is now called by Py_Initialize(), so you don’t have
9393
// > to call it yourself anymore.
9494
#[cfg(not(Py_3_7))]
9595
if ffi::PyEval_ThreadsInitialized() == 0 {
9696
ffi::PyEval_InitThreads();
9797
}
98-
// PyEval_InitThreads() will acquire the GIL,
99-
// but we don't want to hold it at this point
98+
99+
// Py_InitializeEx() will acquire the GIL, but we don't want to hold it at this point
100100
// (it's not acquired in the other code paths)
101101
// So immediately release the GIL:
102-
#[cfg(not(PyPy))]
103102
let _thread_state = ffi::PyEval_SaveThread();
104103
// Note that the PyThreadState returned by PyEval_SaveThread is also held in TLS by the Python runtime,
105104
// and will be restored by PyGILState_Ensure.
@@ -137,7 +136,51 @@ impl GILGuard {
137136
/// If PyO3 does not yet have a `GILPool` for tracking owned PyObject references, then this
138137
/// new `GILGuard` will also contain a `GILPool`.
139138
pub(crate) fn acquire() -> GILGuard {
140-
prepare_freethreaded_python();
139+
// Maybe auto-initialize the GIL:
140+
// - If auto-initialize feature set and supported, try to initalize the interpreter.
141+
// - If the auto-initialize feature is set but unsupported, emit hard errors only when
142+
// the extension-module feature is not activated - extension modules don't care about
143+
// auto-initialize so this avoids breaking existing builds.
144+
// - Otherwise, just check the GIL is initialized.
145+
cfg_if::cfg_if! {
146+
if #[cfg(all(feature = "auto-initialize", Py_SHARED, not(PyPy)))] {
147+
prepare_freethreaded_python();
148+
} else if #[cfg(all(feature = "auto-initialize", not(feature = "extension-module"), not(Py_SHARED), not(__pyo3_ci)))] {
149+
compile_error!(concat!(
150+
"The `auto-initialize` feature is not supported when linking Python ",
151+
"statically instead of with a shared library.\n\n",
152+
"Please disable the `auto-initialize` feature, for example by entering the following ",
153+
"in your cargo.toml:\n\n",
154+
" pyo3 = { version = \"0.13.0\", default-features = false }\n\n",
155+
"Alternatively, compile PyO3 using a Python distribution which contains a shared ",
156+
"libary."
157+
));
158+
} else if #[cfg(all(feature = "auto-initialize", not(feature = "extension-module"), PyPy, not(__pyo3_ci)))] {
159+
compile_error!(concat!(
160+
"The `auto-initialize` feature is not supported by PyPy.\n\n",
161+
"Please disable the `auto-initialize` feature, for example by entering the following ",
162+
"in your cargo.toml:\n\n",
163+
" pyo3 = { version = \"0.13.0\", default-features = false }",
164+
));
165+
} else {
166+
// extension module feature enabled and PyPy or static linking
167+
// OR auto-initialize feature not enabled
168+
START.call_once_force(|_| unsafe {
169+
// Use call_once_force because if there is a panic because the interpreter is not
170+
// initialized, it's fine for the user to initialize the interpreter and retry.
171+
assert_ne!(
172+
ffi::Py_IsInitialized(),
173+
0,
174+
"The Python interpreter is not initalized and the `auto-initialize` feature is not enabled."
175+
);
176+
assert_ne!(
177+
ffi::PyEval_ThreadsInitialized(),
178+
0,
179+
"Python threading is not initalized and the `auto-initialize` feature is not enabled."
180+
);
181+
});
182+
}
183+
}
141184

142185
let gstate = unsafe { ffi::PyGILState_Ensure() }; // acquire GIL
143186

src/lib.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,9 @@
114114
//! Add `pyo3` to your `Cargo.toml`:
115115
//!
116116
//! ```toml
117-
//! [dependencies]
118-
//! pyo3 = "0.13.0"
117+
//! [dependencies.pyo3]
118+
//! version = "0.13.0"
119+
//! features = ["auto-initialize"]
119120
//! ```
120121
//!
121122
//! Example program displaying the value of `sys.version`:
@@ -145,12 +146,14 @@ pub use crate::conversion::{
145146
ToBorrowedObject, ToPyObject,
146147
};
147148
pub use crate::err::{PyDowncastError, PyErr, PyErrArguments, PyResult};
149+
#[cfg(all(Py_SHARED, not(PyPy)))]
150+
pub use crate::gil::prepare_freethreaded_python;
148151
pub use crate::gil::{GILGuard, GILPool};
149152
pub use crate::instance::{Py, PyNativeType, PyObject};
150153
pub use crate::pycell::{PyCell, PyRef, PyRefMut};
151154
pub use crate::pyclass::PyClass;
152155
pub use crate::pyclass_init::PyClassInitializer;
153-
pub use crate::python::{prepare_freethreaded_python, Python, PythonVersionInfo};
156+
pub use crate::python::{Python, PythonVersionInfo};
154157
pub use crate::type_object::{type_flags, PyTypeInfo};
155158
// Since PyAny is as important as PyObject, we expose it to the top level.
156159
pub use crate::types::PyAny;

src/python.rs

+14-6
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ use std::ffi::{CStr, CString};
1111
use std::marker::PhantomData;
1212
use std::os::raw::{c_char, c_int};
1313

14-
pub use gil::prepare_freethreaded_python;
15-
1614
/// Represents the major, minor, and patch (if any) versions of this interpreter.
1715
///
1816
/// See [Python::version].
@@ -134,8 +132,13 @@ impl Python<'_> {
134132
/// Acquires the global interpreter lock, which allows access to the Python runtime. The
135133
/// provided closure F will be executed with the acquired `Python` marker token.
136134
///
137-
/// If the Python runtime is not already initialized, this function will initialize it.
138-
/// See [prepare_freethreaded_python()](fn.prepare_freethreaded_python.html) for details.
135+
/// If the `auto-initialize` feature is enabled and the Python runtime is not already
136+
/// initialized, this function will initialize it. See
137+
/// [prepare_freethreaded_python()](fn.prepare_freethreaded_python.html) for details.
138+
///
139+
/// # Panics
140+
/// - If the `auto-initialize` feature is not enabled and the Python interpreter is not
141+
/// initialized.
139142
///
140143
/// # Example
141144
/// ```
@@ -158,8 +161,9 @@ impl Python<'_> {
158161
impl<'p> Python<'p> {
159162
/// Acquires the global interpreter lock, which allows access to the Python runtime.
160163
///
161-
/// If the Python runtime is not already initialized, this function will initialize it.
162-
/// See [prepare_freethreaded_python()](fn.prepare_freethreaded_python.html) for details.
164+
/// If the `auto-initialize` feature is enabled and the Python runtime is not already
165+
/// initialized, this function will initialize it. See
166+
/// [prepare_freethreaded_python()](fn.prepare_freethreaded_python.html) for details.
163167
///
164168
/// Most users should not need to use this API directly, and should prefer one of two options:
165169
/// 1. When implementing `#[pymethods]` or `#[pyfunction]` add a function argument
@@ -172,6 +176,10 @@ impl<'p> Python<'p> {
172176
/// allowed, and will not deadlock. However, `GILGuard`s must be dropped in the reverse order
173177
/// to acquisition. If PyO3 detects this order is not maintained, it may be forced to begin
174178
/// an irrecoverable panic.
179+
///
180+
/// # Panics
181+
/// - If the `auto-initialize` feature is not enabled and the Python interpreter is not
182+
/// initialized.
175183
#[inline]
176184
pub fn acquire_gil() -> GILGuard {
177185
GILGuard::acquire()

0 commit comments

Comments
 (0)