Files
pybind11/tests/mod_per_interpreter_gil_with_singleton.cpp
Xuehai Pan fee2527dfa Fix concurrency consistency for internals_pp_manager under multiple-interpreters (#5947)
* Add per-interpreter storage for `gil_safe_call_once_and_store`

* Disable thread local cache for `internals_pp_manager`

* Disable thread local cache for `internals_pp_manager` for multi-interpreter only

* Use anonymous namespace to separate these type_ids from other tests with the same class names.

* style: pre-commit fixes

* Revert internals_pp_manager changes

* This is the crux of fix for the subinterpreter_before_main failure.

The pre_init needs to check if it is in a subinterpreter or not. But in 3.13+ this static initializer runs in the main interpreter.  So we need to check this later, during the exec phase.

* Continue to do the ensure in both places, there might be a reason it was where it was...

Should not hurt anything to do it extra times here.

* Change get_num_interpreters_seen to a boolean flag instead.

The count was not used, it was just checked for > 1, we now accomplish this by setting the flag.

* Spelling typo

* Work around older python versions, only need this check for newish versions

* Add more comments for test case

* Add more comments for test case

* Stop traceback propagation

* Re-enable subinterpreter support on ubuntu 3.14 builds

Was disabled in e4873e8

* As suggested, don't use an anonymous namespace.

* Typo in test assert format string

* Use a more appropriate function name

* Fix mod_per_interpreter_gil* output directory on Windows/MSVC

On Windows with MSVC (multi-configuration generators), CMake uses
config-specific properties like LIBRARY_OUTPUT_DIRECTORY_DEBUG when
set, otherwise falls back to LIBRARY_OUTPUT_DIRECTORY/<Config>/.

The main test modules (pybind11_tests, etc.) correctly set both
LIBRARY_OUTPUT_DIRECTORY and the config-specific variants (lines
517-528), so they output directly to tests/.

However, the mod_per_interpreter_gil* modules only copied the base
LIBRARY_OUTPUT_DIRECTORY property, causing them to be placed in
tests/Debug/ instead of tests/.

This mismatch caused test_import_in_subinterpreter_concurrently and
related tests to fail with ModuleNotFoundError on Windows Python 3.14,
because the test code sets sys.path based on pybind11_tests.__file__
(which is in tests/) but tries to import mod_per_interpreter_gil_with_singleton
(which ended up in tests/Debug/).

This bug was previously masked by @pytest.mark.xfail decorators on
these tests. Now that the underlying "Duplicate C++ type registration"
issue is fixed and the xfails are removed, this path issue surfaced.

The fix mirrors the same pattern used for main test targets: also set
LIBRARY_OUTPUT_DIRECTORY_<CONFIG> for each configuration type.

* Remove unneeded `pytest.importorskip`

* Remove comment

---------

Co-authored-by: b-pass <b-pass@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
2025-12-26 13:59:11 -05:00

148 lines
5.1 KiB
C++

#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
}