Skip to content

Commit d6474ed

Browse files
yuantailingrwgk
andauthored
fix: memory leak in cpp_function (#3228) (#3229)
* fix: memory leak in cpp_function (#3228) * add a test case to check objects are deconstructed in cpp_function * update the test case about cpp_function * fix the test case about cpp_function: remove "noexcept" * Actually calling func. CHECK(stat.alive() == 2); Manually verified that the new tests fails without the change in pybind11.h * Moving new test to test_callbacks.cpp,py, with small enhancements. * Removing new test from test_interpreter.cpp (after it was moved to test_callbacks.cpp,py). This restores test_interpreter.cpp to the current state on master. * Using py::detail::silence_unused_warnings(py_func); to make the intent clear. Co-authored-by: Ralf W. Grosse-Kunstleve <[email protected]>
1 parent 76d939d commit d6474ed

File tree

3 files changed

+48
-4
lines changed

3 files changed

+48
-4
lines changed

include/pybind11/pybind11.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ class cpp_function : public function {
175175
#endif
176176
// UB without std::launder, but without breaking ABI and/or
177177
// a significant refactoring it's "impossible" to solve.
178-
if (!std::is_trivially_destructible<Func>::value)
178+
if (!std::is_trivially_destructible<capture>::value)
179179
rec->free_data = [](function_record *r) {
180180
auto data = PYBIND11_STD_LAUNDER((capture *) &r->data);
181181
(void) data;

tests/test_callbacks.cpp

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,55 @@ TEST_SUBMODULE(callbacks, m) {
8181
};
8282
// Export the payload constructor statistics for testing purposes:
8383
m.def("payload_cstats", &ConstructorStats::get<Payload>);
84-
/* Test cleanup of lambda closure */
85-
m.def("test_cleanup", []() -> std::function<void()> {
84+
m.def("test_lambda_closure_cleanup", []() -> std::function<void()> {
8685
Payload p;
8786

87+
// In this situation, `Func` in the implementation of
88+
// `cpp_function::initialize` is NOT trivially destructible.
8889
return [p]() {
8990
/* p should be cleaned up when the returned function is garbage collected */
9091
(void) p;
9192
};
9293
});
9394

95+
class CppCallable {
96+
public:
97+
CppCallable() { track_default_created(this); }
98+
~CppCallable() { track_destroyed(this); }
99+
CppCallable(const CppCallable &) { track_copy_created(this); }
100+
CppCallable(CppCallable &&) noexcept { track_move_created(this); }
101+
void operator()() {}
102+
};
103+
104+
m.def("test_cpp_callable_cleanup", []() {
105+
// Related issue: https://github.com/pybind/pybind11/issues/3228
106+
// Related PR: https://github.com/pybind/pybind11/pull/3229
107+
py::list alive_counts;
108+
ConstructorStats &stat = ConstructorStats::get<CppCallable>();
109+
alive_counts.append(stat.alive());
110+
{
111+
CppCallable cpp_callable;
112+
alive_counts.append(stat.alive());
113+
{
114+
// In this situation, `Func` in the implementation of
115+
// `cpp_function::initialize` IS trivially destructible,
116+
// only `capture` is not.
117+
py::cpp_function py_func(cpp_callable);
118+
py::detail::silence_unused_warnings(py_func);
119+
alive_counts.append(stat.alive());
120+
}
121+
alive_counts.append(stat.alive());
122+
{
123+
py::cpp_function py_func(std::move(cpp_callable));
124+
py::detail::silence_unused_warnings(py_func);
125+
alive_counts.append(stat.alive());
126+
}
127+
alive_counts.append(stat.alive());
128+
}
129+
alive_counts.append(stat.alive());
130+
return alive_counts;
131+
});
132+
94133
// test_cpp_function_roundtrip
95134
/* Test if passing a function pointer from C++ -> Python -> C++ yields the original pointer */
96135
m.def("dummy_function", &dummy_function);

tests/test_callbacks.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,18 @@ def f(*args, **kwargs):
7979

8080

8181
def test_lambda_closure_cleanup():
82-
m.test_cleanup()
82+
m.test_lambda_closure_cleanup()
8383
cstats = m.payload_cstats()
8484
assert cstats.alive() == 0
8585
assert cstats.copy_constructions == 1
8686
assert cstats.move_constructions >= 1
8787

8888

89+
def test_cpp_callable_cleanup():
90+
alive_counts = m.test_cpp_callable_cleanup()
91+
assert alive_counts == [0, 1, 2, 1, 2, 1, 0]
92+
93+
8994
def test_cpp_function_roundtrip():
9095
"""Test if passing a function pointer from C++ -> Python -> C++ yields the original pointer"""
9196

0 commit comments

Comments
 (0)