Skip to content

Commit 23c5b18

Browse files
committed
fix: run argument-to-argument keep_alive before the call again
Moving keep_alive<N, P> (N, P != 0) to postcall meant a keep_alive error was raised after the function ran, so its side effects stayed. Run the precall hook in call_impl after load_args succeeds instead, so a failed overload still does not trigger it and an error still stops the call. Also release the return value when a postcall keep_alive throws, as the dispatcher drops it. Assisted-by: ClaudeCode:claude-opus-5-5
1 parent 66811ce commit 23c5b18

4 files changed

Lines changed: 70 additions & 17 deletions

File tree

‎include/pybind11/attr.h‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ struct process_attribute_default {
409409
/// Default implementation: do nothing
410410
static void init(const T &, function_record *) {}
411411
static void init(const T &, type_record *) {}
412+
/// Runs after argument conversion succeeded, before the call.
412413
static void precall(function_call &) {}
413414
/// The handle is not always a valid object: it is the PYBIND11_TRY_NEXT_OVERLOAD sentinel
414415
/// if argument conversion failed, and null if the call or return-value conversion failed.
@@ -652,12 +653,21 @@ struct process_attribute<call_guard<Ts...>> : process_attribute_default<call_gua
652653

653654
/**
654655
* Process a keep_alive call policy -- invokes keep_alive_impl during the
655-
* post-call handler so that it only runs after a successful call, never for an
656-
* overload that failed argument conversion or a call that raised.
656+
* pre-call handler (after argument conversion succeeded) if both Nurse,
657+
* Patient != 0 and use the post-call handler otherwise
657658
*/
658659
template <size_t Nurse, size_t Patient>
659660
struct process_attribute<keep_alive<Nurse, Patient>>
660661
: public process_attribute_default<keep_alive<Nurse, Patient>> {
662+
template <size_t N = Nurse, size_t P = Patient, enable_if_t<N != 0 && P != 0, int> = 0>
663+
static void precall(function_call &call) {
664+
keep_alive_impl(Nurse, Patient, call, handle());
665+
}
666+
template <size_t N = Nurse, size_t P = Patient, enable_if_t<N != 0 && P != 0, int> = 0>
667+
static void postcall(function_call &, handle) {}
668+
template <size_t N = Nurse, size_t P = Patient, enable_if_t<N == 0 || P == 0, int> = 0>
669+
static void precall(function_call &) {}
670+
template <size_t N = Nurse, size_t P = Patient, enable_if_t<N == 0 || P == 0, int> = 0>
661671
static void postcall(function_call &call, handle ret) {
662672
keep_alive_impl(Nurse, Patient, call, ret);
663673
}

‎include/pybind11/pybind11.h‎

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,9 @@ class cpp_function : public function {
489489
// actual function lambda so that we can get code reuse for
490490
// functions with the same Return, Args, and Guard.
491491
template <typename Return, typename Guard, typename ArgsConverter, typename... Args>
492-
static handle call_impl(detail::function_call &call, detail::function_ref<Return(Args...)> f) {
492+
static handle call_impl(detail::function_call &call,
493+
detail::function_ref<Return(Args...)> f,
494+
void (*precall)(detail::function_call &)) {
493495
using namespace detail;
494496
// Static assertion: function_ref must be trivially copyable to ensure safe pass-by-value.
495497
// Lifetime safety: The function_ref is created from cap->f which lives in the capture
@@ -505,6 +507,9 @@ class cpp_function : public function {
505507
return PYBIND11_TRY_NEXT_OVERLOAD;
506508
}
507509

510+
/* Invoke call policy pre-call hook, only for the overload that matched */
511+
precall(call);
512+
508513
/* Override policy for rvalues -- usually to enforce rvp::move on an rvalue */
509514
return_value_policy policy
510515
= return_value_policy_override<Return>::policy(call.func.policy);
@@ -585,9 +590,6 @@ class cpp_function : public function {
585590

586591
/* Dispatch code which converts function arguments and performs the actual function call */
587592
rec->impl = [](function_call &call) -> handle {
588-
/* Invoke call policy pre-call hook */
589-
process_attributes<Extra...>::precall(call);
590-
591593
/* Get a pointer to the capture object */
592594
const auto *data = (sizeof(capture) <= sizeof(call.func.data) ? &call.func.data
593595
: call.func.data[0]);
@@ -597,7 +599,9 @@ class cpp_function : public function {
597599
/* Function scope guard -- defaults to the compile-to-nothing
598600
`void_type` */
599601
extract_guard_t<Extra...>,
600-
cast_in>(call, detail::function_ref<Return(Args...)>(cap->f));
602+
cast_in>(call,
603+
detail::function_ref<Return(Args...)>(cap->f),
604+
&process_attributes<Extra...>::precall);
601605

602606
/* Invoke call policy post-call hook */
603607
process_attributes<Extra...>::postcall(call, result);
@@ -3387,14 +3391,14 @@ PYBIND11_NOINLINE void keep_alive_impl(handle nurse, handle patient) {
33873391

33883392
PYBIND11_NOINLINE void
33893393
keep_alive_impl(size_t Nurse, size_t Patient, function_call &call, handle ret) {
3390-
// The dispatcher runs postcall even when the overload produced no value: `ret` is the
3391-
// PYBIND11_TRY_NEXT_OVERLOAD sentinel ((PyObject *) 1) if the overload bailed out of
3392-
// `load_args`, and null if the return-value conversion failed (with the real error
3393-
// already set). In both cases there was no successful call and there is no relationship
3394-
// to establish. Checked here because the sentinel is neither null nor `Py_None`, so it
3395-
// passes straight through the guards below and is dereferenced, and because a null `ret`
3396-
// would be reported as "Could not activate keep_alive!", masking the real error.
3397-
if (!ret || ret.ptr() == PYBIND11_TRY_NEXT_OVERLOAD) {
3394+
// With index 0, this runs in postcall, which the dispatcher runs even when the overload
3395+
// produced no value: `ret` is the PYBIND11_TRY_NEXT_OVERLOAD sentinel ((PyObject *) 1) if
3396+
// the overload bailed out of `load_args`, and null if the return-value conversion failed
3397+
// (with the real error already set). There is no relationship to establish then. The
3398+
// sentinel would pass the guards below and be dereferenced, and a null `ret` would be
3399+
// reported as "Could not activate keep_alive!", masking the real error.
3400+
const bool uses_ret = Nurse == 0 || Patient == 0;
3401+
if (uses_ret && (!ret || ret.ptr() == PYBIND11_TRY_NEXT_OVERLOAD)) {
33983402
return;
33993403
}
34003404

@@ -3411,7 +3415,15 @@ keep_alive_impl(size_t Nurse, size_t Patient, function_call &call, handle ret) {
34113415
return handle();
34123416
};
34133417

3414-
keep_alive_impl(get_arg(Nurse), get_arg(Patient));
3418+
try {
3419+
keep_alive_impl(get_arg(Nurse), get_arg(Patient));
3420+
} catch (...) {
3421+
// The dispatcher drops `ret` when postcall throws, so release it here.
3422+
if (uses_ret) {
3423+
ret.dec_ref();
3424+
}
3425+
throw;
3426+
}
34153427
}
34163428

34173429
inline std::pair<decltype(internals::registered_types_py)::iterator, bool>

‎tests/test_call_policies.cpp‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ TEST_SUBMODULE(call_policies, m) {
3535
// Parent/Child are used in:
3636
// test_keep_alive_argument, test_keep_alive_return_value, test_alive_gc_derived,
3737
// test_alive_gc_multi_derived, test_return_none, test_keep_alive_constructor,
38-
// test_keep_alive_failed_overload
38+
// test_keep_alive_failed_overload, test_keep_alive_error
3939
class Child {
4040
public:
4141
Child() { py::print("Allocating child."); }
@@ -67,6 +67,19 @@ TEST_SUBMODULE(call_policies, m) {
6767
.def_static("staticFunction", &Parent::staticFunction, py::keep_alive<1, 0>());
6868

6969
m.def("free_function", [](Parent *, Child *) {}, py::keep_alive<1, 2>());
70+
71+
// test_keep_alive_error
72+
static int keep_alive_error_calls = 0;
73+
m.def(
74+
"keep_alive_error_args",
75+
[](const py::object &, Child *) { ++keep_alive_error_calls; },
76+
py::keep_alive<1, 2>());
77+
m.def("keep_alive_error_calls", [] { return keep_alive_error_calls; });
78+
m.def(
79+
"keep_alive_error_return",
80+
[](const py::object &) { return new Child(); },
81+
py::keep_alive<1, 0>());
82+
7083
m.def("invalid_arg_index", [] {}, py::keep_alive<0, 1>());
7184

7285
#if !defined(PYPY_VERSION)

‎tests/test_call_policies.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,21 @@ def test_keep_alive_failed_return_conversion():
288288
"""A failed return-value conversion must raise its own error, not a keep_alive one."""
289289
with pytest.raises(TypeError, match="Unable to convert function return value"):
290290
m.keep_alive_unregistered_return(m.KeepAliveOverload())
291+
292+
293+
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
294+
def test_keep_alive_error(capture):
295+
"""A keep_alive error must not leave side effects or leak the return value."""
296+
n_inst = ConstructorStats.detail_reg_inst()
297+
c = m.Child()
298+
# An int nurse cannot hold a weak reference, so keep_alive<1, 2> fails.
299+
with pytest.raises(TypeError, match="weak reference"):
300+
m.keep_alive_error_args(1, c)
301+
assert m.keep_alive_error_calls() == 0
302+
del c
303+
with capture:
304+
with pytest.raises(TypeError, match="weak reference"):
305+
m.keep_alive_error_return(1)
306+
pytest.gc_collect()
307+
assert capture == "Allocating child.\nReleasing child."
308+
assert ConstructorStats.detail_reg_inst() == n_inst

0 commit comments

Comments
 (0)