mirror of
https://github.com/pybind/pybind11.git
synced 2026-04-19 22:39:09 +00:00
Merge branch 'master' into b-pass→vectorcall
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -88,7 +88,7 @@ jobs:
|
||||
cmake-args: -DCMAKE_CXX_STANDARD=20 -DPYBIND11_DISABLE_HANDLE_TYPE_NAME_DEFAULT_IMPLEMENTATION=ON
|
||||
- runs-on: ubuntu-latest
|
||||
python-version: '3.14'
|
||||
cmake-args: -DCMAKE_CXX_STANDARD=14 -DCMAKE_CXX_FLAGS="-DPYBIND11_HAS_SUBINTERPRETER_SUPPORT=0"
|
||||
cmake-args: -DCMAKE_CXX_STANDARD=14
|
||||
- runs-on: ubuntu-latest
|
||||
python-version: 'pypy-3.10'
|
||||
cmake-args: -DCMAKE_CXX_STANDARD=14
|
||||
|
||||
@@ -441,12 +441,11 @@ Note that this is run once for each (sub-)interpreter the module is imported int
|
||||
possibly concurrently. The PyModuleDef is allowed to be static, but the PyObject* resulting from
|
||||
PyModuleDef_Init should be treated like any other PyObject (so not shared across interpreters).
|
||||
*/
|
||||
#define PYBIND11_MODULE_PYINIT(name, pre_init, ...) \
|
||||
#define PYBIND11_MODULE_PYINIT(name, ...) \
|
||||
static int PYBIND11_CONCAT(pybind11_exec_, name)(PyObject *); \
|
||||
PYBIND11_PLUGIN_IMPL(name) { \
|
||||
PYBIND11_CHECK_PYTHON_VERSION \
|
||||
pre_init; \
|
||||
PYBIND11_ENSURE_INTERNALS_READY \
|
||||
pybind11::detail::ensure_internals(); \
|
||||
static ::pybind11::detail::slots_array mod_def_slots = ::pybind11::detail::init_slots( \
|
||||
&PYBIND11_CONCAT(pybind11_exec_, name), ##__VA_ARGS__); \
|
||||
static PyModuleDef def{/* m_base */ PyModuleDef_HEAD_INIT, \
|
||||
@@ -465,6 +464,7 @@ PyModuleDef_Init should be treated like any other PyObject (so not shared across
|
||||
static void PYBIND11_CONCAT(pybind11_init_, name)(::pybind11::module_ &); \
|
||||
int PYBIND11_CONCAT(pybind11_exec_, name)(PyObject * pm) { \
|
||||
try { \
|
||||
pybind11::detail::ensure_internals(); \
|
||||
auto m = pybind11::reinterpret_borrow<::pybind11::module_>(pm); \
|
||||
if (!pybind11::detail::get_cached_module(m.attr("__spec__").attr("name"))) { \
|
||||
PYBIND11_CONCAT(pybind11_init_, name)(m); \
|
||||
@@ -518,8 +518,7 @@ PyModuleDef_Init should be treated like any other PyObject (so not shared across
|
||||
|
||||
\endrst */
|
||||
#define PYBIND11_MODULE(name, variable, ...) \
|
||||
PYBIND11_MODULE_PYINIT( \
|
||||
name, (pybind11::detail::get_num_interpreters_seen() += 1), ##__VA_ARGS__) \
|
||||
PYBIND11_MODULE_PYINIT(name, ##__VA_ARGS__) \
|
||||
PYBIND11_MODULE_EXEC(name, variable)
|
||||
|
||||
// pop gnu-zero-variadic-macro-arguments
|
||||
|
||||
@@ -418,11 +418,12 @@ inline PyThreadState *get_thread_state_unchecked() {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// We use this counter to figure out if there are or have been multiple subinterpreters active at
|
||||
/// any point. This must never decrease while any interpreter may be running in any thread!
|
||||
inline std::atomic<int> &get_num_interpreters_seen() {
|
||||
static std::atomic<int> counter(0);
|
||||
return counter;
|
||||
/// We use this to figure out if there are or have been multiple subinterpreters active at any
|
||||
/// point. This must never go from true to false while any interpreter may be running in any
|
||||
/// thread!
|
||||
inline std::atomic_bool &has_seen_non_main_interpreter() {
|
||||
static std::atomic_bool multi(false);
|
||||
return multi;
|
||||
}
|
||||
|
||||
template <class T,
|
||||
@@ -550,6 +551,91 @@ inline object get_python_state_dict() {
|
||||
return state_dict;
|
||||
}
|
||||
|
||||
// Get or create per-storage capsule in the current interpreter's state dict.
|
||||
// - The storage is interpreter-dependent: different interpreters will have different storage.
|
||||
// This is important when using multiple-interpreters, to avoid sharing unshareable objects
|
||||
// between interpreters.
|
||||
// - There is one storage per `key` in an interpreter and it is accessible between all extensions
|
||||
// in the same interpreter.
|
||||
// - The life span of the storage is tied to the interpreter: it will be kept alive until the
|
||||
// interpreter shuts down.
|
||||
//
|
||||
// Use test-and-set pattern with `PyDict_SetDefault` for thread-safe concurrent access.
|
||||
// WARNING: There can be multiple threads creating the storage at the same time, while only one
|
||||
// will succeed in inserting its capsule into the dict. Therefore, the deleter will be
|
||||
// used to clean up the storage of the unused capsules.
|
||||
//
|
||||
// Returns: pair of (pointer to storage, bool indicating if newly created).
|
||||
// The bool follows std::map::insert convention: true = created, false = existed.
|
||||
template <typename Payload>
|
||||
std::pair<Payload *, bool> atomic_get_or_create_in_state_dict(const char *key,
|
||||
bool clear_destructor = false) {
|
||||
error_scope err_scope; // preserve any existing Python error states
|
||||
|
||||
auto state_dict = reinterpret_borrow<dict>(get_python_state_dict());
|
||||
PyObject *capsule_obj = nullptr;
|
||||
bool created = false;
|
||||
|
||||
// Try to get existing storage (fast path).
|
||||
capsule_obj = dict_getitemstring(state_dict.ptr(), key);
|
||||
if (capsule_obj == nullptr) {
|
||||
if (PyErr_Occurred()) {
|
||||
throw error_already_set();
|
||||
}
|
||||
// Storage doesn't exist yet, create a new one.
|
||||
// Use unique_ptr for exception safety: if capsule creation throws, the storage is
|
||||
// automatically deleted.
|
||||
auto storage_ptr = std::unique_ptr<Payload>(new Payload{});
|
||||
// Create capsule with destructor to clean up when the interpreter shuts down.
|
||||
auto new_capsule = capsule(
|
||||
storage_ptr.get(),
|
||||
// The destructor will be called when the capsule is GC'ed.
|
||||
// - If our capsule is inserted into the dict below, it will be kept alive until
|
||||
// interpreter shutdown, so the destructor will be called at that time.
|
||||
// - If our capsule is NOT inserted (another thread inserted first), it will be
|
||||
// destructed when going out of scope here, so the destructor will be called
|
||||
// immediately, which will also free the storage.
|
||||
/*destructor=*/[](void *ptr) -> void { delete static_cast<Payload *>(ptr); });
|
||||
// At this point, the capsule object is created successfully.
|
||||
// Release the unique_ptr and let the capsule object own the storage to avoid double-free.
|
||||
(void) storage_ptr.release();
|
||||
|
||||
// Use `PyDict_SetDefault` for atomic test-and-set:
|
||||
// - If key doesn't exist, inserts our capsule and returns it.
|
||||
// - If key exists (another thread inserted first), returns the existing value.
|
||||
// This is thread-safe because `PyDict_SetDefault` will hold a lock on the dict.
|
||||
//
|
||||
// NOTE: Here we use `PyDict_SetDefault` instead of `PyDict_SetDefaultRef` because the
|
||||
// capsule is kept alive until interpreter shutdown, so we do not need to handle
|
||||
// incref and decref here.
|
||||
capsule_obj = dict_setdefaultstring(state_dict.ptr(), key, new_capsule.ptr());
|
||||
if (capsule_obj == nullptr) {
|
||||
throw error_already_set();
|
||||
}
|
||||
created = (capsule_obj == new_capsule.ptr());
|
||||
if (clear_destructor && created) {
|
||||
// Our capsule was inserted.
|
||||
// Remove the destructor to leak the storage on interpreter shutdown.
|
||||
if (PyCapsule_SetDestructor(capsule_obj, nullptr) < 0) {
|
||||
throw error_already_set();
|
||||
}
|
||||
}
|
||||
// - If key already existed, our `new_capsule` is not inserted, it will be destructed when
|
||||
// going out of scope here, which will also free the storage.
|
||||
// - Otherwise, our `new_capsule` is now in the dict, and it owns the storage and the state
|
||||
// dict will incref it.
|
||||
}
|
||||
|
||||
// Get the storage pointer from the capsule.
|
||||
void *raw_ptr = PyCapsule_GetPointer(capsule_obj, /*name=*/nullptr);
|
||||
if (!raw_ptr) {
|
||||
raise_from(PyExc_SystemError,
|
||||
"pybind11::detail::atomic_get_or_create_in_state_dict() FAILED");
|
||||
throw error_already_set();
|
||||
}
|
||||
return std::pair<Payload *, bool>(static_cast<Payload *>(raw_ptr), created);
|
||||
}
|
||||
|
||||
template <typename InternalsType>
|
||||
class internals_pp_manager {
|
||||
public:
|
||||
@@ -564,7 +650,7 @@ public:
|
||||
/// acquire the GIL. Will never return nullptr.
|
||||
std::unique_ptr<InternalsType> *get_pp() {
|
||||
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
|
||||
if (get_num_interpreters_seen() > 1) {
|
||||
if (has_seen_non_main_interpreter()) {
|
||||
// Whenever the interpreter changes on the current thread we need to invalidate the
|
||||
// internals_pp so that it can be pulled from the interpreter's state dict. That is
|
||||
// slow, so we use the current PyThreadState to check if it is necessary.
|
||||
@@ -590,7 +676,7 @@ public:
|
||||
/// Drop all the references we're currently holding.
|
||||
void unref() {
|
||||
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
|
||||
if (get_num_interpreters_seen() > 1) {
|
||||
if (has_seen_non_main_interpreter()) {
|
||||
last_istate_tls() = nullptr;
|
||||
internals_p_tls() = nullptr;
|
||||
return;
|
||||
@@ -601,7 +687,7 @@ public:
|
||||
|
||||
void destroy() {
|
||||
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
|
||||
if (get_num_interpreters_seen() > 1) {
|
||||
if (has_seen_non_main_interpreter()) {
|
||||
auto *tstate = get_thread_state_unchecked();
|
||||
// this could be called without an active interpreter, just use what was cached
|
||||
if (!tstate || tstate->interp == last_istate_tls()) {
|
||||
@@ -622,26 +708,18 @@ private:
|
||||
: holder_id_(id), on_fetch_(on_fetch) {}
|
||||
|
||||
std::unique_ptr<InternalsType> *get_or_create_pp_in_state_dict() {
|
||||
error_scope err_scope;
|
||||
dict state_dict = get_python_state_dict();
|
||||
auto internals_obj
|
||||
= reinterpret_steal<object>(dict_getitemstringref(state_dict.ptr(), holder_id_));
|
||||
std::unique_ptr<InternalsType> *pp = nullptr;
|
||||
if (internals_obj) {
|
||||
void *raw_ptr = PyCapsule_GetPointer(internals_obj.ptr(), /*name=*/nullptr);
|
||||
if (!raw_ptr) {
|
||||
raise_from(PyExc_SystemError,
|
||||
"pybind11::detail::internals_pp_manager::get_pp_from_dict() FAILED");
|
||||
throw error_already_set();
|
||||
}
|
||||
pp = reinterpret_cast<std::unique_ptr<InternalsType> *>(raw_ptr);
|
||||
if (on_fetch_ && pp) {
|
||||
on_fetch_(pp->get());
|
||||
}
|
||||
} else {
|
||||
pp = new std::unique_ptr<InternalsType>;
|
||||
// NOLINTNEXTLINE(bugprone-casting-through-void)
|
||||
state_dict[holder_id_] = capsule(reinterpret_cast<void *>(pp));
|
||||
// The `unique_ptr<InternalsType>` output is leaked on interpreter shutdown. Once an
|
||||
// instance is created, it will never be deleted until the process exits (compare to
|
||||
// interpreter shutdown in multiple-interpreter scenarios).
|
||||
// Because we cannot guarantee the order of destruction of capsules in the interpreter
|
||||
// state dict, leaking avoids potential use-after-free issues during interpreter shutdown.
|
||||
auto result = atomic_get_or_create_in_state_dict<std::unique_ptr<InternalsType>>(
|
||||
holder_id_, /*clear_destructor=*/true);
|
||||
auto *pp = result.first;
|
||||
bool created = result.second;
|
||||
// Only call on_fetch_ when fetching existing internals, not when creating new ones.
|
||||
if (!created && on_fetch_ && pp) {
|
||||
on_fetch_(pp->get());
|
||||
}
|
||||
return pp;
|
||||
}
|
||||
@@ -660,6 +738,8 @@ private:
|
||||
|
||||
char const *holder_id_ = nullptr;
|
||||
on_fetch_function *on_fetch_ = nullptr;
|
||||
// Pointer-to-pointer to the singleton internals for the first seen interpreter (may not be the
|
||||
// main interpreter)
|
||||
std::unique_ptr<InternalsType> *internals_singleton_pp_;
|
||||
};
|
||||
|
||||
@@ -712,6 +792,16 @@ PYBIND11_NOINLINE internals &get_internals() {
|
||||
return *internals_ptr;
|
||||
}
|
||||
|
||||
inline void ensure_internals() {
|
||||
pybind11::detail::get_internals_pp_manager().unref();
|
||||
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
|
||||
if (PyInterpreterState_Get() != PyInterpreterState_Main()) {
|
||||
has_seen_non_main_interpreter() = true;
|
||||
}
|
||||
#endif
|
||||
pybind11::detail::get_internals();
|
||||
}
|
||||
|
||||
inline internals_pp_manager<local_internals> &get_local_internals_pp_manager() {
|
||||
// Use the address of this static itself as part of the key, so that the value is uniquely tied
|
||||
// to where the module is loaded in memory
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
PYBIND11_WARNING_PUSH
|
||||
PYBIND11_WARNING_DISABLE_CLANG("-Wgnu-zero-variadic-macro-arguments")
|
||||
#define PYBIND11_EMBEDDED_MODULE(name, variable, ...) \
|
||||
PYBIND11_MODULE_PYINIT(name, {}, ##__VA_ARGS__) \
|
||||
PYBIND11_MODULE_PYINIT(name, ##__VA_ARGS__) \
|
||||
::pybind11::detail::embedded_module PYBIND11_CONCAT(pybind11_module_, name)( \
|
||||
PYBIND11_TOSTRING(name), PYBIND11_CONCAT(PyInit_, name)); \
|
||||
PYBIND11_MODULE_EXEC(name, variable)
|
||||
@@ -202,7 +202,7 @@ inline void initialize_interpreter(bool init_signal_handlers = true,
|
||||
#endif
|
||||
|
||||
// There is exactly one interpreter alive currently.
|
||||
detail::get_num_interpreters_seen() = 1;
|
||||
detail::has_seen_non_main_interpreter() = false;
|
||||
}
|
||||
|
||||
/** \rst
|
||||
@@ -242,12 +242,12 @@ inline void initialize_interpreter(bool init_signal_handlers = true,
|
||||
\endrst */
|
||||
inline void finalize_interpreter() {
|
||||
// get rid of any thread-local interpreter cache that currently exists
|
||||
if (detail::get_num_interpreters_seen() > 1) {
|
||||
if (detail::has_seen_non_main_interpreter()) {
|
||||
detail::get_internals_pp_manager().unref();
|
||||
detail::get_local_internals_pp_manager().unref();
|
||||
|
||||
// We know there can be no other interpreter alive now, so we can lower the count
|
||||
detail::get_num_interpreters_seen() = 1;
|
||||
// We know there can be no other interpreter alive now
|
||||
detail::has_seen_non_main_interpreter() = false;
|
||||
}
|
||||
|
||||
// Re-fetch the internals pointer-to-pointer (but not the internals itself, which might not
|
||||
@@ -265,8 +265,8 @@ inline void finalize_interpreter() {
|
||||
// avoid undefined behaviors when initializing another interpreter
|
||||
detail::get_local_internals_pp_manager().destroy();
|
||||
|
||||
// We know there is no interpreter alive now, so we can reset the count
|
||||
detail::get_num_interpreters_seen() = 0;
|
||||
// We know there is no interpreter alive now, so we can reset the multi-flag
|
||||
detail::has_seen_non_main_interpreter() = false;
|
||||
}
|
||||
|
||||
/** \rst
|
||||
|
||||
@@ -3,17 +3,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "detail/common.h"
|
||||
#include "detail/internals.h"
|
||||
#include "gil.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <mutex>
|
||||
|
||||
#ifdef Py_GIL_DISABLED
|
||||
#if defined(Py_GIL_DISABLED) || defined(PYBIND11_HAS_SUBINTERPRETER_SUPPORT)
|
||||
# include <atomic>
|
||||
#endif
|
||||
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
|
||||
# include <cstdint>
|
||||
# include <memory>
|
||||
# include <string>
|
||||
#endif
|
||||
|
||||
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
|
||||
|
||||
PYBIND11_NAMESPACE_BEGIN(detail)
|
||||
#if defined(Py_GIL_DISABLED) || defined(PYBIND11_HAS_SUBINTERPRETER_SUPPORT)
|
||||
using atomic_bool = std::atomic_bool;
|
||||
#else
|
||||
using atomic_bool = bool;
|
||||
#endif
|
||||
PYBIND11_NAMESPACE_END(detail)
|
||||
|
||||
// Use the `gil_safe_call_once_and_store` class below instead of the naive
|
||||
//
|
||||
// static auto imported_obj = py::module_::import("module_name"); // BAD, DO NOT USE!
|
||||
@@ -48,12 +62,23 @@ PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
|
||||
// functions, which is usually the case.
|
||||
//
|
||||
// For in-depth background, see docs/advanced/deadlock.md
|
||||
#ifndef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
|
||||
// Subinterpreter support is disabled.
|
||||
// In this case, we can store the result globally, because there is only a single interpreter.
|
||||
//
|
||||
// The life span of the stored result is the entire process lifetime. It is leaked on process
|
||||
// termination to avoid destructor calls after the Python interpreter was finalized.
|
||||
template <typename T>
|
||||
class gil_safe_call_once_and_store {
|
||||
public:
|
||||
// PRECONDITION: The GIL must be held when `call_once_and_store_result()` is called.
|
||||
//
|
||||
// NOTE: The second parameter (finalize callback) is intentionally unused when subinterpreter
|
||||
// support is disabled. In that case, storage is process-global and intentionally leaked to
|
||||
// avoid calling destructors after the Python interpreter has been finalized.
|
||||
template <typename Callable>
|
||||
gil_safe_call_once_and_store &call_once_and_store_result(Callable &&fn) {
|
||||
gil_safe_call_once_and_store &call_once_and_store_result(Callable &&fn,
|
||||
void (*)(T &) /*unused*/ = nullptr) {
|
||||
if (!is_initialized_) { // This read is guarded by the GIL.
|
||||
// Multiple threads may enter here, because the GIL is released in the next line and
|
||||
// CPython API calls in the `fn()` call below may release and reacquire the GIL.
|
||||
@@ -74,29 +99,175 @@ public:
|
||||
T &get_stored() {
|
||||
assert(is_initialized_);
|
||||
PYBIND11_WARNING_PUSH
|
||||
#if !defined(__clang__) && defined(__GNUC__) && __GNUC__ < 5
|
||||
# if !defined(__clang__) && defined(__GNUC__) && __GNUC__ < 5
|
||||
// Needed for gcc 4.8.5
|
||||
PYBIND11_WARNING_DISABLE_GCC("-Wstrict-aliasing")
|
||||
#endif
|
||||
# endif
|
||||
return *reinterpret_cast<T *>(storage_);
|
||||
PYBIND11_WARNING_POP
|
||||
}
|
||||
|
||||
constexpr gil_safe_call_once_and_store() = default;
|
||||
// The instance is a global static, so its destructor runs when the process
|
||||
// is terminating. Therefore, do nothing here because the Python interpreter
|
||||
// may have been finalized already.
|
||||
PYBIND11_DTOR_CONSTEXPR ~gil_safe_call_once_and_store() = default;
|
||||
|
||||
// Disable copy and move operations.
|
||||
gil_safe_call_once_and_store(const gil_safe_call_once_and_store &) = delete;
|
||||
gil_safe_call_once_and_store(gil_safe_call_once_and_store &&) = delete;
|
||||
gil_safe_call_once_and_store &operator=(const gil_safe_call_once_and_store &) = delete;
|
||||
gil_safe_call_once_and_store &operator=(gil_safe_call_once_and_store &&) = delete;
|
||||
|
||||
private:
|
||||
// The global static storage (per-process) when subinterpreter support is disabled.
|
||||
alignas(T) char storage_[sizeof(T)] = {};
|
||||
std::once_flag once_flag_;
|
||||
#ifdef Py_GIL_DISABLED
|
||||
std::atomic_bool
|
||||
#else
|
||||
bool
|
||||
#endif
|
||||
is_initialized_{false};
|
||||
|
||||
// The `is_initialized_`-`storage_` pair is very similar to `std::optional`,
|
||||
// but the latter does not have the triviality properties of former,
|
||||
// therefore `std::optional` is not a viable alternative here.
|
||||
detail::atomic_bool is_initialized_{false};
|
||||
};
|
||||
#else
|
||||
// Subinterpreter support is enabled.
|
||||
// In this case, we should store the result per-interpreter instead of globally, because each
|
||||
// subinterpreter has its own separate state. The cached result may not shareable across
|
||||
// interpreters (e.g., imported modules and their members).
|
||||
|
||||
PYBIND11_NAMESPACE_BEGIN(detail)
|
||||
|
||||
template <typename T>
|
||||
struct call_once_storage {
|
||||
alignas(T) char storage[sizeof(T)] = {};
|
||||
std::once_flag once_flag;
|
||||
void (*finalize)(T &) = nullptr;
|
||||
std::atomic_bool is_initialized{false};
|
||||
|
||||
call_once_storage() = default;
|
||||
~call_once_storage() {
|
||||
if (is_initialized) {
|
||||
if (finalize != nullptr) {
|
||||
finalize(*reinterpret_cast<T *>(storage));
|
||||
} else {
|
||||
reinterpret_cast<T *>(storage)->~T();
|
||||
}
|
||||
}
|
||||
}
|
||||
call_once_storage(const call_once_storage &) = delete;
|
||||
call_once_storage(call_once_storage &&) = delete;
|
||||
call_once_storage &operator=(const call_once_storage &) = delete;
|
||||
call_once_storage &operator=(call_once_storage &&) = delete;
|
||||
};
|
||||
|
||||
PYBIND11_NAMESPACE_END(detail)
|
||||
|
||||
// Prefix for storage keys in the interpreter state dict.
|
||||
# define PYBIND11_CALL_ONCE_STORAGE_KEY_PREFIX PYBIND11_INTERNALS_ID "_call_once_storage__"
|
||||
|
||||
// The life span of the stored result is the entire interpreter lifetime. An additional
|
||||
// `finalize_fn` can be provided to clean up the stored result when the interpreter is destroyed.
|
||||
template <typename T>
|
||||
class gil_safe_call_once_and_store {
|
||||
public:
|
||||
// PRECONDITION: The GIL must be held when `call_once_and_store_result()` is called.
|
||||
template <typename Callable>
|
||||
gil_safe_call_once_and_store &call_once_and_store_result(Callable &&fn,
|
||||
void (*finalize_fn)(T &) = nullptr) {
|
||||
if (!is_last_storage_valid()) {
|
||||
// Multiple threads may enter here, because the GIL is released in the next line and
|
||||
// CPython API calls in the `fn()` call below may release and reacquire the GIL.
|
||||
gil_scoped_release gil_rel; // Needed to establish lock ordering.
|
||||
// There can be multiple threads going through here.
|
||||
storage_type *value = nullptr;
|
||||
{
|
||||
gil_scoped_acquire gil_acq; // Restore lock ordering.
|
||||
// This function is thread-safe under free-threading.
|
||||
value = get_or_create_storage_in_state_dict();
|
||||
}
|
||||
assert(value != nullptr);
|
||||
std::call_once(value->once_flag, [&] {
|
||||
// Only one thread will ever enter here.
|
||||
gil_scoped_acquire gil_acq;
|
||||
// fn may release, but will reacquire, the GIL.
|
||||
::new (value->storage) T(fn());
|
||||
value->finalize = finalize_fn;
|
||||
value->is_initialized = true;
|
||||
last_storage_ptr_ = reinterpret_cast<T *>(value->storage);
|
||||
is_initialized_by_at_least_one_interpreter_ = true;
|
||||
});
|
||||
// All threads will observe `is_initialized_by_at_least_one_interpreter_` as true here.
|
||||
}
|
||||
// Intentionally not returning `T &` to ensure the calling code is self-documenting.
|
||||
return *this;
|
||||
}
|
||||
|
||||
// This must only be called after `call_once_and_store_result()` was called.
|
||||
T &get_stored() {
|
||||
T *result = last_storage_ptr_;
|
||||
if (!is_last_storage_valid()) {
|
||||
gil_scoped_acquire gil_acq;
|
||||
auto *value = get_or_create_storage_in_state_dict();
|
||||
result = last_storage_ptr_ = reinterpret_cast<T *>(value->storage);
|
||||
}
|
||||
assert(result != nullptr);
|
||||
return *result;
|
||||
}
|
||||
|
||||
constexpr gil_safe_call_once_and_store() = default;
|
||||
// The instance is a global static, so its destructor runs when the process
|
||||
// is terminating. Therefore, do nothing here because the Python interpreter
|
||||
// may have been finalized already.
|
||||
PYBIND11_DTOR_CONSTEXPR ~gil_safe_call_once_and_store() = default;
|
||||
|
||||
// Disable copy and move operations because the memory address is used as key.
|
||||
gil_safe_call_once_and_store(const gil_safe_call_once_and_store &) = delete;
|
||||
gil_safe_call_once_and_store(gil_safe_call_once_and_store &&) = delete;
|
||||
gil_safe_call_once_and_store &operator=(const gil_safe_call_once_and_store &) = delete;
|
||||
gil_safe_call_once_and_store &operator=(gil_safe_call_once_and_store &&) = delete;
|
||||
|
||||
private:
|
||||
using storage_type = detail::call_once_storage<T>;
|
||||
|
||||
// Indicator of fast path for single-interpreter case.
|
||||
bool is_last_storage_valid() const {
|
||||
return is_initialized_by_at_least_one_interpreter_
|
||||
&& !detail::has_seen_non_main_interpreter();
|
||||
}
|
||||
|
||||
// Get the unique key for this storage instance in the interpreter's state dict.
|
||||
// The return type should not be `py::str` because PyObject is interpreter-dependent.
|
||||
std::string get_storage_key() const {
|
||||
// The instance is expected to be global static, so using its address as unique identifier.
|
||||
// The typical usage is like:
|
||||
//
|
||||
// PYBIND11_CONSTINIT static gil_safe_call_once_and_store<T> storage;
|
||||
//
|
||||
return PYBIND11_CALL_ONCE_STORAGE_KEY_PREFIX
|
||||
+ std::to_string(reinterpret_cast<std::uintptr_t>(this));
|
||||
}
|
||||
|
||||
// Get or create per-storage capsule in the current interpreter's state dict.
|
||||
// The storage is interpreter-dependent and will not be shared across interpreters.
|
||||
storage_type *get_or_create_storage_in_state_dict() {
|
||||
return detail::atomic_get_or_create_in_state_dict<storage_type>(get_storage_key().c_str())
|
||||
.first;
|
||||
}
|
||||
|
||||
// No storage needed when subinterpreter support is enabled.
|
||||
// The actual storage is stored in the per-interpreter state dict via
|
||||
// `get_or_create_storage_in_state_dict()`.
|
||||
|
||||
// Fast local cache to avoid repeated lookups when there are no multiple interpreters.
|
||||
// This is only valid if there is a single interpreter. Otherwise, it is not used.
|
||||
// WARNING: We cannot use thread local cache similar to `internals_pp_manager::internals_p_tls`
|
||||
// because the thread local storage cannot be explicitly invalidated when interpreters
|
||||
// are destroyed (unlike `internals_pp_manager` which has explicit hooks for that).
|
||||
T *last_storage_ptr_ = nullptr;
|
||||
// This flag is true if the value has been initialized by any interpreter (may not be the
|
||||
// current one).
|
||||
detail::atomic_bool is_initialized_by_at_least_one_interpreter_{false};
|
||||
};
|
||||
#endif
|
||||
|
||||
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
|
||||
|
||||
@@ -997,8 +997,10 @@ inline PyObject *dict_getitem(PyObject *v, PyObject *key) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// PyDict_GetItemStringRef was added in Python 3.13.0a1.
|
||||
// See also: https://github.com/python/pythoncapi-compat/blob/main/pythoncapi_compat.h
|
||||
inline PyObject *dict_getitemstringref(PyObject *v, const char *key) {
|
||||
#if PY_VERSION_HEX >= 0x030D0000
|
||||
#if PY_VERSION_HEX >= 0x030D00A1
|
||||
PyObject *rv = nullptr;
|
||||
if (PyDict_GetItemStringRef(v, key, &rv) < 0) {
|
||||
throw error_already_set();
|
||||
@@ -1014,6 +1016,46 @@ inline PyObject *dict_getitemstringref(PyObject *v, const char *key) {
|
||||
#endif
|
||||
}
|
||||
|
||||
inline PyObject *dict_setdefaultstring(PyObject *v, const char *key, PyObject *defaultobj) {
|
||||
PyObject *kv = PyUnicode_FromString(key);
|
||||
if (kv == nullptr) {
|
||||
throw error_already_set();
|
||||
}
|
||||
|
||||
PyObject *rv = PyDict_SetDefault(v, kv, defaultobj);
|
||||
Py_DECREF(kv);
|
||||
if (rv == nullptr) {
|
||||
throw error_already_set();
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
// PyDict_SetDefaultRef was added in Python 3.13.0a4.
|
||||
// See also: https://github.com/python/pythoncapi-compat/blob/main/pythoncapi_compat.h
|
||||
inline PyObject *dict_setdefaultstringref(PyObject *v, const char *key, PyObject *defaultobj) {
|
||||
#if PY_VERSION_HEX >= 0x030D00A4
|
||||
PyObject *kv = PyUnicode_FromString(key);
|
||||
if (kv == nullptr) {
|
||||
throw error_already_set();
|
||||
}
|
||||
|
||||
PyObject *rv = nullptr;
|
||||
if (PyDict_SetDefaultRef(v, kv, defaultobj, &rv) < 0) {
|
||||
Py_DECREF(kv);
|
||||
throw error_already_set();
|
||||
}
|
||||
Py_DECREF(kv);
|
||||
return rv;
|
||||
#else
|
||||
PyObject *rv = dict_setdefaultstring(v, key, defaultobj);
|
||||
if (rv == nullptr || PyErr_Occurred()) {
|
||||
throw error_already_set();
|
||||
}
|
||||
Py_XINCREF(rv);
|
||||
return rv;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Helper aliases/functions to support implicit casting of values given to python
|
||||
// accessors/methods. When given a pyobject, this simply returns the pyobject as-is; for other C++
|
||||
// type, the value goes through pybind11::cast(obj) to convert it to an `object`.
|
||||
|
||||
@@ -109,7 +109,7 @@ public:
|
||||
|
||||
// upon success, the new interpreter is activated in this thread
|
||||
result.istate_ = result.creation_tstate_->interp;
|
||||
detail::get_num_interpreters_seen() += 1; // there are now many interpreters
|
||||
detail::has_seen_non_main_interpreter() = true;
|
||||
detail::get_internals(); // initialize internals.tstate, amongst other things...
|
||||
|
||||
// In 3.13+ this state should be deleted right away, and the memory will be reused for
|
||||
|
||||
@@ -578,24 +578,40 @@ add_custom_target(
|
||||
USES_TERMINAL)
|
||||
|
||||
if(NOT PYBIND11_CUDA_TESTS)
|
||||
# This module doesn't get mixed with other test modules because those aren't subinterpreter safe.
|
||||
pybind11_add_module(mod_per_interpreter_gil THIN_LTO mod_per_interpreter_gil.cpp)
|
||||
pybind11_add_module(mod_shared_interpreter_gil THIN_LTO mod_shared_interpreter_gil.cpp)
|
||||
set_target_properties(mod_per_interpreter_gil PROPERTIES LIBRARY_OUTPUT_DIRECTORY
|
||||
"$<1:${CMAKE_CURRENT_BINARY_DIR}>")
|
||||
set_target_properties(mod_shared_interpreter_gil PROPERTIES LIBRARY_OUTPUT_DIRECTORY
|
||||
"$<1:${CMAKE_CURRENT_BINARY_DIR}>")
|
||||
set(PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES
|
||||
mod_per_interpreter_gil mod_shared_interpreter_gil mod_per_interpreter_gil_with_singleton)
|
||||
|
||||
# These modules don't get mixed with other test modules because those aren't subinterpreter safe.
|
||||
foreach(mod IN LISTS PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES)
|
||||
pybind11_add_module("${mod}" THIN_LTO "${mod}.cpp")
|
||||
pybind11_enable_warnings("${mod}")
|
||||
endforeach()
|
||||
|
||||
# Put the built modules next to `pybind11_tests.so` so that the test scripts can find them.
|
||||
get_target_property(pybind11_tests_output_directory pybind11_tests LIBRARY_OUTPUT_DIRECTORY)
|
||||
foreach(mod IN LISTS PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES)
|
||||
set_target_properties("${mod}" PROPERTIES LIBRARY_OUTPUT_DIRECTORY
|
||||
"${pybind11_tests_output_directory}")
|
||||
# Also set config-specific output directories for multi-configuration generators (MSVC)
|
||||
if(DEFINED CMAKE_CONFIGURATION_TYPES)
|
||||
foreach(config ${CMAKE_CONFIGURATION_TYPES})
|
||||
string(TOUPPER ${config} config)
|
||||
set_target_properties("${mod}" PROPERTIES LIBRARY_OUTPUT_DIRECTORY_${config}
|
||||
"${pybind11_tests_output_directory}")
|
||||
endforeach()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(PYBIND11_TEST_SMART_HOLDER)
|
||||
target_compile_definitions(
|
||||
mod_per_interpreter_gil
|
||||
PUBLIC -DPYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE
|
||||
)
|
||||
target_compile_definitions(
|
||||
mod_shared_interpreter_gil
|
||||
PUBLIC -DPYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE
|
||||
)
|
||||
foreach(mod IN LISTS PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES)
|
||||
target_compile_definitions(
|
||||
"${mod}"
|
||||
PUBLIC
|
||||
-DPYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE)
|
||||
endforeach()
|
||||
endif()
|
||||
add_dependencies(pytest mod_per_interpreter_gil mod_shared_interpreter_gil)
|
||||
|
||||
add_dependencies(pytest ${PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES})
|
||||
endif()
|
||||
|
||||
if(PYBIND11_TEST_OVERRIDE)
|
||||
|
||||
147
tests/mod_per_interpreter_gil_with_singleton.cpp
Normal file
147
tests/mod_per_interpreter_gil_with_singleton.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
#ifdef PYBIND11_HAS_NATIVE_ENUM
|
||||
# include <pybind11/native_enum.h>
|
||||
#endif
|
||||
|
||||
namespace pybind11_tests {
|
||||
namespace mod_per_interpreter_gil_with_singleton {
|
||||
// A singleton class that holds references to certain Python objects
|
||||
// This singleton is per-interpreter using gil_safe_call_once_and_store
|
||||
class MySingleton {
|
||||
public:
|
||||
MySingleton() = default;
|
||||
~MySingleton() = default;
|
||||
MySingleton(const MySingleton &) = delete;
|
||||
MySingleton &operator=(const MySingleton &) = delete;
|
||||
MySingleton(MySingleton &&) = default;
|
||||
MySingleton &operator=(MySingleton &&) = default;
|
||||
|
||||
static MySingleton &get_instance() {
|
||||
PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<MySingleton> storage;
|
||||
return storage
|
||||
.call_once_and_store_result([]() -> MySingleton {
|
||||
MySingleton instance{};
|
||||
|
||||
auto emplace = [&instance](const py::handle &obj) -> void {
|
||||
obj.inc_ref(); // Ensure the object is not GC'd while interpreter is alive
|
||||
instance.objects.emplace_back(obj);
|
||||
};
|
||||
|
||||
// Example objects to store in the singleton
|
||||
emplace(py::type::handle_of(py::none())); // static type
|
||||
emplace(py::type::handle_of(py::tuple())); // static type
|
||||
emplace(py::type::handle_of(py::list())); // static type
|
||||
emplace(py::type::handle_of(py::dict())); // static type
|
||||
emplace(py::module_::import("collections").attr("OrderedDict")); // static type
|
||||
emplace(py::module_::import("collections").attr("defaultdict")); // heap type
|
||||
emplace(py::module_::import("collections").attr("deque")); // heap type
|
||||
|
||||
assert(instance.objects.size() == 7);
|
||||
return instance;
|
||||
})
|
||||
.get_stored();
|
||||
}
|
||||
|
||||
std::vector<py::handle> &get_objects() { return objects; }
|
||||
|
||||
static void init() {
|
||||
// Ensure the singleton is created
|
||||
auto &instance = get_instance();
|
||||
(void) instance; // suppress unused variable warning
|
||||
assert(instance.objects.size() == 7);
|
||||
// Register cleanup at interpreter exit
|
||||
py::module_::import("atexit").attr("register")(py::cpp_function(&MySingleton::clear));
|
||||
}
|
||||
|
||||
static void clear() {
|
||||
auto &instance = get_instance();
|
||||
(void) instance; // suppress unused variable warning
|
||||
assert(instance.objects.size() == 7);
|
||||
for (const auto &obj : instance.objects) {
|
||||
obj.dec_ref();
|
||||
}
|
||||
instance.objects.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<py::handle> objects;
|
||||
};
|
||||
|
||||
class MyClass {
|
||||
public:
|
||||
explicit MyClass(py::ssize_t v) : value(v) {}
|
||||
py::ssize_t get_value() const { return value; }
|
||||
|
||||
private:
|
||||
py::ssize_t value;
|
||||
};
|
||||
|
||||
class MyGlobalError : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
class MyLocalError : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
enum class MyEnum : int {
|
||||
ONE = 1,
|
||||
TWO = 2,
|
||||
THREE = 3,
|
||||
};
|
||||
} // namespace mod_per_interpreter_gil_with_singleton
|
||||
} // namespace pybind11_tests
|
||||
|
||||
PYBIND11_MODULE(mod_per_interpreter_gil_with_singleton,
|
||||
m,
|
||||
py::mod_gil_not_used(),
|
||||
py::multiple_interpreters::per_interpreter_gil()) {
|
||||
using namespace pybind11_tests::mod_per_interpreter_gil_with_singleton;
|
||||
|
||||
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
|
||||
m.attr("defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT") = true;
|
||||
#else
|
||||
m.attr("defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT") = false;
|
||||
#endif
|
||||
|
||||
MySingleton::init();
|
||||
|
||||
// Ensure py::multiple_interpreters::per_interpreter_gil() works with singletons using
|
||||
// py::gil_safe_call_once_and_store
|
||||
m.def(
|
||||
"get_objects_in_singleton",
|
||||
[]() -> std::vector<py::handle> { return MySingleton::get_instance().get_objects(); },
|
||||
"Get the list of objects stored in the singleton");
|
||||
|
||||
// Ensure py::multiple_interpreters::per_interpreter_gil() works with class bindings
|
||||
py::class_<MyClass>(m, "MyClass")
|
||||
.def(py::init<py::ssize_t>())
|
||||
.def("get_value", &MyClass::get_value);
|
||||
|
||||
// Ensure py::multiple_interpreters::per_interpreter_gil() works with global exceptions
|
||||
py::register_exception<MyGlobalError>(m, "MyGlobalError");
|
||||
// Ensure py::multiple_interpreters::per_interpreter_gil() works with local exceptions
|
||||
py::register_local_exception<MyLocalError>(m, "MyLocalError");
|
||||
|
||||
#ifdef PYBIND11_HAS_NATIVE_ENUM
|
||||
// Ensure py::multiple_interpreters::per_interpreter_gil() works with native_enum
|
||||
py::native_enum<MyEnum>(m, "MyEnum", "enum.IntEnum")
|
||||
.value("ONE", MyEnum::ONE)
|
||||
.value("TWO", MyEnum::TWO)
|
||||
.value("THREE", MyEnum::THREE)
|
||||
.finalize();
|
||||
#else
|
||||
py::enum_<MyEnum>(m, "MyEnum")
|
||||
.value("ONE", MyEnum::ONE)
|
||||
.value("TWO", MyEnum::TWO)
|
||||
.value("THREE", MyEnum::THREE);
|
||||
#endif
|
||||
}
|
||||
@@ -3,10 +3,14 @@ from __future__ import annotations
|
||||
import contextlib
|
||||
import os
|
||||
import pickle
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
import pybind11_tests
|
||||
|
||||
# 3.14.0b3+, though sys.implementation.supports_isolated_interpreters is being added in b4
|
||||
# Can be simplified when we drop support for the first three betas
|
||||
CONCURRENT_INTERPRETERS_SUPPORT = (
|
||||
@@ -82,21 +86,23 @@ def get_interpreters(*, modern: bool):
|
||||
def test_independent_subinterpreters():
|
||||
"""Makes sure the internals object differs across independent subinterpreters"""
|
||||
|
||||
sys.path.append(".")
|
||||
sys.path.insert(0, os.path.dirname(pybind11_tests.__file__))
|
||||
|
||||
run_string, create = get_interpreters(modern=True)
|
||||
|
||||
m = pytest.importorskip("mod_per_interpreter_gil")
|
||||
import mod_per_interpreter_gil as m
|
||||
|
||||
if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT:
|
||||
pytest.skip("Does not have subinterpreter support compiled in")
|
||||
|
||||
code = """
|
||||
import mod_per_interpreter_gil as m
|
||||
import pickle
|
||||
with open(pipeo, 'wb') as f:
|
||||
pickle.dump(m.internals_at(), f)
|
||||
"""
|
||||
code = textwrap.dedent(
|
||||
"""
|
||||
import mod_per_interpreter_gil as m
|
||||
import pickle
|
||||
with open(pipeo, 'wb') as f:
|
||||
pickle.dump(m.internals_at(), f)
|
||||
"""
|
||||
).strip()
|
||||
|
||||
with create() as interp1, create() as interp2:
|
||||
try:
|
||||
@@ -131,20 +137,22 @@ with open(pipeo, 'wb') as f:
|
||||
def test_independent_subinterpreters_modern():
|
||||
"""Makes sure the internals object differs across independent subinterpreters. Modern (3.14+) syntax."""
|
||||
|
||||
sys.path.append(".")
|
||||
sys.path.insert(0, os.path.dirname(pybind11_tests.__file__))
|
||||
|
||||
m = pytest.importorskip("mod_per_interpreter_gil")
|
||||
import mod_per_interpreter_gil as m
|
||||
|
||||
if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT:
|
||||
pytest.skip("Does not have subinterpreter support compiled in")
|
||||
|
||||
from concurrent import interpreters
|
||||
|
||||
code = """
|
||||
import mod_per_interpreter_gil as m
|
||||
code = textwrap.dedent(
|
||||
"""
|
||||
import mod_per_interpreter_gil as m
|
||||
|
||||
values.put_nowait(m.internals_at())
|
||||
"""
|
||||
values.put_nowait(m.internals_at())
|
||||
"""
|
||||
).strip()
|
||||
|
||||
with contextlib.closing(interpreters.create()) as interp1, contextlib.closing(
|
||||
interpreters.create()
|
||||
@@ -175,21 +183,23 @@ values.put_nowait(m.internals_at())
|
||||
def test_dependent_subinterpreters():
|
||||
"""Makes sure the internals object differs across subinterpreters"""
|
||||
|
||||
sys.path.append(".")
|
||||
sys.path.insert(0, os.path.dirname(pybind11_tests.__file__))
|
||||
|
||||
run_string, create = get_interpreters(modern=False)
|
||||
|
||||
m = pytest.importorskip("mod_shared_interpreter_gil")
|
||||
import mod_shared_interpreter_gil as m
|
||||
|
||||
if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT:
|
||||
pytest.skip("Does not have subinterpreter support compiled in")
|
||||
|
||||
code = """
|
||||
import mod_shared_interpreter_gil as m
|
||||
import pickle
|
||||
with open(pipeo, 'wb') as f:
|
||||
pickle.dump(m.internals_at(), f)
|
||||
"""
|
||||
code = textwrap.dedent(
|
||||
"""
|
||||
import mod_shared_interpreter_gil as m
|
||||
import pickle
|
||||
with open(pipeo, 'wb') as f:
|
||||
pickle.dump(m.internals_at(), f)
|
||||
"""
|
||||
).strip()
|
||||
|
||||
with create("legacy") as interp1:
|
||||
pipei, pipeo = os.pipe()
|
||||
@@ -198,3 +208,244 @@ with open(pipeo, 'wb') as f:
|
||||
res1 = pickle.load(f)
|
||||
|
||||
assert res1 != m.internals_at(), "internals should differ from main interpreter"
|
||||
|
||||
|
||||
PREAMBLE_CODE = textwrap.dedent(
|
||||
f"""
|
||||
def test():
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, {os.path.dirname(pybind11_tests.__file__)!r})
|
||||
|
||||
import collections
|
||||
import mod_per_interpreter_gil_with_singleton as m
|
||||
|
||||
objects = m.get_objects_in_singleton()
|
||||
expected = [
|
||||
type(None), # static type: shared between interpreters
|
||||
tuple, # static type: shared between interpreters
|
||||
list, # static type: shared between interpreters
|
||||
dict, # static type: shared between interpreters
|
||||
collections.OrderedDict, # static type: shared between interpreters
|
||||
collections.defaultdict, # heap type: dynamically created per interpreter
|
||||
collections.deque, # heap type: dynamically created per interpreter
|
||||
]
|
||||
# Check that we have the expected objects. Avoid IndexError by checking lengths first.
|
||||
assert len(objects) == len(expected), (
|
||||
f"Expected {{expected!r}} ({{len(expected)}}), got {{objects!r}} ({{len(objects)}})."
|
||||
)
|
||||
# The first ones are static types shared between interpreters.
|
||||
assert objects[:-2] == expected[:-2], (
|
||||
f"Expected static objects {{expected[:-2]!r}}, got {{objects[:-2]!r}}."
|
||||
)
|
||||
# The last two are heap types created per-interpreter.
|
||||
# The expected objects are dynamically imported from `collections`.
|
||||
assert objects[-2:] == expected[-2:], (
|
||||
f"Expected heap objects {{expected[-2:]!r}}, got {{objects[-2:]!r}}."
|
||||
)
|
||||
|
||||
assert hasattr(m, 'MyClass'), "Module missing MyClass"
|
||||
assert hasattr(m, 'MyGlobalError'), "Module missing MyGlobalError"
|
||||
assert hasattr(m, 'MyLocalError'), "Module missing MyLocalError"
|
||||
assert hasattr(m, 'MyEnum'), "Module missing MyEnum"
|
||||
"""
|
||||
).lstrip()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
|
||||
)
|
||||
@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+")
|
||||
def test_import_module_with_singleton_per_interpreter():
|
||||
"""Tests that a singleton storing Python objects works correctly per-interpreter"""
|
||||
from concurrent import interpreters
|
||||
|
||||
code = f"{PREAMBLE_CODE.strip()}\n\ntest()\n"
|
||||
with contextlib.closing(interpreters.create()) as interp:
|
||||
interp.exec(code)
|
||||
|
||||
|
||||
def check_script_success_in_subprocess(code: str, *, rerun: int = 8) -> None:
|
||||
"""Runs the given code in a subprocess."""
|
||||
code = textwrap.dedent(code).strip()
|
||||
try:
|
||||
for _ in range(rerun): # run flakily failing test multiple times
|
||||
subprocess.check_output(
|
||||
[sys.executable, "-c", code],
|
||||
cwd=os.getcwd(),
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as ex:
|
||||
raise RuntimeError(
|
||||
f"Subprocess failed with exit code {ex.returncode}.\n\n"
|
||||
f"Code:\n"
|
||||
f"```python\n"
|
||||
f"{code}\n"
|
||||
f"```\n\n"
|
||||
f"Output:\n"
|
||||
f"{ex.output}"
|
||||
) from None
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
|
||||
)
|
||||
@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+")
|
||||
def test_import_in_subinterpreter_after_main():
|
||||
"""Tests that importing a module in a subinterpreter after the main interpreter works correctly"""
|
||||
check_script_success_in_subprocess(
|
||||
PREAMBLE_CODE
|
||||
+ textwrap.dedent(
|
||||
"""
|
||||
import contextlib
|
||||
import gc
|
||||
from concurrent import interpreters
|
||||
|
||||
test()
|
||||
|
||||
interp = None
|
||||
with contextlib.closing(interpreters.create()) as interp:
|
||||
interp.call(test)
|
||||
|
||||
del interp
|
||||
for _ in range(5):
|
||||
gc.collect()
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
check_script_success_in_subprocess(
|
||||
PREAMBLE_CODE
|
||||
+ textwrap.dedent(
|
||||
"""
|
||||
import contextlib
|
||||
import gc
|
||||
import random
|
||||
from concurrent import interpreters
|
||||
|
||||
test()
|
||||
|
||||
interps = interp = None
|
||||
with contextlib.ExitStack() as stack:
|
||||
interps = [
|
||||
stack.enter_context(contextlib.closing(interpreters.create()))
|
||||
for _ in range(8)
|
||||
]
|
||||
random.shuffle(interps)
|
||||
for interp in interps:
|
||||
interp.call(test)
|
||||
|
||||
del interps, interp, stack
|
||||
for _ in range(5):
|
||||
gc.collect()
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
|
||||
)
|
||||
@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+")
|
||||
def test_import_in_subinterpreter_before_main():
|
||||
"""Tests that importing a module in a subinterpreter before the main interpreter works correctly"""
|
||||
check_script_success_in_subprocess(
|
||||
PREAMBLE_CODE
|
||||
+ textwrap.dedent(
|
||||
"""
|
||||
import contextlib
|
||||
import gc
|
||||
from concurrent import interpreters
|
||||
|
||||
interp = None
|
||||
with contextlib.closing(interpreters.create()) as interp:
|
||||
interp.call(test)
|
||||
|
||||
test()
|
||||
|
||||
del interp
|
||||
for _ in range(5):
|
||||
gc.collect()
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
check_script_success_in_subprocess(
|
||||
PREAMBLE_CODE
|
||||
+ textwrap.dedent(
|
||||
"""
|
||||
import contextlib
|
||||
import gc
|
||||
from concurrent import interpreters
|
||||
|
||||
interps = interp = None
|
||||
with contextlib.ExitStack() as stack:
|
||||
interps = [
|
||||
stack.enter_context(contextlib.closing(interpreters.create()))
|
||||
for _ in range(8)
|
||||
]
|
||||
for interp in interps:
|
||||
interp.call(test)
|
||||
|
||||
test()
|
||||
|
||||
del interps, interp, stack
|
||||
for _ in range(5):
|
||||
gc.collect()
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
check_script_success_in_subprocess(
|
||||
PREAMBLE_CODE
|
||||
+ textwrap.dedent(
|
||||
"""
|
||||
import contextlib
|
||||
import gc
|
||||
from concurrent import interpreters
|
||||
|
||||
interps = interp = None
|
||||
with contextlib.ExitStack() as stack:
|
||||
interps = [
|
||||
stack.enter_context(contextlib.closing(interpreters.create()))
|
||||
for _ in range(8)
|
||||
]
|
||||
for interp in interps:
|
||||
interp.call(test)
|
||||
|
||||
test()
|
||||
|
||||
del interps, interp, stack
|
||||
for _ in range(5):
|
||||
gc.collect()
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
|
||||
)
|
||||
@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+")
|
||||
def test_import_in_subinterpreter_concurrently():
|
||||
"""Tests that importing a module in multiple subinterpreters concurrently works correctly"""
|
||||
check_script_success_in_subprocess(
|
||||
PREAMBLE_CODE
|
||||
+ textwrap.dedent(
|
||||
"""
|
||||
import gc
|
||||
from concurrent.futures import InterpreterPoolExecutor, as_completed
|
||||
|
||||
futures = future = None
|
||||
with InterpreterPoolExecutor(max_workers=16) as executor:
|
||||
futures = [executor.submit(test) for _ in range(32)]
|
||||
for future in as_completed(futures):
|
||||
future.result()
|
||||
del futures, future, executor
|
||||
|
||||
for _ in range(5):
|
||||
gc.collect()
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <pybind11/embed.h>
|
||||
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
|
||||
# include <pybind11/gil_safe_call_once.h>
|
||||
# include <pybind11/subinterpreter.h>
|
||||
|
||||
// Silence MSVC C++17 deprecation warning from Catch regarding std::uncaught_exceptions (up to
|
||||
@@ -30,7 +31,7 @@ void unsafe_reset_internals_for_single_interpreter() {
|
||||
py::detail::get_local_internals_pp_manager().unref();
|
||||
|
||||
// we know there are no other interpreters, so we can lower this. SUPER DANGEROUS
|
||||
py::detail::get_num_interpreters_seen() = 1;
|
||||
py::detail::has_seen_non_main_interpreter() = false;
|
||||
|
||||
// now we unref the static global singleton internals
|
||||
py::detail::get_internals_pp_manager().unref();
|
||||
@@ -41,6 +42,30 @@ void unsafe_reset_internals_for_single_interpreter() {
|
||||
py::detail::get_local_internals();
|
||||
}
|
||||
|
||||
py::object &get_dict_type_object() {
|
||||
PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<py::object> storage;
|
||||
return storage
|
||||
.call_once_and_store_result(
|
||||
[]() -> py::object { return py::module_::import("builtins").attr("dict"); })
|
||||
.get_stored();
|
||||
}
|
||||
|
||||
py::object &get_ordered_dict_type_object() {
|
||||
PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<py::object> storage;
|
||||
return storage
|
||||
.call_once_and_store_result(
|
||||
[]() -> py::object { return py::module_::import("collections").attr("OrderedDict"); })
|
||||
.get_stored();
|
||||
}
|
||||
|
||||
py::object &get_default_dict_type_object() {
|
||||
PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<py::object> storage;
|
||||
return storage
|
||||
.call_once_and_store_result(
|
||||
[]() -> py::object { return py::module_::import("collections").attr("defaultdict"); })
|
||||
.get_stored();
|
||||
}
|
||||
|
||||
TEST_CASE("Single Subinterpreter") {
|
||||
unsafe_reset_internals_for_single_interpreter();
|
||||
|
||||
@@ -308,6 +333,103 @@ TEST_CASE("Multiple Subinterpreters") {
|
||||
unsafe_reset_internals_for_single_interpreter();
|
||||
}
|
||||
|
||||
// Test that gil_safe_call_once_and_store provides per-interpreter storage.
|
||||
// Without the per-interpreter storage fix, the subinterpreter would see the value
|
||||
// cached by the main interpreter, which is invalid (different interpreter's object).
|
||||
TEST_CASE("gil_safe_call_once_and_store per-interpreter isolation") {
|
||||
unsafe_reset_internals_for_single_interpreter();
|
||||
|
||||
// This static simulates a typical usage pattern where a module caches
|
||||
// an imported object using gil_safe_call_once_and_store.
|
||||
PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<py::object> storage;
|
||||
|
||||
// Get the interpreter ID in the main interpreter
|
||||
auto main_interp_id = PyInterpreterState_GetID(PyInterpreterState_Get());
|
||||
|
||||
// Store a value in the main interpreter - we'll store the interpreter ID as a Python int
|
||||
auto &main_value = storage
|
||||
.call_once_and_store_result([]() {
|
||||
return py::int_(PyInterpreterState_GetID(PyInterpreterState_Get()));
|
||||
})
|
||||
.get_stored();
|
||||
REQUIRE(main_value.cast<int64_t>() == main_interp_id);
|
||||
|
||||
py::object dict_type = get_dict_type_object();
|
||||
py::object ordered_dict_type = get_ordered_dict_type_object();
|
||||
py::object default_dict_type = get_default_dict_type_object();
|
||||
|
||||
int64_t sub_interp_id = -1;
|
||||
int64_t sub_cached_value = -1;
|
||||
|
||||
bool sub_default_dict_type_destroyed = false;
|
||||
|
||||
// Create a subinterpreter and check that it gets its own storage
|
||||
{
|
||||
py::scoped_subinterpreter ssi;
|
||||
|
||||
sub_interp_id = PyInterpreterState_GetID(PyInterpreterState_Get());
|
||||
REQUIRE(sub_interp_id != main_interp_id);
|
||||
|
||||
// Access the same static storage from the subinterpreter.
|
||||
// With per-interpreter storage, this should call the lambda again
|
||||
// and cache a NEW value for this interpreter.
|
||||
// Without per-interpreter storage, this would return main's cached value.
|
||||
auto &sub_value
|
||||
= storage
|
||||
.call_once_and_store_result([]() {
|
||||
return py::int_(PyInterpreterState_GetID(PyInterpreterState_Get()));
|
||||
})
|
||||
.get_stored();
|
||||
|
||||
sub_cached_value = sub_value.cast<int64_t>();
|
||||
|
||||
// The cached value should be the SUBINTERPRETER's ID, not the main interpreter's.
|
||||
// This would fail without per-interpreter storage.
|
||||
REQUIRE(sub_cached_value == sub_interp_id);
|
||||
REQUIRE(sub_cached_value != main_interp_id);
|
||||
|
||||
py::object sub_dict_type = get_dict_type_object();
|
||||
py::object sub_ordered_dict_type = get_ordered_dict_type_object();
|
||||
py::object sub_default_dict_type = get_default_dict_type_object();
|
||||
|
||||
// Verify that the subinterpreter has its own cached type objects.
|
||||
// For static types, they should be the same object across interpreters.
|
||||
// See also: https://docs.python.org/3/c-api/typeobj.html#static-types
|
||||
REQUIRE(sub_dict_type.is(dict_type)); // dict is a static type
|
||||
REQUIRE(sub_ordered_dict_type.is(ordered_dict_type)); // OrderedDict is a static type
|
||||
// For heap types, they are dynamically created per-interpreter.
|
||||
// See also: https://docs.python.org/3/c-api/typeobj.html#heap-types
|
||||
REQUIRE_FALSE(sub_default_dict_type.is(default_dict_type)); // defaultdict is a heap type
|
||||
|
||||
// Set up a weakref callback to detect when the subinterpreter's cached default_dict_type
|
||||
// is destroyed so the gil_safe_call_once_and_store storage is not leaked when the
|
||||
// subinterpreter is shutdown.
|
||||
(void) py::weakref(sub_default_dict_type,
|
||||
py::cpp_function([&](py::handle weakref) -> void {
|
||||
sub_default_dict_type_destroyed = true;
|
||||
weakref.dec_ref();
|
||||
}))
|
||||
.release();
|
||||
}
|
||||
|
||||
// Back in main interpreter, verify main's value is unchanged
|
||||
auto &main_value_after = storage.get_stored();
|
||||
REQUIRE(main_value_after.cast<int64_t>() == main_interp_id);
|
||||
|
||||
// Verify that the types cached in main are unchanged
|
||||
py::object dict_type_after = get_dict_type_object();
|
||||
py::object ordered_dict_type_after = get_ordered_dict_type_object();
|
||||
py::object default_dict_type_after = get_default_dict_type_object();
|
||||
REQUIRE(dict_type_after.is(dict_type));
|
||||
REQUIRE(ordered_dict_type_after.is(ordered_dict_type));
|
||||
REQUIRE(default_dict_type_after.is(default_dict_type));
|
||||
|
||||
// Verify that the subinterpreter's cached default_dict_type was destroyed
|
||||
REQUIRE(sub_default_dict_type_destroyed);
|
||||
|
||||
unsafe_reset_internals_for_single_interpreter();
|
||||
}
|
||||
|
||||
# ifdef Py_MOD_PER_INTERPRETER_GIL_SUPPORTED
|
||||
TEST_CASE("Per-Subinterpreter GIL") {
|
||||
auto main_int
|
||||
|
||||
Reference in New Issue
Block a user