Skip to content

Commit afdc09d

Browse files
[master] Wrong caching of overrides (#3465)
* override: Fix wrong caching of the overrides There was a problem when the python type, which was stored in override cache for C++ functions, was destroyed and the record wasn't removed from the override cache. Therefor, dangling pointer was stored there. Then when the memory was reused and new type was allocated at the given address and the method with the same name (as previously stored in the cache) was actually overridden in python, it would wrongly find it in the override cache for C++ functions and therefor override from python wouldn't be called. The fix is to erase the type from the override cache when the type is destroyed. * test: Pass by const ref instead of by value (clang-tidy) * test: Rename classes and move to different files Rename the classes and files so they're no too generic. Also, better place to test the stuff is in test_virtual_functions.cpp/.py as we're basically testing the virtual functions/trampolines. * Add TODO for erasure code * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 270b11d commit afdc09d

File tree

6 files changed

+122
-1
lines changed

6 files changed

+122
-1
lines changed

include/pybind11/pybind11.h

+10
Original file line numberDiff line numberDiff line change
@@ -1979,6 +1979,16 @@ inline std::pair<decltype(internals::registered_types_py)::iterator, bool> all_t
19791979
// gets destroyed:
19801980
weakref((PyObject *) type, cpp_function([type](handle wr) {
19811981
get_internals().registered_types_py.erase(type);
1982+
1983+
// TODO consolidate the erasure code in pybind11_meta_dealloc() in class.h
1984+
auto &cache = get_internals().inactive_override_cache;
1985+
for (auto it = cache.begin(), last = cache.end(); it != last; ) {
1986+
if (it->first == reinterpret_cast<PyObject *>(type))
1987+
it = cache.erase(it);
1988+
else
1989+
++it;
1990+
}
1991+
19821992
wr.dec_ref();
19831993
})).release();
19841994
}

tests/test_embed/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pybind11_enable_warnings(test_embed)
2525
target_link_libraries(test_embed PRIVATE pybind11::embed Catch2::Catch2 Threads::Threads)
2626

2727
if(NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_CURRENT_BINARY_DIR)
28-
file(COPY test_interpreter.py DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
28+
file(COPY test_interpreter.py test_trampoline.py DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
2929
endif()
3030

3131
add_custom_target(

tests/test_embed/test_interpreter.cpp

+49
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ class PyWidget final : public Widget {
3737
std::string argv0() const override { PYBIND11_OVERRIDE_PURE(std::string, Widget, argv0); }
3838
};
3939

40+
class test_override_cache_helper {
41+
42+
public:
43+
virtual int func() { return 0; }
44+
45+
test_override_cache_helper() = default;
46+
virtual ~test_override_cache_helper() = default;
47+
// Non-copyable
48+
test_override_cache_helper &operator=(test_override_cache_helper const &Right) = delete;
49+
test_override_cache_helper(test_override_cache_helper const &Copy) = delete;
50+
};
51+
52+
class test_override_cache_helper_trampoline : public test_override_cache_helper {
53+
int func() override { PYBIND11_OVERRIDE(int, test_override_cache_helper, func); }
54+
};
55+
4056
PYBIND11_EMBEDDED_MODULE(widget_module, m) {
4157
py::class_<Widget, PyWidget>(m, "Widget")
4258
.def(py::init<std::string>())
@@ -45,6 +61,12 @@ PYBIND11_EMBEDDED_MODULE(widget_module, m) {
4561
m.def("add", [](int i, int j) { return i + j; });
4662
}
4763

64+
PYBIND11_EMBEDDED_MODULE(trampoline_module, m) {
65+
py::class_<test_override_cache_helper, test_override_cache_helper_trampoline, std::shared_ptr<test_override_cache_helper>>(m, "test_override_cache_helper")
66+
.def(py::init_alias<>())
67+
.def("func", &test_override_cache_helper::func);
68+
}
69+
4870
PYBIND11_EMBEDDED_MODULE(throw_exception, ) {
4971
throw std::runtime_error("C++ Error");
5072
}
@@ -73,6 +95,33 @@ TEST_CASE("Pass classes and data between modules defined in C++ and Python") {
7395
REQUIRE(cpp_widget.the_answer() == 42);
7496
}
7597

98+
TEST_CASE("Override cache") {
99+
auto module_ = py::module_::import("test_trampoline");
100+
REQUIRE(py::hasattr(module_, "func"));
101+
REQUIRE(py::hasattr(module_, "func2"));
102+
103+
auto locals = py::dict(**module_.attr("__dict__"));
104+
105+
int i = 0;
106+
for (; i < 1500; ++i) {
107+
std::shared_ptr<test_override_cache_helper> p_obj;
108+
std::shared_ptr<test_override_cache_helper> p_obj2;
109+
110+
py::object loc_inst = locals["func"]();
111+
p_obj = py::cast<std::shared_ptr<test_override_cache_helper>>(loc_inst);
112+
113+
int ret = p_obj->func();
114+
115+
REQUIRE(ret == 42);
116+
117+
loc_inst = locals["func2"]();
118+
119+
p_obj2 = py::cast<std::shared_ptr<test_override_cache_helper>>(loc_inst);
120+
121+
p_obj2->func();
122+
}
123+
}
124+
76125
TEST_CASE("Import error handling") {
77126
REQUIRE_NOTHROW(py::module_::import("widget_module"));
78127
REQUIRE_THROWS_WITH(py::module_::import("throw_exception"),

tests/test_embed/test_trampoline.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import trampoline_module
4+
5+
6+
def func():
7+
class Test(trampoline_module.test_override_cache_helper):
8+
def func(self):
9+
return 42
10+
11+
return Test()
12+
13+
14+
def func2():
15+
class Test(trampoline_module.test_override_cache_helper):
16+
pass
17+
18+
return Test()

tests/test_virtual_functions.cpp

+25
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,25 @@ static void test_gil_from_thread() {
214214
t.join();
215215
}
216216

217+
class test_override_cache_helper {
218+
219+
public:
220+
virtual int func() { return 0; }
221+
222+
test_override_cache_helper() = default;
223+
virtual ~test_override_cache_helper() = default;
224+
// Non-copyable
225+
test_override_cache_helper &operator=(test_override_cache_helper const &Right) = delete;
226+
test_override_cache_helper(test_override_cache_helper const &Copy) = delete;
227+
};
228+
229+
class test_override_cache_helper_trampoline : public test_override_cache_helper {
230+
int func() override { PYBIND11_OVERRIDE(int, test_override_cache_helper, func); }
231+
};
232+
233+
inline int test_override_cache(std::shared_ptr<test_override_cache_helper> const &instance) { return instance->func(); }
234+
235+
217236

218237
// Forward declaration (so that we can put the main tests here; the inherited virtual approaches are
219238
// rather long).
@@ -378,6 +397,12 @@ TEST_SUBMODULE(virtual_functions, m) {
378397
// .def("str_ref", &OverrideTest::str_ref)
379398
.def("A_value", &OverrideTest::A_value)
380399
.def("A_ref", &OverrideTest::A_ref);
400+
401+
py::class_<test_override_cache_helper, test_override_cache_helper_trampoline, std::shared_ptr<test_override_cache_helper>>(m, "test_override_cache_helper")
402+
.def(py::init_alias<>())
403+
.def("func", &test_override_cache_helper::func);
404+
405+
m.def("test_override_cache", test_override_cache);
381406
}
382407

383408

tests/test_virtual_functions.py

+19
Original file line numberDiff line numberDiff line change
@@ -439,3 +439,22 @@ def test_issue_1454():
439439
# Fix issue #1454 (crash when acquiring/releasing GIL on another thread in Python 2.7)
440440
m.test_gil()
441441
m.test_gil_from_thread()
442+
443+
444+
def test_python_override():
445+
def func():
446+
class Test(m.test_override_cache_helper):
447+
def func(self):
448+
return 42
449+
450+
return Test()
451+
452+
def func2():
453+
class Test(m.test_override_cache_helper):
454+
pass
455+
456+
return Test()
457+
458+
for _ in range(1500):
459+
assert m.test_override_cache(func()) == 42
460+
assert m.test_override_cache(func2()) == 0

0 commit comments

Comments
 (0)