Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -1910,6 +1910,13 @@ enable_if_t<detail::none_of<cast_is_temporary_value_reference<T>,
std::is_void<T>>::value,
T>
cast_safe(object &&o) {
// A registered type returned by value is copied or moved before this frame ends.
// Preserve an enclosing frame, and never shorten the lifetime of borrowed results.
if (std::is_base_of<type_caster_generic, make_caster<T>>::value && !std::is_reference<T>::value

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codex GPT-6.1-Sol xhigh narration for this change:

The change supplies temporary-object lifetime support when native C++ calls a Python override that returns a value requiring implicit conversion—for example, a Python list converted to an opaque C++ vector.

Normally, a Python-to-C++ bound call already has a loader_life_support frame. Implicit conversion registers its temporary Python wrapper with that frame, keeping the underlying C++ object alive during conversion. A call originating in native C++ can lack that frame, so registration throws. Existing lifetime-support code

The first two production edits add a query for that situation: loader_life_support::is_active() is declared in the header and defined in the implementation header. It simply checks whether the existing thread-local current-frame pointer is non-null. It adds no state.

The behavioral change is in detail::cast_safe<T>(), which handles trampoline return conversion. Previously it immediately called pybind11::cast<T>(). Now it first checks three conditions:

  • The caster derives from type_caster_generic, covering registered C++ types and their holder casters.
  • T is returned by value, rather than by reference or pointer.
  • No lifetime-support frame is already active.

When all three hold, it creates a local frame and performs the same cast:

loader_life_support life_support;
return pybind11::cast<T>(std::move(o));

The order matters: C++ constructs the returned T before destroying life_support. Thus the implicit-conversion temporary survives until its value has been copied or moved into the result. The frame then releases its temporary objects; stack unwinding also cleans it up if conversion throws.

If an enclosing frame already exists, execution takes the original cast path, so conversion temporaries remain attached to that longer-lived frame. Reference and pointer returns also keep their previous behavior: a frame ending inside cast_safe() would expire too early to support a borrowed result. The caster restriction similarly leaves ordinary string-view conversions on their existing path.

&& !std::is_pointer<T>::value && !loader_life_support::is_active()) {
loader_life_support life_support;
return pybind11::cast<T>(std::move(o));
}
return pybind11::cast<T>(std::move(o));
}

Expand Down
2 changes: 2 additions & 0 deletions include/pybind11/detail/type_caster_base-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ PYBIND11_INLINE loader_life_support::~loader_life_support() {
}
}

PYBIND11_INLINE bool loader_life_support::is_active() { return tls_current_frame() != nullptr; }

PYBIND11_NOINLINE_ATTR PYBIND11_INLINE bool loader_life_support::try_add_patient(handle h) {
loader_life_support *frame = tls_current_frame();
if (!frame) {
Expand Down
3 changes: 3 additions & 0 deletions include/pybind11/detail/type_caster_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class loader_life_support {
/// ... and destroyed after it returns
~loader_life_support();

/// Whether a caller already provides a frame for temporary objects.
static bool is_active();

/// Keep `h` alive until the current patient frame is destroyed, if there is one.
/// Returns false when called outside a bound function (no frame). Use this, rather
/// than `add_patient`, when failing to register is acceptable because the caller
Expand Down
117 changes: 117 additions & 0 deletions tests/test_with_catch/test_interpreter.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <pybind11/critical_section.h>
#include <pybind11/embed.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl_bind.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).
Expand All @@ -18,6 +19,122 @@ PYBIND11_WARNING_DISABLE_MSVC(4996)
namespace py = pybind11;
using namespace py::literals;

struct ImplicitReturnItem {
explicit ImplicitReturnItem(int value) : value(value) {}
int value;
};
struct ImplicitReturnValues : std::vector<std::shared_ptr<ImplicitReturnItem>> {
using std::vector<std::shared_ptr<ImplicitReturnItem>>::vector;
static int instances;
struct Counter {
Counter() { ++instances; }
Counter(const Counter &) { ++instances; }
~Counter() { --instances; }
} counter;
};
int ImplicitReturnValues::instances = 0;
PYBIND11_MAKE_OPAQUE(ImplicitReturnValues)

struct ImplicitReturnProvider {
virtual ~ImplicitReturnProvider() = default;
virtual ImplicitReturnValues values() = 0;
virtual const ImplicitReturnValues &reference() = 0;
virtual ImplicitReturnValues *pointer() = 0;
#ifdef PYBIND11_HAS_STRING_VIEW
virtual std::u16string_view view() = 0;
#endif
};

struct PyImplicitReturnProvider : ImplicitReturnProvider {
ImplicitReturnValues values() override {
PYBIND11_OVERRIDE_PURE(ImplicitReturnValues, ImplicitReturnProvider, values, );
}
const ImplicitReturnValues &reference() override {
PYBIND11_OVERRIDE_PURE(const ImplicitReturnValues &, ImplicitReturnProvider, reference, );
}
ImplicitReturnValues *pointer() override {
PYBIND11_OVERRIDE_PURE(ImplicitReturnValues *, ImplicitReturnProvider, pointer, );
}
#ifdef PYBIND11_HAS_STRING_VIEW
std::u16string_view view() override {
PYBIND11_OVERRIDE_PURE(std::u16string_view, ImplicitReturnProvider, view, );
}
#endif
};

PYBIND11_EMBEDDED_MODULE(implicit_return_module, m, py::multiple_interpreters::not_supported()) {
py::class_<ImplicitReturnItem, std::shared_ptr<ImplicitReturnItem>>(m, "Item").def(
py::init<int>());
py::bind_vector<ImplicitReturnValues>(m, "Values");
py::implicitly_convertible<py::list, ImplicitReturnValues>();
py::class_<ImplicitReturnProvider, PyImplicitReturnProvider>(m, "Provider").def(py::init<>());
}

TEST_CASE("Implicit conversions in native trampoline returns") {
py::dict locals;
py::exec(R"(
from implicit_return_module import Item, Provider
class PythonProvider(Provider):
def __init__(self):
super().__init__()
self.result = [Item(17), Item(-4), Item(99)]
self.fresh = False
def values(self):
return list(self.result) if self.fresh else self.result
def reference(self):
return self.result
def pointer(self):
return self.result
def view(self):
return "hello"
provider = PythonProvider()
)",
locals);
py::object provider = locals["provider"];
auto *cpp_provider = provider.cast<ImplicitReturnProvider *>();

SECTION("Copy from a retained Python return value") { provider.attr("fresh") = false; }
SECTION("Move from a temporary Python return value") { provider.attr("fresh") = true; }

REQUIRE(ImplicitReturnValues::instances == 0);
// An enclosing bound call already owns its conversion temporaries. They must
// not be destroyed earlier by a nested return-conversion frame.
auto bound_call = py::cpp_function([&]() {
auto result = cpp_provider->values();
return ImplicitReturnValues::instances;
});
REQUIRE(bound_call().cast<int>() == 2);
REQUIRE(ImplicitReturnValues::instances == 0);

// A short-lived return-conversion frame cannot support a borrowed result.
REQUIRE_THROWS_AS(cpp_provider->reference(), py::cast_error);
REQUIRE_THROWS_AS(cpp_provider->pointer(), py::cast_error);
#ifdef PYBIND11_HAS_STRING_VIEW
REQUIRE_THROWS_AS(cpp_provider->view(), py::cast_error);
#endif

// Calling from native C++ is essential: a bound function would already provide
// a loader frame and hide the missing life support during return conversion.
for (int i = 0; i < 3; ++i) {
auto result = cpp_provider->values();
REQUIRE(ImplicitReturnValues::instances == 1);
REQUIRE(result.size() == 3);
REQUIRE(result[0]->value == 17);
REQUIRE(result[1]->value == -4);
REQUIRE(result[2]->value == 99);
}
REQUIRE_THROWS_AS(cpp_provider->reference(), py::cast_error);

auto retained = cpp_provider->values();
provider.attr("result").attr("clear")();
REQUIRE(retained[0]->value == 17);
REQUIRE(retained[1]->value == -4);
REQUIRE(retained[2]->value == 99);

provider.attr("result") = py::make_tuple("invalid");
REQUIRE_THROWS_AS(cpp_provider->values(), py::cast_error);
}

size_t get_sys_path_size() {
auto sys_path = py::module::import("sys").attr("path");
return py::len(sys_path);
Expand Down
Loading