From 70d829de664c2cc8bca576367e405aaa7deadb0a Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Thu, 16 Sep 2021 23:59:00 +0100 Subject: [PATCH] gil: try to initialize threads on Python 3.6 if possible --- CHANGELOG.md | 1 + src/gil.rs | 41 +++++++++++++++++++++++++---------------- tests/test_py36_init.rs | 9 +++++++++ 3 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 tests/test_py36_init.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 908a94d3865..b8ef99ccd67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed - Fix building with a conda environment on Windows. [#1873](https://github.com/PyO3/pyo3/pull/1873) +- Fix panic on Python 3.6 when calling `Python::with_gil` with Python initialized but threading not initialized. [#1874](https://github.com/PyO3/pyo3/pull/1874) ## [0.14.5] - 2021-09-05 diff --git a/src/gil.rs b/src/gil.rs index d75da5db3b2..2f93ac55b39 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -52,10 +52,12 @@ pub(crate) fn gil_is_acquired() -> bool { /// software). Support for this is tracked on the /// [PyPy issue tracker](https://foss.heptapod.net/pypy/pypy/-/issues/3286). /// +/// Python 3.6 only: If the Python interpreter is initialized but Python threading is not, this +/// function will initialize Python threading. +/// /// # Panics -/// - If the Python interpreter is initialized but Python threading is not, a panic occurs. -/// It is not possible to safely access the Python runtime unless the main thread (the thread -/// which originally initialized Python) also initializes threading. +/// - Python 3.6 only: If this function needs to initialize Python threading but the calling thread +/// is not the thread which initialized Python, this function will panic. /// /// # Examples /// ```rust @@ -77,21 +79,28 @@ pub fn prepare_freethreaded_python() { // concurrently call 'prepare_freethreaded_python()'. Note that we do not protect against // concurrent initialization of the Python runtime by other users of the Python C API. START.call_once_force(|_| unsafe { - // Use call_once_force because if initialization panics, it's okay to try again. - if ffi::Py_IsInitialized() != 0 { - // If Python is already initialized, we expect Python threading to also be initialized, - // as we can't make the existing Python main thread acquire the GIL. - assert_ne!(ffi::PyEval_ThreadsInitialized(), 0); - } else { - ffi::Py_InitializeEx(0); - - // Changed in version 3.7: This function is now called by Py_Initialize(), so you don’t - // have to call it yourself anymore. - if cfg!(not(Py_3_7)) { + if cfg!(not(Py_3_7)) { + // Use call_once_force because if initialization panics, it's okay to try again. + if ffi::Py_IsInitialized() != 0 { if ffi::PyEval_ThreadsInitialized() == 0 { + // We can only safely initialize threads if this thread holds the GIL. + assert!( + !ffi::PyGILState_GetThisThreadState().is_null(), + "Python threading is not initialized and cannot be initialized by this \ + thread, because it is not the thread which initialized Python." + ); ffi::PyEval_InitThreads(); } + } else { + ffi::Py_InitializeEx(0); + ffi::PyEval_InitThreads(); + + // Release the GIL. + ffi::PyEval_SaveThread(); } + } else if ffi::Py_IsInitialized() == 0 { + // In Python 3.7 and up PyEval_InitThreads is irrelevant. + ffi::Py_InitializeEx(0); // Release the GIL. ffi::PyEval_SaveThread(); @@ -101,7 +110,7 @@ pub fn prepare_freethreaded_python() { /// Executes the provided closure with an embedded Python interpreter. /// -/// This function intializes the Python interpreter, executes the provided closure, and then +/// This function initializes the Python interpreter, executes the provided closure, and then /// finalizes the Python interpreter. /// /// After execution all Python resources are cleaned up, and no further Python APIs can be called. @@ -110,7 +119,7 @@ pub fn prepare_freethreaded_python() { /// initialize correctly on the second run.) /// /// # Panics -/// - If the Python interpreter is already initalized before calling this function. +/// - If the Python interpreter is already initialized before calling this function. /// /// # Safety /// - This function should only ever be called once per process (usually as part of the `main` diff --git a/tests/test_py36_init.rs b/tests/test_py36_init.rs new file mode 100644 index 00000000000..d4f1c8e9ef1 --- /dev/null +++ b/tests/test_py36_init.rs @@ -0,0 +1,9 @@ +// This test checks Python initialization on python 3.6, so needs to be standalone in its own process. + +#[cfg(not(PyPy))] +#[test] +fn test_py36_init_threads() { + unsafe { pyo3::ffi::Py_InitializeEx(0) }; + pyo3::prepare_freethreaded_python(); + assert_eq!(unsafe { pyo3::ffi::PyEval_ThreadsInitialized() }, 1); +}