Files
pybind11/tests/test_embed/test_subinterpreter.cpp
b-pass 07d028f218 feat: Embeded sub-interpreters (#5666)
* First draft a subinterpreter embedding API

* Move subinterpreter tests to their own file

* Migrate subinterpreter tests to use the new embedded class.

* Add a test for moving subinterpreters across threads for destruction

And find a better way to make that work.

* Code organization

* Add a test which shows demostrates how gil_scoped interacts with sub-interpreters

* Add documentation for embeded sub-interpreters

* Some additional docs work

* Add some convenience accessors

* Add some docs cross references

* Sync some things that were split out into #5665

* Update subinterpreter docs example to not use the CPython api

* Fix pip test

* style: pre-commit fixes

* Fix MSVC warnings

I am surprised other compilers allowed this code with a deleted move ctor.

* Add some sub-headings to the docs

* Oops, make_unique is C++14 so remove it from the tests.

* I think this fixes the EndInterpreter issues on all versions.

It just has to be ifdef'd because it is slightly broken on 3.12, working well on 3.13, and kind of crashy on 3.14beta.  These two verion ifdefs solve all the issues.

* Add a note about exceptions.

They contain Python object references and acquire the GIL, that means they are a danger with subinterpreters!

* style: pre-commit fixes

* Add try/catch to docs examples to match the tips

* Python 3.12 is very picky about this first PyThreadState

Try special casing the destruction on the same thread.

* style: pre-commit fixes

* Missed a rename in a ifdef block

* I think this test is causing problems in 3.12, so try ifdefing it to see if the problems go away.

* style: pre-commit fixes

* Document the 3.12 constraints with a warning

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* ci: add cpptest to the clang-tidy job

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* noexcept move operations

* Update include/pybind11/subinterpreter.h

std::memset

Co-authored-by: Aaron Gokaslan <aaronGokaslan@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Aaron Gokaslan <aaronGokaslan@gmail.com>
2025-05-20 18:17:48 -04:00

432 lines
15 KiB
C++

#include <pybind11/embed.h>
#ifdef PYBIND11_SUBINTERPRETER_SUPPORT
# include <pybind11/subinterpreter.h>
// Silence MSVC C++17 deprecation warning from Catch regarding std::uncaught_exceptions (up to
// catch 2.0.1; this should be fixed in the next catch release after 2.0.1).
PYBIND11_WARNING_DISABLE_MSVC(4996)
# include <catch.hpp>
# include <cstdlib>
# include <fstream>
# include <functional>
# include <thread>
# include <utility>
namespace py = pybind11;
using namespace py::literals;
bool has_state_dict_internals_obj();
bool has_pybind11_internals_static();
uintptr_t get_details_as_uintptr();
void unsafe_reset_internals_for_single_interpreter() {
// unsafe normally, but for subsequent tests, put this back.. we know there are no threads
// running and only 1 interpreter
py::detail::get_num_interpreters_seen() = 1;
py::detail::get_internals_pp<py::detail::internals>() = nullptr;
py::detail::get_internals();
py::detail::get_internals_pp<py::detail::local_internals>() = nullptr;
py::detail::get_local_internals();
}
TEST_CASE("Single Subinterpreter") {
py::module_::import("external_module"); // in the main interpreter
// Add tags to the modules in the main interpreter and test the basics.
py::module_::import("__main__").attr("main_tag") = "main interpreter";
{
auto m = py::module_::import("widget_module");
m.attr("extension_module_tag") = "added to module in main interpreter";
REQUIRE(m.attr("add")(1, 2).cast<int>() == 3);
}
REQUIRE(has_state_dict_internals_obj());
REQUIRE(has_pybind11_internals_static());
auto main_int
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
/// Create and switch to a subinterpreter.
{
py::scoped_subinterpreter ssi;
// The subinterpreter has internals populated
REQUIRE(has_pybind11_internals_static());
py::list(py::module_::import("sys").attr("path")).append(py::str("."));
auto ext_int
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
py::detail::get_internals();
REQUIRE(has_pybind11_internals_static());
REQUIRE(get_details_as_uintptr() == ext_int);
REQUIRE(ext_int != main_int);
// Modules tags should be gone.
REQUIRE_FALSE(py::hasattr(py::module_::import("__main__"), "tag"));
{
auto m = py::module_::import("widget_module");
REQUIRE_FALSE(py::hasattr(m, "extension_module_tag"));
// Function bindings should still work.
REQUIRE(m.attr("add")(1, 2).cast<int>() == 3);
}
}
REQUIRE(py::hasattr(py::module_::import("__main__"), "main_tag"));
REQUIRE(py::hasattr(py::module_::import("widget_module"), "extension_module_tag"));
REQUIRE(has_state_dict_internals_obj());
unsafe_reset_internals_for_single_interpreter();
}
# if PY_VERSION_HEX >= 0x030D0000
TEST_CASE("Move Subinterpreter") {
std::unique_ptr<py::subinterpreter> sub(new py::subinterpreter(py::subinterpreter::create()));
// on this thread, use the subinterpreter and import some non-trivial junk
{
py::subinterpreter_scoped_activate activate(*sub);
py::list(py::module_::import("sys").attr("path")).append(py::str("."));
py::module_::import("datetime");
py::module_::import("threading");
py::module_::import("external_module");
}
std::thread([&]() {
// Use it again
{
py::subinterpreter_scoped_activate activate(*sub);
py::module_::import("external_module");
}
sub.reset();
}).join();
REQUIRE(!sub);
unsafe_reset_internals_for_single_interpreter();
}
# endif
TEST_CASE("GIL Subinterpreter") {
PyInterpreterState *main_interp = PyInterpreterState_Get();
{
auto sub = py::subinterpreter::create();
REQUIRE(main_interp == PyInterpreterState_Get());
PyInterpreterState *sub_interp = nullptr;
{
py::subinterpreter_scoped_activate activate(sub);
sub_interp = PyInterpreterState_Get();
REQUIRE(sub_interp != main_interp);
py::list(py::module_::import("sys").attr("path")).append(py::str("."));
py::module_::import("datetime");
py::module_::import("threading");
py::module_::import("external_module");
{
py::subinterpreter_scoped_activate main(py::subinterpreter::main());
REQUIRE(PyInterpreterState_Get() == main_interp);
{
py::gil_scoped_release nogil{};
{
py::gil_scoped_acquire yesgil{};
REQUIRE(PyInterpreterState_Get() == main_interp);
}
}
REQUIRE(PyInterpreterState_Get() == main_interp);
}
REQUIRE(PyInterpreterState_Get() == sub_interp);
{
py::gil_scoped_release nogil{};
{
py::gil_scoped_acquire yesgil{};
REQUIRE(PyInterpreterState_Get() == sub_interp);
}
}
REQUIRE(PyInterpreterState_Get() == sub_interp);
}
REQUIRE(PyInterpreterState_Get() == main_interp);
{
py::gil_scoped_release nogil{};
{
py::gil_scoped_acquire yesgil{};
REQUIRE(PyInterpreterState_Get() == main_interp);
}
}
REQUIRE(PyInterpreterState_Get() == main_interp);
bool thread_result;
{
thread_result = false;
py::gil_scoped_release nogil{};
std::thread([&]() {
{
py::subinterpreter_scoped_activate ssa{sub};
}
{
py::gil_scoped_acquire gil{};
thread_result = (PyInterpreterState_Get() == main_interp);
}
}).join();
}
REQUIRE(thread_result);
{
thread_result = false;
py::gil_scoped_release nogil{};
std::thread([&]() {
py::gil_scoped_acquire gil{};
thread_result = (PyInterpreterState_Get() == main_interp);
}).join();
}
REQUIRE(thread_result);
}
REQUIRE(PyInterpreterState_Get() == main_interp);
unsafe_reset_internals_for_single_interpreter();
}
TEST_CASE("Multiple Subinterpreters") {
// Make sure the module is in the main interpreter and save its pointer
auto *main_ext = py::module_::import("external_module").ptr();
auto main_int
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
py::module_::import("external_module").attr("multi_interp") = "1";
{
py::subinterpreter si1 = py::subinterpreter::create();
std::unique_ptr<py::subinterpreter> psi2;
PyObject *sub1_ext = nullptr;
PyObject *sub2_ext = nullptr;
uintptr_t sub1_int = 0;
uintptr_t sub2_int = 0;
{
py::subinterpreter_scoped_activate scoped(si1);
py::list(py::module_::import("sys").attr("path")).append(py::str("."));
// The subinterpreter has its own copy of this module which is completely separate from
// main
sub1_ext = py::module_::import("external_module").ptr();
REQUIRE(sub1_ext != main_ext);
REQUIRE_FALSE(py::hasattr(py::module_::import("external_module"), "multi_interp"));
py::module_::import("external_module").attr("multi_interp") = "2";
// The subinterpreter also has its own internals
sub1_int
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
REQUIRE(sub1_int != main_int);
// while the old one is active, create a new one
psi2.reset(new py::subinterpreter(py::subinterpreter::create()));
}
{
py::subinterpreter_scoped_activate scoped(*psi2);
py::list(py::module_::import("sys").attr("path")).append(py::str("."));
// The second subinterpreter is separate from both main and the other subinterpreter
sub2_ext = py::module_::import("external_module").ptr();
REQUIRE(sub2_ext != main_ext);
REQUIRE(sub2_ext != sub1_ext);
REQUIRE_FALSE(py::hasattr(py::module_::import("external_module"), "multi_interp"));
py::module_::import("external_module").attr("multi_interp") = "3";
// The subinterpreter also has its own internals
sub2_int
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
REQUIRE(sub2_int != main_int);
REQUIRE(sub2_int != sub1_int);
}
{
py::subinterpreter_scoped_activate scoped(si1);
REQUIRE(
py::cast<std::string>(py::module_::import("external_module").attr("multi_interp"))
== "2");
}
// out here we should be in the main interpreter, with the GIL, with the other 2 still
// alive
auto post_int
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
// Make sure internals went back the way it was before
REQUIRE(main_int == post_int);
REQUIRE(py::cast<std::string>(py::module_::import("external_module").attr("multi_interp"))
== "1");
}
// now back to just main
auto post_int
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
// Make sure internals went back the way it was before
REQUIRE(main_int == post_int);
REQUIRE(py::cast<std::string>(py::module_::import("external_module").attr("multi_interp"))
== "1");
unsafe_reset_internals_for_single_interpreter();
}
# ifdef Py_MOD_PER_INTERPRETER_GIL_SUPPORTED
TEST_CASE("Per-Subinterpreter GIL") {
auto main_int
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
std::atomic<int> started, sync, failure;
started = 0;
sync = 0;
failure = 0;
// REQUIRE throws on failure, so we can't use it within the thread
# define T_REQUIRE(status) \
do { \
assert(status); \
if (!(status)) \
++failure; \
} while (0)
auto &&thread_main = [&](int num) {
while (started == 0)
std::this_thread::sleep_for(std::chrono::microseconds(1));
++started;
py::gil_scoped_acquire gil;
// we have the GIL, we can access the main interpreter
auto t_int
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
T_REQUIRE(t_int == main_int);
py::module_::import("external_module").attr("multi_interp") = "1";
auto sub = py::subinterpreter::create();
{
py::subinterpreter_scoped_activate sguard{sub};
py::list(py::module_::import("sys").attr("path")).append(py::str("."));
// we have switched to the new interpreter and released the main gil
// trampoline_module did not provide the per_interpreter_gil tag, so it cannot be
// imported
bool caught = false;
try {
py::module_::import("trampoline_module");
} catch (pybind11::error_already_set &pe) {
T_REQUIRE(pe.matches(PyExc_ImportError));
std::string msg(pe.what());
T_REQUIRE(msg.find("does not support loading in subinterpreters")
!= std::string::npos);
caught = true;
}
T_REQUIRE(caught);
// widget_module did provide the per_interpreter_gil tag, so it this does not throw
try {
py::module_::import("widget_module");
caught = false;
} catch (pybind11::error_already_set &) {
caught = true;
}
T_REQUIRE(!caught);
// widget_module did provide the per_interpreter_gil tag, so it this does not throw
py::module_::import("widget_module");
T_REQUIRE(!py::hasattr(py::module_::import("external_module"), "multi_interp"));
py::module_::import("external_module").attr("multi_interp") = std::to_string(num);
// wait for something to set sync to our thread number
// we are holding our subinterpreter's GIL
while (sync != num)
std::this_thread::sleep_for(std::chrono::microseconds(1));
// now change it so the next thread can move on
++sync;
// but keep holding the GIL until after the next thread moves on as well
while (sync == num + 1)
std::this_thread::sleep_for(std::chrono::microseconds(1));
// one last check before quitting the thread, the internals should be different
auto sub_int
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
T_REQUIRE(sub_int != main_int);
}
};
# undef T_REQUIRE
std::thread t1(thread_main, 1);
std::thread t2(thread_main, 2);
// we spawned two threads, at this point they are both waiting for started to increase
++started;
// ok now wait for the threads to start
while (started != 3)
std::this_thread::sleep_for(std::chrono::microseconds(1));
// we still hold the main GIL, at this point both threads are waiting on the main GIL
// IN THE CASE of free threading, the threads are waiting on sync (because there is no GIL)
// IF the below code hangs in one of the wait loops, then the child thread GIL behavior did not
// function as expected.
{
// release the GIL and allow the threads to run
py::gil_scoped_release nogil;
// the threads are now waiting on the sync
REQUIRE(sync == 0);
// this will trigger thread 1 and then advance and trigger 2 and then advance
sync = 1;
// wait for thread 2 to advance
while (sync != 3)
std::this_thread::sleep_for(std::chrono::microseconds(1));
// we know now that thread 1 has run and may be finishing
// and thread 2 is waiting for permission to advance
// so we move sync so that thread 2 can finish executing
++sync;
// now wait for both threads to complete
t1.join();
t2.join();
}
// now we have the gil again, sanity check
REQUIRE(py::cast<std::string>(py::module_::import("external_module").attr("multi_interp"))
== "1");
unsafe_reset_internals_for_single_interpreter();
// make sure nothing unexpected happened inside the threads, now that they are completed
REQUIRE(failure == 0);
}
# endif // Py_MOD_PER_INTERPRETER_GIL_SUPPORTED
#endif // PYBIND11_SUBINTERPRETER_SUPPORT