From f17e70316751285340508d0009103570af7e0873 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:40:43 +0200 Subject: [PATCH] return existing module on Python 3.9 and up --- guide/src/building_and_distribution.md | 2 +- newsfragments/3446.changed.md | 2 +- pyo3-build-config/src/lib.rs | 5 -- pytests/tests/test_misc.py | 9 +++ src/impl_/pymodule.rs | 87 +++++++++++++++++--------- 5 files changed, 67 insertions(+), 38 deletions(-) diff --git a/guide/src/building_and_distribution.md b/guide/src/building_and_distribution.md index 0aee432c2d0..8cb675f4303 100644 --- a/guide/src/building_and_distribution.md +++ b/guide/src/building_and_distribution.md @@ -276,7 +276,7 @@ If you encounter these or other complications when linking the interpreter stati ### Import your module when embedding the Python interpreter -When you run your Rust binary with an embedded interpreter, any `#[pymodule]` created modules won't be accessible to import unless added to a table called `PyImport_Inittab` before the embedded interpreter is initialized. This will cause Python statements in your embedded interpreter such as `import your_new_module` to fail. You can call the macro [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) with your module before initializing the Python interpreter to add the module function into that table. (The Python interpreter will be initialized by calling `prepare_freethreaded_python`, `with_embedded_interpreter`, or `Python::with_gil` with the [`auto-initialize`](features.md#auto-initialize) feature enabled.) +When you run your Rust binary with an embedded interpreter, any `#[pymodule]` created modules won't be accessible to import unless added to a table called `PyImport_Inittab` before the embedded interpreter is initialized. This will cause Python statements in your embedded interpreter such as `import your_new_module` to fail. You can call the macro [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) with your module before initializing the Python interpreter to add the module function into that table. (The Python interpreter will be initialized by calling `prepare_freethreaded_python`, `with_embedded_python_interpreter`, or `Python::with_gil` with the [`auto-initialize`](features.md#auto-initialize) feature enabled.) ## Cross Compiling diff --git a/newsfragments/3446.changed.md b/newsfragments/3446.changed.md index 089947ff275..a258fb4af67 100644 --- a/newsfragments/3446.changed.md +++ b/newsfragments/3446.changed.md @@ -1 +1 @@ -Relax multiple import check to only prevent use of subinterpreters. +`#[pymodule]` will now return the same module object on repeated import by the same Python interpreter, on Python 3.9 and up. diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 2f5ff4662a9..05320add478 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -146,11 +146,6 @@ pub fn print_feature_cfgs() { if rustc_minor_version >= 59 { println!("cargo:rustc-cfg=thread_local_const_init"); } - - // Enable use of OnceLock on Rust 1.70 and greater - if rustc_minor_version >= 70 { - println!("cargo:rustc-cfg=once_lock"); - } } /// Private exports used in PyO3's build.rs diff --git a/pytests/tests/test_misc.py b/pytests/tests/test_misc.py index 226b89d87a6..c3e03c04b09 100644 --- a/pytests/tests/test_misc.py +++ b/pytests/tests/test_misc.py @@ -1,5 +1,6 @@ import importlib import platform +import sys import pyo3_pytests.misc import pytest @@ -10,6 +11,10 @@ def test_issue_219(): pyo3_pytests.misc.issue_219() +@pytest.mark.xfail( + platform.python_implementation() == "CPython" and sys.version_info < (3, 9), + reason="Cannot identify subinterpreters on Python older than 3.9", +) def test_multiple_imports_same_interpreter_ok(): spec = importlib.util.find_spec("pyo3_pytests.pyo3_pytests") @@ -17,6 +22,10 @@ def test_multiple_imports_same_interpreter_ok(): assert dir(module) == dir(pyo3_pytests.pyo3_pytests) +@pytest.mark.xfail( + platform.python_implementation() == "CPython" and sys.version_info < (3, 9), + reason="Cannot identify subinterpreters on Python older than 3.9", +) @pytest.mark.skipif( platform.python_implementation() == "PyPy", reason="PyPy does not support subinterpreters", diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index bebc1156d8d..522341287c6 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -1,23 +1,24 @@ //! Implementation details of `#[pymodule]` which need to be accessible from proc-macro generated code. use std::cell::UnsafeCell; -#[cfg(once_lock)] -use std::sync::OnceLock; -#[cfg(not(once_lock))] -use parking_lot::Mutex; +#[cfg(all(not(PyPy), Py_3_9))] +use std::sync::atomic::{AtomicI64, Ordering}; -use crate::{exceptions::PyImportError, ffi, types::PyModule, Py, PyResult, Python}; +#[cfg(not(PyPy))] +use crate::exceptions::PyImportError; +use crate::{ffi, sync::GILOnceCell, types::PyModule, Py, PyResult, Python}; /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { // wrapped in UnsafeCell so that Rust compiler treats this as interior mutability ffi_def: UnsafeCell, initializer: ModuleInitializer, - #[cfg(once_lock)] - interpreter: OnceLock, - #[cfg(not(once_lock))] - interpreter: Mutex>, + /// Interpreter ID where module was initialized (not applicable on PyPy). + #[cfg(all(not(PyPy), Py_3_9))] + interpreter: AtomicI64, + /// Initialized module object, cached to avoid reinitialization. + module: GILOnceCell>, } /// Wrapper to enable initializer to be used in const fns. @@ -56,10 +57,10 @@ impl ModuleDef { ModuleDef { ffi_def, initializer, - #[cfg(once_lock)] - interpreter: OnceLock::new(), - #[cfg(not(once_lock))] - interpreter: Mutex::new(None), + // -1 is never expected to be a valid interpreter ID + #[cfg(all(not(PyPy), Py_3_9))] + interpreter: AtomicI64::new(-1), + module: GILOnceCell::new(), } } /// Builds a module using user given initializer. Used for [`#[pymodule]`][crate::pymodule]. @@ -79,29 +80,53 @@ impl ModuleDef { ))?; } } - let module = unsafe { - Py::::from_owned_ptr_or_err(py, ffi::PyModule_Create(self.ffi_def.get()))? - }; - let current_interpreter = - unsafe { ffi::PyInterpreterState_GetID(ffi::PyInterpreterState_Get()) }; - let initialized_interpreter = py.allow_threads(|| { - #[cfg(once_lock)] + // Check the interpreter ID has not changed, since we currently have no way to guarantee + // that static data is not reused across interpreters. + // + // PyPy does not have subinterpreters, so no need to check interpreter ID. + #[cfg(not(PyPy))] + { + #[cfg(Py_3_9)] { - *self.interpreter.get_or_init(|| current_interpreter) + let current_interpreter = + unsafe { ffi::PyInterpreterState_GetID(ffi::PyInterpreterState_Get()) }; + crate::err::error_on_minusone(py, current_interpreter)?; + if let Err(initialized_interpreter) = self.interpreter.compare_exchange( + -1, + current_interpreter, + Ordering::SeqCst, + Ordering::SeqCst, + ) { + if initialized_interpreter != current_interpreter { + return Err(PyImportError::new_err( + "PyO3 modules do not yet support subinterpreters, see https://github.com/PyO3/pyo3/issues/576", + )); + } + } } - - #[cfg(not(once_lock))] + #[cfg(not(Py_3_9))] { - *self.interpreter.lock().get_or_insert(current_interpreter) + // CPython before 3.9 does not have APIs to check the interpreter ID, so best that can be + // done to guard against subinterpreters is fail if the module is initialized twice + if self.module.get(py).is_some() { + return Err(PyImportError::new_err( + "PyO3 modules compiled for CPython 3.8 or older may only be initialized once per interpreter process" + )); + } } - }); - if current_interpreter != initialized_interpreter { - return Err(PyImportError::new_err( - "PyO3 modules do not yet support subinterpreters, see https://github.com/PyO3/pyo3/issues/576", - )); } - (self.initializer.0)(py, module.as_ref(py))?; - Ok(module) + self.module + .get_or_try_init(py, || { + let module = unsafe { + Py::::from_owned_ptr_or_err( + py, + ffi::PyModule_Create(self.ffi_def.get()), + )? + }; + (self.initializer.0)(py, module.as_ref(py))?; + Ok(module) + }) + .map(|py_module| py_module.clone_ref(py)) } }