pyo3/src/gil.rs

714 lines
24 KiB
Rust
Raw Normal View History

2017-06-20 21:10:12 +00:00
// Copyright (c) 2017-present PyO3 Project and Contributors
2019-02-23 17:01:22 +00:00
//! Interaction with python's global interpreter lock
2020-05-01 16:09:10 +00:00
use crate::{ffi, internal_tricks::Unsendable, Python};
use parking_lot::{const_mutex, Mutex};
use std::cell::{Cell, RefCell};
use std::{any, mem::ManuallyDrop, ptr::NonNull, sync};
2015-01-05 16:05:53 +00:00
static START: sync::Once = sync::Once::new();
2015-01-05 16:05:53 +00:00
thread_local! {
/// This is a internal counter in pyo3 monitoring whether this thread has the GIL.
///
2020-05-03 21:29:44 +00:00
/// It will be incremented whenever a GILPool is created, and decremented whenever they are
/// dropped.
///
/// As a result, if this thread has the GIL, GIL_COUNT is greater than zero.
///
/// pub(crate) because it is manipulated temporarily by Python::allow_threads
pub(crate) static GIL_COUNT: Cell<u32> = Cell::new(0);
2020-05-02 12:12:38 +00:00
/// These are objects owned by the current thread, to be released when the GILPool drops.
static OWNED_OBJECTS: RefCell<Vec<NonNull<ffi::PyObject>>> = RefCell::new(Vec::with_capacity(256));
/// These are non-python objects such as (String) owned by the current thread, to be released
/// when the GILPool drops.
static OWNED_ANYS: RefCell<Vec<Box<dyn any::Any>>> = RefCell::new(Vec::with_capacity(256));
}
/// Check whether the GIL is acquired.
///
/// Note: This uses pyo3's internal count rather than PyGILState_Check for two reasons:
/// 1) for performance
/// 2) PyGILState_Check always returns 1 if the sub-interpreter APIs have ever been called,
/// which could lead to incorrect conclusions that the GIL is held.
fn gil_is_acquired() -> bool {
GIL_COUNT.with(|c| c.get() > 0)
}
2015-06-27 20:45:35 +00:00
/// Prepares the use of Python in a free-threaded context.
2015-04-18 20:20:19 +00:00
///
2015-06-27 20:45:35 +00:00
/// If the Python interpreter is not already initialized, this function
2015-04-18 20:20:19 +00:00
/// will initialize it with disabled signal handling
2015-06-27 20:45:35 +00:00
/// (Python will not raise the `KeyboardInterrupt` exception).
2015-04-18 20:20:19 +00:00
/// Python signal handling depends on the notion of a 'main thread', which must be
2015-06-27 20:45:35 +00:00
/// the thread that initializes the Python interpreter.
///
/// If both the Python interpreter and Python threading are already initialized,
/// this function has no effect.
///
/// # Panic
/// 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.
///
2018-11-12 21:28:45 +00:00
/// When writing an extension module, the `#[pymodule]` macro
2015-06-27 20:45:35 +00:00
/// will ensure that Python threading is initialized.
///
2015-01-04 04:50:28 +00:00
pub fn prepare_freethreaded_python() {
2015-06-27 20:45:35 +00:00
// Protect against race conditions when Python is not yet initialized
2015-01-04 19:11:18 +00:00
// and multiple threads concurrently call 'prepare_freethreaded_python()'.
2015-06-27 20:45:35 +00:00
// Note that we do not protect against concurrent initialization of the Python runtime
// by other users of the Python C API.
2015-01-04 04:50:28 +00:00
START.call_once(|| unsafe {
2015-01-04 19:11:18 +00:00
if ffi::Py_IsInitialized() != 0 {
2015-06-27 20:45:35 +00:00
// 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.
Support rust extensions for PyPy via cpyext (#393) * wip * removed stuff * removed another change * implemented minimum amouth of ifdefs to make pypy3 hello world to compile * implemented minimum amount of ifdefs to make pypy3 hello world to compile * hacking on build.rs * compiler is happy! * few todos remain * extracted build logic to seperate module * added pypy test * finally fixed pypy structs * removed some todos * test should now be machine independent * fixed all pypy3 symbols * added pypy feature * removed `is_pypy` * added pypy2 declerations also * fix for cpython2 * improved libpypy detection * added all pypy2 macros * fixed errneous type * more fixes * fix python2 string macros * modsupport symbol * fix * fixed and added many symbols * fixes * remove dup * remove mac-specific config * fix all name mangling macros * unite imports * missing symbol * fix pybool * implemented another missing symbol * it works * fix merge conflict * uncomment non default features * cargo.toml * Cargo fmt * small merge fixes * use newer build version * whoops * fix build script * more build hacks * some random hiccups * small fixes * it builds! * it builds and runs * revert everything in FFI2 * revert changes to ffi2 * check python3 for pypy * tiny fix * revert ffi2 for real * revert weird formatting changes * bring back missing feature * tiny error * fix py3.7 issue * add pypy3.5 6.0 to travis * remove dbg! * another tiny fix * removed some useless annotations, and fixed inlines annotations * removed `pretty_assertions` * removed pypy feature from cargo.toml * fix for Py_CompileStringFlags * tox runs word_count! * __dict__ changes are not supported for PyPy * fix 3.7 and copy comment * fix test script :flushed: * transfer ownership of strings to cpython when possible * remove cstr! macro * added missing nuls * as_bytes() -> b’’ string * symbol removed by mistake * properly shim pypy date time API, some tests are passing! * extension_module tests now not crashing! (some still skipped) * maybe travis has new pypy version? * small error on windows (build script) * fix conditional compilation * try to make tests run on travis.. * invert condition * added pytest-faulthandler to facilitate debugging * correctly name dir * use full paths * say —yes to conda * fix * syntax error * change PATH * fixed a terrible bug with PyTypeObjects in PyPy * fix PyTypeObject defs * re-enabled tests! * all tests are passing! * make the fix ad-hoc for now * removed build module * revert changes that cause an additional GC bug * prevented buggy test from failing pypy * removed unused comment * don’t run coverage on pypy * removed some erroneous symbols from function calls which are actually macros * restore py37 pyunicode missing def * use only `link_name` in PyPy specific declarations * only setup PyPy when testing against PyPy * annotation that was eaten during merge * remove change to comment by mistake + unnecessary changes to cargo.toml * xfail dates test only on pypy * changed comment to be a little more helpful * cleaned up some warnings * Update src/ffi3/ceval.rs Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * @konstin PR notes * rustfmt * some documentation * if configured via env var only, default to cpython * remove extra unsafe * refer users to guide for pypy * Update guide/src/pypy.md Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * Update guide/src/pypy.md Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * @konstin applied patch * check that pypy at least build * search explicitly for libpypy * added note about some known unsupported features * use ld_version * export PYTHON_SYS_EXECUTABLE to `cargo build` test * inverted if * always link pypy dynamically * remove unused imports * Apply @kngwyu’s suggestion * fix tox configuration * try conda virtualenv * try to simply not install python at all inside pypy environment * setup pypy before using “python" * use system_site_packages * revert change to .travis * moved cpyext datetime documentation to module level, and revised it. * Update src/ffi/datetime.rs Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * rustfmt * Update src/ffi/datetime.rs Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * kept only notes that are relevant to users. * invert if * use bash and not sh
2019-04-23 11:18:42 +00:00
#[cfg(not(Py_3_7))]
2017-07-18 11:28:49 +00:00
assert_ne!(ffi::PyEval_ThreadsInitialized(), 0);
2015-01-04 19:11:18 +00:00
} else {
2018-02-21 18:06:48 +00:00
// If Python isn't initialized yet, we expect that Python threading
// isn't initialized either.
2018-02-21 18:29:14 +00:00
#[cfg(not(Py_3_7))]
2018-02-21 18:06:48 +00:00
assert_eq!(ffi::PyEval_ThreadsInitialized(), 0);
2015-06-27 20:45:35 +00:00
// Initialize Python.
2015-01-04 19:11:18 +00:00
// We use Py_InitializeEx() with initsigs=0 to disable Python signal handling.
// Signal handling depends on the notion of a 'main thread', which doesn't exist in this case.
2015-06-27 20:45:35 +00:00
// Note that the 'main thread' notion in Python isn't documented properly;
// and running Python without one is not officially supported.
Support rust extensions for PyPy via cpyext (#393) * wip * removed stuff * removed another change * implemented minimum amouth of ifdefs to make pypy3 hello world to compile * implemented minimum amount of ifdefs to make pypy3 hello world to compile * hacking on build.rs * compiler is happy! * few todos remain * extracted build logic to seperate module * added pypy test * finally fixed pypy structs * removed some todos * test should now be machine independent * fixed all pypy3 symbols * added pypy feature * removed `is_pypy` * added pypy2 declerations also * fix for cpython2 * improved libpypy detection * added all pypy2 macros * fixed errneous type * more fixes * fix python2 string macros * modsupport symbol * fix * fixed and added many symbols * fixes * remove dup * remove mac-specific config * fix all name mangling macros * unite imports * missing symbol * fix pybool * implemented another missing symbol * it works * fix merge conflict * uncomment non default features * cargo.toml * Cargo fmt * small merge fixes * use newer build version * whoops * fix build script * more build hacks * some random hiccups * small fixes * it builds! * it builds and runs * revert everything in FFI2 * revert changes to ffi2 * check python3 for pypy * tiny fix * revert ffi2 for real * revert weird formatting changes * bring back missing feature * tiny error * fix py3.7 issue * add pypy3.5 6.0 to travis * remove dbg! * another tiny fix * removed some useless annotations, and fixed inlines annotations * removed `pretty_assertions` * removed pypy feature from cargo.toml * fix for Py_CompileStringFlags * tox runs word_count! * __dict__ changes are not supported for PyPy * fix 3.7 and copy comment * fix test script :flushed: * transfer ownership of strings to cpython when possible * remove cstr! macro * added missing nuls * as_bytes() -> b’’ string * symbol removed by mistake * properly shim pypy date time API, some tests are passing! * extension_module tests now not crashing! (some still skipped) * maybe travis has new pypy version? * small error on windows (build script) * fix conditional compilation * try to make tests run on travis.. * invert condition * added pytest-faulthandler to facilitate debugging * correctly name dir * use full paths * say —yes to conda * fix * syntax error * change PATH * fixed a terrible bug with PyTypeObjects in PyPy * fix PyTypeObject defs * re-enabled tests! * all tests are passing! * make the fix ad-hoc for now * removed build module * revert changes that cause an additional GC bug * prevented buggy test from failing pypy * removed unused comment * don’t run coverage on pypy * removed some erroneous symbols from function calls which are actually macros * restore py37 pyunicode missing def * use only `link_name` in PyPy specific declarations * only setup PyPy when testing against PyPy * annotation that was eaten during merge * remove change to comment by mistake + unnecessary changes to cargo.toml * xfail dates test only on pypy * changed comment to be a little more helpful * cleaned up some warnings * Update src/ffi3/ceval.rs Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * @konstin PR notes * rustfmt * some documentation * if configured via env var only, default to cpython * remove extra unsafe * refer users to guide for pypy * Update guide/src/pypy.md Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * Update guide/src/pypy.md Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * @konstin applied patch * check that pypy at least build * search explicitly for libpypy * added note about some known unsupported features * use ld_version * export PYTHON_SYS_EXECUTABLE to `cargo build` test * inverted if * always link pypy dynamically * remove unused imports * Apply @kngwyu’s suggestion * fix tox configuration * try conda virtualenv * try to simply not install python at all inside pypy environment * setup pypy before using “python" * use system_site_packages * revert change to .travis * moved cpyext datetime documentation to module level, and revised it. * Update src/ffi/datetime.rs Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * rustfmt * Update src/ffi/datetime.rs Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * kept only notes that are relevant to users. * invert if * use bash and not sh
2019-04-23 11:18:42 +00:00
// PyPy does not support the embedding API
#[cfg(not(PyPy))]
{
ffi::Py_InitializeEx(0);
// Make sure Py_Finalize will be called before exiting.
libc::atexit(finalize);
}
Support rust extensions for PyPy via cpyext (#393) * wip * removed stuff * removed another change * implemented minimum amouth of ifdefs to make pypy3 hello world to compile * implemented minimum amount of ifdefs to make pypy3 hello world to compile * hacking on build.rs * compiler is happy! * few todos remain * extracted build logic to seperate module * added pypy test * finally fixed pypy structs * removed some todos * test should now be machine independent * fixed all pypy3 symbols * added pypy feature * removed `is_pypy` * added pypy2 declerations also * fix for cpython2 * improved libpypy detection * added all pypy2 macros * fixed errneous type * more fixes * fix python2 string macros * modsupport symbol * fix * fixed and added many symbols * fixes * remove dup * remove mac-specific config * fix all name mangling macros * unite imports * missing symbol * fix pybool * implemented another missing symbol * it works * fix merge conflict * uncomment non default features * cargo.toml * Cargo fmt * small merge fixes * use newer build version * whoops * fix build script * more build hacks * some random hiccups * small fixes * it builds! * it builds and runs * revert everything in FFI2 * revert changes to ffi2 * check python3 for pypy * tiny fix * revert ffi2 for real * revert weird formatting changes * bring back missing feature * tiny error * fix py3.7 issue * add pypy3.5 6.0 to travis * remove dbg! * another tiny fix * removed some useless annotations, and fixed inlines annotations * removed `pretty_assertions` * removed pypy feature from cargo.toml * fix for Py_CompileStringFlags * tox runs word_count! * __dict__ changes are not supported for PyPy * fix 3.7 and copy comment * fix test script :flushed: * transfer ownership of strings to cpython when possible * remove cstr! macro * added missing nuls * as_bytes() -> b’’ string * symbol removed by mistake * properly shim pypy date time API, some tests are passing! * extension_module tests now not crashing! (some still skipped) * maybe travis has new pypy version? * small error on windows (build script) * fix conditional compilation * try to make tests run on travis.. * invert condition * added pytest-faulthandler to facilitate debugging * correctly name dir * use full paths * say —yes to conda * fix * syntax error * change PATH * fixed a terrible bug with PyTypeObjects in PyPy * fix PyTypeObject defs * re-enabled tests! * all tests are passing! * make the fix ad-hoc for now * removed build module * revert changes that cause an additional GC bug * prevented buggy test from failing pypy * removed unused comment * don’t run coverage on pypy * removed some erroneous symbols from function calls which are actually macros * restore py37 pyunicode missing def * use only `link_name` in PyPy specific declarations * only setup PyPy when testing against PyPy * annotation that was eaten during merge * remove change to comment by mistake + unnecessary changes to cargo.toml * xfail dates test only on pypy * changed comment to be a little more helpful * cleaned up some warnings * Update src/ffi3/ceval.rs Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * @konstin PR notes * rustfmt * some documentation * if configured via env var only, default to cpython * remove extra unsafe * refer users to guide for pypy * Update guide/src/pypy.md Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * Update guide/src/pypy.md Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * @konstin applied patch * check that pypy at least build * search explicitly for libpypy * added note about some known unsupported features * use ld_version * export PYTHON_SYS_EXECUTABLE to `cargo build` test * inverted if * always link pypy dynamically * remove unused imports * Apply @kngwyu’s suggestion * fix tox configuration * try conda virtualenv * try to simply not install python at all inside pypy environment * setup pypy before using “python" * use system_site_packages * revert change to .travis * moved cpyext datetime documentation to module level, and revised it. * Update src/ffi/datetime.rs Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * rustfmt * Update src/ffi/datetime.rs Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * kept only notes that are relevant to users. * invert if * use bash and not sh
2019-04-23 11:18:42 +00:00
// > Changed in version 3.7: This function is now called by Py_Initialize(), so you dont have
// > to call it yourself anymore.
#[cfg(not(Py_3_7))]
2015-01-04 19:11:18 +00:00
ffi::PyEval_InitThreads();
// PyEval_InitThreads() will acquire the GIL,
// but we don't want to hold it at this point
2015-01-04 04:50:28 +00:00
// (it's not acquired in the other code paths)
2015-01-04 19:11:18 +00:00
// So immediately release the GIL:
Support rust extensions for PyPy via cpyext (#393) * wip * removed stuff * removed another change * implemented minimum amouth of ifdefs to make pypy3 hello world to compile * implemented minimum amount of ifdefs to make pypy3 hello world to compile * hacking on build.rs * compiler is happy! * few todos remain * extracted build logic to seperate module * added pypy test * finally fixed pypy structs * removed some todos * test should now be machine independent * fixed all pypy3 symbols * added pypy feature * removed `is_pypy` * added pypy2 declerations also * fix for cpython2 * improved libpypy detection * added all pypy2 macros * fixed errneous type * more fixes * fix python2 string macros * modsupport symbol * fix * fixed and added many symbols * fixes * remove dup * remove mac-specific config * fix all name mangling macros * unite imports * missing symbol * fix pybool * implemented another missing symbol * it works * fix merge conflict * uncomment non default features * cargo.toml * Cargo fmt * small merge fixes * use newer build version * whoops * fix build script * more build hacks * some random hiccups * small fixes * it builds! * it builds and runs * revert everything in FFI2 * revert changes to ffi2 * check python3 for pypy * tiny fix * revert ffi2 for real * revert weird formatting changes * bring back missing feature * tiny error * fix py3.7 issue * add pypy3.5 6.0 to travis * remove dbg! * another tiny fix * removed some useless annotations, and fixed inlines annotations * removed `pretty_assertions` * removed pypy feature from cargo.toml * fix for Py_CompileStringFlags * tox runs word_count! * __dict__ changes are not supported for PyPy * fix 3.7 and copy comment * fix test script :flushed: * transfer ownership of strings to cpython when possible * remove cstr! macro * added missing nuls * as_bytes() -> b’’ string * symbol removed by mistake * properly shim pypy date time API, some tests are passing! * extension_module tests now not crashing! (some still skipped) * maybe travis has new pypy version? * small error on windows (build script) * fix conditional compilation * try to make tests run on travis.. * invert condition * added pytest-faulthandler to facilitate debugging * correctly name dir * use full paths * say —yes to conda * fix * syntax error * change PATH * fixed a terrible bug with PyTypeObjects in PyPy * fix PyTypeObject defs * re-enabled tests! * all tests are passing! * make the fix ad-hoc for now * removed build module * revert changes that cause an additional GC bug * prevented buggy test from failing pypy * removed unused comment * don’t run coverage on pypy * removed some erroneous symbols from function calls which are actually macros * restore py37 pyunicode missing def * use only `link_name` in PyPy specific declarations * only setup PyPy when testing against PyPy * annotation that was eaten during merge * remove change to comment by mistake + unnecessary changes to cargo.toml * xfail dates test only on pypy * changed comment to be a little more helpful * cleaned up some warnings * Update src/ffi3/ceval.rs Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * @konstin PR notes * rustfmt * some documentation * if configured via env var only, default to cpython * remove extra unsafe * refer users to guide for pypy * Update guide/src/pypy.md Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * Update guide/src/pypy.md Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * @konstin applied patch * check that pypy at least build * search explicitly for libpypy * added note about some known unsupported features * use ld_version * export PYTHON_SYS_EXECUTABLE to `cargo build` test * inverted if * always link pypy dynamically * remove unused imports * Apply @kngwyu’s suggestion * fix tox configuration * try conda virtualenv * try to simply not install python at all inside pypy environment * setup pypy before using “python" * use system_site_packages * revert change to .travis * moved cpyext datetime documentation to module level, and revised it. * Update src/ffi/datetime.rs Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * rustfmt * Update src/ffi/datetime.rs Co-Authored-By: omerbenamram <omerbenamram@gmail.com> * kept only notes that are relevant to users. * invert if * use bash and not sh
2019-04-23 11:18:42 +00:00
#[cfg(not(PyPy))]
2015-01-04 19:11:18 +00:00
let _thread_state = ffi::PyEval_SaveThread();
2015-06-27 20:45:35 +00:00
// Note that the PyThreadState returned by PyEval_SaveThread is also held in TLS by the Python runtime,
2015-01-04 19:11:18 +00:00
// and will be restored by PyGILState_Ensure.
2015-01-04 04:50:28 +00:00
}
});
2015-01-05 16:05:53 +00:00
}
2016-03-05 16:41:04 +00:00
/// RAII type that represents the Global Interpreter Lock acquisition.
2015-06-27 20:45:35 +00:00
///
/// # Example
/// ```
2017-05-13 05:43:17 +00:00
/// use pyo3::Python;
2015-06-27 20:45:35 +00:00
///
/// {
/// let gil_guard = Python::acquire_gil();
/// let py = gil_guard.python();
/// } // GIL is released when gil_guard is dropped
/// ```
#[must_use]
pub struct GILGuard {
gstate: ffi::PyGILState_STATE,
2020-05-03 21:29:44 +00:00
pool: ManuallyDrop<Option<GILPool>>,
}
impl GILGuard {
2020-05-03 21:29:44 +00:00
/// Acquires the global interpreter lock, which allows access to the Python runtime. This is
/// safe to call multiple times without causing a deadlock.
///
/// If the Python runtime is not already initialized, this function will initialize it.
/// See [prepare_freethreaded_python()](fn.prepare_freethreaded_python.html) for details.
2020-05-03 21:29:44 +00:00
///
/// If PyO3 does not yet have a `GILPool` for tracking owned PyObject references, then this
/// new `GILGuard` will also contain a `GILPool`.
pub fn acquire() -> GILGuard {
prepare_freethreaded_python();
unsafe {
let gstate = ffi::PyGILState_Ensure(); // acquire GIL
2020-05-03 21:29:44 +00:00
// If there's already a GILPool, we should not create another or this could lead to
// incorrect dangling references in safe code (see #864).
let pool = if !gil_is_acquired() {
Some(GILPool::new())
} else {
None
};
GILGuard {
gstate,
2020-05-03 21:29:44 +00:00
pool: ManuallyDrop::new(pool),
}
}
}
/// Retrieves the marker type that proves that the GIL was acquired.
#[inline]
pub fn python(&self) -> Python {
unsafe { Python::assume_gil_acquired() }
}
}
2017-06-11 15:46:23 +00:00
/// The Drop implementation for `GILGuard` will release the GIL.
impl Drop for GILGuard {
fn drop(&mut self) {
2017-06-21 06:26:28 +00:00
unsafe {
2020-05-02 12:12:38 +00:00
// Must drop the objects in the pool before releasing the GILGuard
ManuallyDrop::drop(&mut self.pool);
2017-06-21 06:26:28 +00:00
ffi::PyGILState_Release(self.gstate);
}
}
}
2020-05-06 08:04:57 +00:00
/// Thread-safe storage for objects which were inc_ref / dec_ref while the GIL was not held.
struct ReferencePool {
pointers_to_incref: Mutex<Vec<NonNull<ffi::PyObject>>>,
pointers_to_decref: Mutex<Vec<NonNull<ffi::PyObject>>>,
2017-07-09 06:08:57 +00:00
}
2020-05-06 08:04:57 +00:00
impl ReferencePool {
2020-05-02 12:12:38 +00:00
const fn new() -> Self {
Self {
pointers_to_incref: const_mutex(Vec::new()),
pointers_to_decref: const_mutex(Vec::new()),
2017-07-09 06:08:57 +00:00
}
}
2020-05-06 08:04:57 +00:00
fn register_incref(&self, obj: NonNull<ffi::PyObject>) {
self.pointers_to_incref.lock().push(obj)
2017-07-09 06:08:57 +00:00
}
2020-05-06 08:04:57 +00:00
fn register_decref(&self, obj: NonNull<ffi::PyObject>) {
self.pointers_to_decref.lock().push(obj)
2020-05-06 08:04:57 +00:00
}
fn update_counts(&self, _py: Python) {
2020-05-13 17:36:40 +00:00
macro_rules! swap_vec_with_lock {
// Get vec from one of ReferencePool's mutexes via lock, swap vec if needed, unlock.
2020-05-13 17:36:40 +00:00
($cell:expr) => {{
let mut locked = $cell.lock();
let mut out = Vec::new();
if !locked.is_empty() {
std::mem::swap(&mut out, &mut *locked);
2020-05-13 17:36:40 +00:00
}
drop(locked);
2020-05-13 17:36:40 +00:00
out
}};
};
2020-05-06 08:04:57 +00:00
// Always increase reference counts first - as otherwise objects which have a
// nonzero total reference count might be incorrectly dropped by Python during
// this update.
for ptr in swap_vec_with_lock!(self.pointers_to_incref) {
unsafe { ffi::Py_INCREF(ptr.as_ptr()) };
2020-05-06 08:04:57 +00:00
}
for ptr in swap_vec_with_lock!(self.pointers_to_decref) {
unsafe { ffi::Py_DECREF(ptr.as_ptr()) };
}
}
}
2020-05-06 08:04:57 +00:00
unsafe impl Sync for ReferencePool {}
2017-06-21 06:26:28 +00:00
2020-05-06 08:04:57 +00:00
static POOL: ReferencePool = ReferencePool::new();
2020-05-02 12:12:38 +00:00
2020-05-03 21:29:44 +00:00
/// A RAII pool which PyO3 uses to store owned Python references.
pub struct GILPool {
2020-05-02 12:12:38 +00:00
owned_objects_start: usize,
owned_anys_start: usize,
// Stable solution for impl !Send
no_send: Unsendable,
}
impl GILPool {
2020-05-03 21:29:44 +00:00
/// Create a new `GILPool`. This function should only ever be called with the GIL.
///
/// It is recommended not to use this API directly, but instead to use `Python::new_pool`, as
/// that guarantees the GIL is held.
///
/// # Safety
2020-05-03 21:29:44 +00:00
/// As well as requiring the GIL, see the notes on `Python::new_pool`.
#[inline]
pub unsafe fn new() -> GILPool {
increment_gil_count();
2020-05-06 08:04:57 +00:00
// Update counts of PyObjects / Py that have been cloned or dropped since last acquisition
POOL.update_counts(Python::assume_gil_acquired());
GILPool {
2020-05-02 12:12:38 +00:00
owned_objects_start: OWNED_OBJECTS.with(|o| o.borrow().len()),
owned_anys_start: OWNED_ANYS.with(|o| o.borrow().len()),
no_send: Unsendable::default(),
}
}
2020-05-03 21:29:44 +00:00
/// Get the Python token associated with this `GILPool`.
pub fn python(&self) -> Python {
unsafe { Python::assume_gil_acquired() }
}
}
impl Drop for GILPool {
fn drop(&mut self) {
unsafe {
2020-05-02 12:12:38 +00:00
OWNED_OBJECTS.with(|owned_objects| {
// Note: inside this closure we must be careful to not hold a borrow too long, because
// while calling Py_DECREF we may cause other callbacks to run which will need to
// register objects into the GILPool.
let len = owned_objects.borrow().len();
2020-05-02 14:38:12 +00:00
if self.owned_objects_start < len {
let rest = owned_objects
.borrow_mut()
.split_off(self.owned_objects_start);
for obj in rest {
ffi::Py_DECREF(obj.as_ptr());
}
2020-05-02 12:12:38 +00:00
}
});
OWNED_ANYS.with(|owned_anys| owned_anys.borrow_mut().truncate(self.owned_anys_start));
}
decrement_gil_count();
}
}
2020-05-06 08:04:57 +00:00
/// Register a Python object pointer inside the release pool, to have reference count increased
/// next time the GIL is acquired in pyo3.
///
/// If the GIL is held, the reference count will be increased immediately instead of being queued
/// for later.
///
/// # Safety
/// The object must be an owned Python reference.
pub unsafe fn register_incref(obj: NonNull<ffi::PyObject>) {
if gil_is_acquired() {
ffi::Py_INCREF(obj.as_ptr())
} else {
POOL.register_incref(obj);
}
}
2020-05-02 12:12:38 +00:00
/// Register a Python object pointer inside the release pool, to have reference count decreased
/// next time the GIL is acquired in pyo3.
///
2020-05-06 08:04:57 +00:00
/// If the GIL is held, the reference count will be decreased immediately instead of being queued
/// for later.
///
2020-05-02 12:12:38 +00:00
/// # Safety
/// The object must be an owned Python reference.
2020-05-06 08:04:57 +00:00
pub unsafe fn register_decref(obj: NonNull<ffi::PyObject>) {
if gil_is_acquired() {
ffi::Py_DECREF(obj.as_ptr())
} else {
2020-05-06 08:04:57 +00:00
POOL.register_decref(obj);
}
}
2020-05-02 12:12:38 +00:00
/// Register an owned object inside the GILPool.
///
/// # Safety
/// The object must be an owned Python reference.
2020-05-01 16:09:10 +00:00
pub unsafe fn register_owned(_py: Python, obj: NonNull<ffi::PyObject>) {
2020-05-02 12:12:38 +00:00
debug_assert!(gil_is_acquired());
OWNED_OBJECTS.with(|objs| objs.borrow_mut().push(obj));
}
/// Register any value inside the GILPool.
///
/// # Safety
/// It is the caller's responsibility to ensure that the inferred lifetime 'p is not inferred by
/// the Rust compiler to outlast the current GILPool.
pub unsafe fn register_any<'p, T: 'static>(obj: T) -> &'p T {
debug_assert!(gil_is_acquired());
OWNED_ANYS.with(|owned_anys| {
let boxed = Box::new(obj);
let value_ref: &T = &*boxed;
// Sneaky - extend the lifetime of the reference so that the box can be moved
let value_ref_extended_lifetime = std::mem::transmute(value_ref);
owned_anys.borrow_mut().push(boxed);
value_ref_extended_lifetime
})
}
/// Increment pyo3's internal GIL count - to be called whenever GILPool or GILGuard is created.
#[inline(always)]
fn increment_gil_count() {
GIL_COUNT.with(|c| c.set(c.get() + 1))
}
/// Decrement pyo3's internal GIL count - to be called whenever GILPool or GILGuard is dropped.
#[inline(always)]
fn decrement_gil_count() {
GIL_COUNT.with(|c| {
let current = c.get();
debug_assert!(
current > 0,
"Negative GIL count detected. Please report this error to the PyO3 repo as a bug."
);
c.set(current - 1);
})
}
extern "C" fn finalize() {
unsafe {
if ffi::Py_IsInitialized() != 0 {
ffi::PyGILState_Ensure();
ffi::Py_Finalize();
}
}
}
/// Ensure the GIL is held, useful in implementation of APIs like PyErr::new where it's
/// inconvenient to force the user to acquire the GIL.
2020-06-22 14:49:33 +00:00
#[doc(hidden)]
pub fn ensure_gil() -> EnsureGIL {
if gil_is_acquired() {
EnsureGIL(None)
} else {
EnsureGIL(Some(GILGuard::acquire()))
}
}
/// Struct used internally which avoids acquiring the GIL where it's not necessary.
2020-06-22 14:49:33 +00:00
#[doc(hidden)]
pub struct EnsureGIL(Option<GILGuard>);
impl EnsureGIL {
/// Get the GIL token.
///
/// # Safety
/// If `self.0` is `None`, then this calls [Python::assume_gil_acquired].
/// Thus this method could be used to get access to a GIL token while the GIL is not held.
/// Care should be taken to only use the returned Python in contexts where it is certain the
/// GIL continues to be held.
pub unsafe fn python(&self) -> Python {
match &self.0 {
Some(gil) => gil.python(),
None => Python::assume_gil_acquired(),
}
}
}
#[cfg(test)]
mod test {
2020-05-06 08:04:57 +00:00
use super::{gil_is_acquired, GILPool, GIL_COUNT, OWNED_OBJECTS, POOL};
2020-05-02 06:33:10 +00:00
use crate::{ffi, gil, AsPyPointer, IntoPyPointer, PyObject, Python, ToPyObject};
use std::ptr::NonNull;
2020-05-03 21:29:44 +00:00
fn get_object(py: Python) -> PyObject {
// Convenience function for getting a single unique object, using `new_pool` so as to leave
// the original pool state unchanged.
let pool = unsafe { py.new_pool() };
let py = pool.python();
let obj = py.eval("object()", None, None).unwrap();
obj.to_object(py)
}
2020-05-02 12:12:38 +00:00
fn owned_object_count() -> usize {
OWNED_OBJECTS.with(|objs| objs.borrow().len())
}
#[test]
fn test_owned() {
2019-03-09 23:28:25 +00:00
let gil = Python::acquire_gil();
let py = gil.python();
2020-05-03 21:29:44 +00:00
let obj = get_object(py);
2019-03-09 23:28:25 +00:00
let obj_ptr = obj.as_ptr();
// Ensure that obj does not get freed
let _ref = obj.clone_ref(py);
unsafe {
{
2020-05-03 21:29:44 +00:00
let pool = py.new_pool();
gil::register_owned(pool.python(), NonNull::new_unchecked(obj.into_ptr()));
2020-05-02 12:12:38 +00:00
assert_eq!(owned_object_count(), 1);
2020-05-03 21:29:44 +00:00
assert_eq!(ffi::Py_REFCNT(obj_ptr), 2);
}
{
2020-05-03 21:29:44 +00:00
let _pool = py.new_pool();
2020-05-02 12:12:38 +00:00
assert_eq!(owned_object_count(), 0);
2019-03-09 23:28:25 +00:00
assert_eq!(ffi::Py_REFCNT(obj_ptr), 1);
}
}
}
#[test]
fn test_owned_nested() {
2017-08-08 06:52:24 +00:00
let gil = Python::acquire_gil();
let py = gil.python();
2020-05-03 21:29:44 +00:00
let obj = get_object(py);
2019-03-09 23:28:25 +00:00
// Ensure that obj does not get freed
let _ref = obj.clone_ref(py);
let obj_ptr = obj.as_ptr();
unsafe {
{
2020-05-03 21:29:44 +00:00
let _pool = py.new_pool();
2020-05-02 12:12:38 +00:00
assert_eq!(owned_object_count(), 0);
2020-05-02 06:33:10 +00:00
gil::register_owned(py, NonNull::new_unchecked(obj.into_ptr()));
2020-05-02 12:12:38 +00:00
assert_eq!(owned_object_count(), 1);
2019-03-09 23:28:25 +00:00
assert_eq!(ffi::Py_REFCNT(obj_ptr), 2);
{
2020-05-03 21:29:44 +00:00
let _pool = py.new_pool();
let obj = get_object(py);
2020-05-02 06:33:10 +00:00
gil::register_owned(py, NonNull::new_unchecked(obj.into_ptr()));
2020-05-02 12:12:38 +00:00
assert_eq!(owned_object_count(), 2);
}
2020-05-02 12:12:38 +00:00
assert_eq!(owned_object_count(), 1);
}
{
2020-05-02 12:12:38 +00:00
assert_eq!(owned_object_count(), 0);
2019-03-09 23:28:25 +00:00
assert_eq!(ffi::Py_REFCNT(obj_ptr), 1);
}
}
}
#[test]
fn test_pyobject_drop_with_gil_decreases_refcnt() {
let gil = Python::acquire_gil();
let py = gil.python();
2020-05-03 21:29:44 +00:00
let obj = get_object(py);
// Ensure that obj does not get freed
let _ref = obj.clone_ref(py);
let obj_ptr = obj.as_ptr();
unsafe {
{
2020-05-02 12:12:38 +00:00
assert_eq!(owned_object_count(), 0);
assert_eq!(ffi::Py_REFCNT(obj_ptr), 2);
}
// With the GIL held, obj can be dropped immediately
drop(obj);
assert_eq!(ffi::Py_REFCNT(obj_ptr), 1);
}
}
#[test]
fn test_pyobject_drop_without_gil_doesnt_decrease_refcnt() {
2019-03-09 23:28:25 +00:00
let gil = Python::acquire_gil();
let py = gil.python();
2020-05-03 21:29:44 +00:00
let obj = get_object(py);
2019-03-09 23:28:25 +00:00
// Ensure that obj does not get freed
let _ref = obj.clone_ref(py);
let obj_ptr = obj.as_ptr();
unsafe {
{
2020-05-02 12:12:38 +00:00
assert_eq!(owned_object_count(), 0);
2019-03-09 23:28:25 +00:00
assert_eq!(ffi::Py_REFCNT(obj_ptr), 2);
}
// Without the GIL held, obj cannot be dropped until the next GIL acquire
drop(gil);
drop(obj);
2019-04-24 20:41:59 +00:00
assert_eq!(ffi::Py_REFCNT(obj_ptr), 2);
{
// Next time the GIL is acquired, the object is released
let _gil = Python::acquire_gil();
assert_eq!(ffi::Py_REFCNT(obj_ptr), 1);
}
}
}
#[test]
fn test_gil_counts() {
// Check GILGuard and GILPool both increase counts correctly
let get_gil_count = || GIL_COUNT.with(|c| c.get());
assert_eq!(get_gil_count(), 0);
let gil = Python::acquire_gil();
assert_eq!(get_gil_count(), 1);
assert_eq!(get_gil_count(), 1);
let pool = unsafe { GILPool::new() };
assert_eq!(get_gil_count(), 2);
let pool2 = unsafe { GILPool::new() };
assert_eq!(get_gil_count(), 3);
drop(pool);
assert_eq!(get_gil_count(), 2);
2020-05-03 21:29:44 +00:00
// Creating a new GILGuard should not increment the gil count if a GILPool already exists
let gil2 = Python::acquire_gil();
assert_eq!(get_gil_count(), 2);
drop(pool2);
assert_eq!(get_gil_count(), 1);
2020-05-03 21:29:44 +00:00
drop(gil2);
assert_eq!(get_gil_count(), 1);
drop(gil);
assert_eq!(get_gil_count(), 0);
}
#[test]
fn test_allow_threads() {
2020-05-06 08:04:57 +00:00
// allow_threads should temporarily release GIL in Py03's internal tracking too.
let gil = Python::acquire_gil();
let py = gil.python();
2020-05-06 08:04:57 +00:00
assert!(gil_is_acquired());
2020-05-06 08:04:57 +00:00
py.allow_threads(move || {
assert!(!gil_is_acquired());
let gil = Python::acquire_gil();
2020-05-06 08:04:57 +00:00
assert!(gil_is_acquired());
drop(gil);
2020-05-06 08:04:57 +00:00
assert!(!gil_is_acquired());
});
2020-05-06 08:04:57 +00:00
assert!(gil_is_acquired());
}
2020-05-03 21:29:44 +00:00
#[test]
fn dropping_gil_does_not_invalidate_references() {
// Acquiring GIL for the second time should be safe - see #864
let gil = Python::acquire_gil();
let py = gil.python();
let obj;
let gil2 = Python::acquire_gil();
obj = py.eval("object()", None, None).unwrap();
drop(gil2);
// After gil2 drops, obj should still have a reference count of one
2020-05-06 08:04:57 +00:00
assert_eq!(obj.get_refcnt(), 1);
}
#[test]
fn test_clone_with_gil() {
let gil = Python::acquire_gil();
let py = gil.python();
let obj = get_object(py);
let count = obj.get_refcnt(py);
2020-05-06 08:04:57 +00:00
// Cloning with the GIL should increase reference count immediately
#[allow(clippy::redundant_clone)]
let c = obj.clone();
assert_eq!(count + 1, c.get_refcnt(py));
2020-05-06 08:04:57 +00:00
}
#[test]
fn test_clone_without_gil() {
let gil = Python::acquire_gil();
let py = gil.python();
let obj = get_object(py);
let count = obj.get_refcnt(py);
2020-05-06 08:04:57 +00:00
// Cloning without GIL should not update reference count
drop(gil);
let c = obj.clone();
assert_eq!(
count,
obj.get_refcnt(unsafe { Python::assume_gil_acquired() })
);
2020-05-06 08:04:57 +00:00
// Acquring GIL will clear this pending change
let gil = Python::acquire_gil();
let py = gil.python();
2020-05-06 08:04:57 +00:00
// Total reference count should be one higher
assert_eq!(count + 1, obj.get_refcnt(py));
2020-05-06 08:04:57 +00:00
// Clone dropped
2020-05-06 08:04:57 +00:00
drop(c);
// Overall count is now back to the original, and should be no pending change
assert_eq!(count, obj.get_refcnt(py));
2020-05-06 08:04:57 +00:00
}
#[test]
fn test_clone_in_other_thread() {
let gil = Python::acquire_gil();
let py = gil.python();
let obj = get_object(py);
let count = obj.get_refcnt(py);
2020-05-06 08:04:57 +00:00
// Move obj to a thread which does not have the GIL, and clone it
let t = std::thread::spawn(move || {
// Cloning without GIL should not update reference count
2020-06-05 11:53:01 +00:00
#[allow(clippy::redundant_clone)]
2020-05-06 08:04:57 +00:00
let _ = obj.clone();
assert_eq!(
count,
obj.get_refcnt(unsafe { Python::assume_gil_acquired() })
);
2020-05-06 08:04:57 +00:00
// Return obj so original thread can continue to use
obj
});
let obj = t.join().unwrap();
let ptr = NonNull::new(obj.as_ptr()).unwrap();
// The pointer should appear once in the incref pool, and once in the
// decref pool (for the clone being created and also dropped)
assert_eq!(&*POOL.pointers_to_incref.lock(), &vec![ptr]);
assert_eq!(&*POOL.pointers_to_decref.lock(), &vec![ptr]);
2020-05-06 08:04:57 +00:00
// Re-acquring GIL will clear these pending changes
drop(gil);
let gil = Python::acquire_gil();
2020-05-06 08:04:57 +00:00
assert!(POOL.pointers_to_incref.lock().is_empty());
assert!(POOL.pointers_to_decref.lock().is_empty());
2020-05-06 08:04:57 +00:00
// Overall count is still unchanged
assert_eq!(count, obj.get_refcnt(gil.python()));
2020-05-03 21:29:44 +00:00
}
2020-05-13 17:36:40 +00:00
#[test]
fn test_update_counts_does_not_deadlock() {
// update_counts can run arbitrary Python code during Py_DECREF.
// if the locking is implemented incorrectly, it will deadlock.
let gil = Python::acquire_gil();
let obj = get_object(gil.python());
unsafe {
unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) {
// This line will implicitly call update_counts
// -> and so cause deadlock if update_counts is not handling recursion correctly.
let pool = GILPool::new();
// Rebuild obj so that it can be dropped
PyObject::from_owned_ptr(
pool.python(),
ffi::PyCapsule_GetPointer(capsule, std::ptr::null()) as _,
);
}
let ptr = obj.into_ptr();
let capsule = ffi::PyCapsule_New(ptr as _, std::ptr::null(), Some(capsule_drop));
POOL.register_decref(NonNull::new(capsule).unwrap());
2020-05-13 17:36:40 +00:00
// Updating the counts will call decref on the capsule, which calls capsule_drop
POOL.update_counts(gil.python())
}
}
}