Skip to content

Commit e250155

Browse files
authored
Fix a long-standing bug in the handling of Python multiple inheritance (#4762)
* Equivalent of google/clif@5718e4d * Resolve clang-tidy errors. * Moving test_PPCCInit() first changes the behavior! * Resolve new Clang dev C++11 errors: ``` The CXX compiler identification is Clang 17.0.0 ``` ``` pytypes.h:1615:23: error: identifier '_s' preceded by whitespace in a literal operator declaration is deprecated [-Werror,-Wdeprecated-literal-operator] ``` ``` cast.h:1380:26: error: identifier '_a' preceded by whitespace in a literal operator declaration is deprecated [-Werror,-Wdeprecated-literal-operator] ``` * Resolve gcc 4.8.5 error: ``` pytypes.h:1615:12: error: missing space between '""' and suffix identifier ``` * Specifically exclude `__clang__` * Snapshot of debugging code (does NOT pass pre-commit checks). * Revert "Snapshot of debugging code (does NOT pass pre-commit checks)." This reverts commit 1d4f9ff. * [ci skip] Order Dependence Demo * Revert "[ci skip] Order Dependence Demo" This reverts commit d37b540. * One way to deal with the order dependency issue. This is not the best way, more like a proof of concept. * Move test_PC() first again. * Add `all_type_info_add_base_most_derived_first()`, use in `all_type_info_populate()` * Revert "One way to deal with the order dependency issue. This is not the best way, more like a proof of concept." This reverts commit eb09c6c. * clang-tidy fixes (automatic) * Add `is_redundant_value_and_holder()` and use to avoid forcing `__init__` overrides when they are not needed. * Streamline implementation and avoid unsafe `reinterpret_cast<instance *>()` introduced with PR #2152 The `reinterpret_cast<instance *>(self)` is unsafe if `__new__` is mocked, which was actually found in the wild: the mock returned `None` for `self`. This was inconsequential because `inst` is currently cast straight back to `PyObject *` to compute `all_type_info()`, which is empty if `self` is not a pybind11 `instance`, and then `inst` is never dereferenced. However, the unsafe detour through `instance *` is easily avoided and the updated implementation is less prone to accidents while debugging or refactoring. * Fix actual undefined behavior exposed by previous changes. It turns out the previous commit message is incorrect, the `inst` pointer is actually dereferenced, in the `value_and_holder` ctor here: https://github.com/pybind/pybind11/blob/f3e0602802c7840992c97f4960515777cad6a5c7/include/pybind11/detail/type_caster_base.h#L262-L263 ``` 259 // Main constructor for a found value/holder: 260 value_and_holder(instance *i, const detail::type_info *type, size_t vpos, size_t index) 261 : inst{i}, index{index}, type{type}, 262 vh{inst->simple_layout ? inst->simple_value_holder 263 : &inst->nonsimple.values_and_holders[vpos]} {} ``` * Add test_mock_new() * Experiment: specify indirect bases * Revert "Experiment: specify indirect bases" This reverts commit 4f90d85. * Add `all_type_info_check_for_divergence()` and some tests. * Call `all_type_info_check_for_divergence()` also from `type_caster_generic::load_impl<>` * Resolve clang-tidy error: ``` include/pybind11/detail/type_caster_base.h:795:21: error: the 'empty' method should be used to check for emptiness instead of 'size' [readability-container-size-empty,-warnings-as-errors] if (matching_bases.size() != 0) { ^~~~~~~~~~~~~~~~~~~~~~~~~~ !matching_bases.empty() ``` * Revert "Resolve clang-tidy error:" This reverts commit df27188. * Revert "Call `all_type_info_check_for_divergence()` also from `type_caster_generic::load_impl<>`" This reverts commit 5f5fd6a. * Revert "Add `all_type_info_check_for_divergence()` and some tests." This reverts commit 0a9599f.
1 parent 2c35fde commit e250155

File tree

6 files changed

+140
-12
lines changed

6 files changed

+140
-12
lines changed

include/pybind11/detail/class.h

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,12 +189,10 @@ extern "C" inline PyObject *pybind11_meta_call(PyObject *type, PyObject *args, P
189189
return nullptr;
190190
}
191191

192-
// This must be a pybind11 instance
193-
auto *instance = reinterpret_cast<detail::instance *>(self);
194-
195192
// Ensure that the base __init__ function(s) were called
196-
for (const auto &vh : values_and_holders(instance)) {
197-
if (!vh.holder_constructed()) {
193+
values_and_holders vhs(self);
194+
for (const auto &vh : vhs) {
195+
if (!vh.holder_constructed() && !vhs.is_redundant_value_and_holder(vh)) {
198196
PyErr_Format(PyExc_TypeError,
199197
"%.200s.__init__() must be called when overriding __init__",
200198
get_fully_qualified_tp_name(vh.type->type).c_str());

include/pybind11/detail/type_caster_base.h

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,22 @@ class loader_life_support {
102102
inline std::pair<decltype(internals::registered_types_py)::iterator, bool>
103103
all_type_info_get_cache(PyTypeObject *type);
104104

105+
// Band-aid workaround to fix a subtle but serious bug in a minimalistic fashion. See PR #4762.
106+
inline void all_type_info_add_base_most_derived_first(std::vector<type_info *> &bases,
107+
type_info *addl_base) {
108+
for (auto it = bases.begin(); it != bases.end(); it++) {
109+
type_info *existing_base = *it;
110+
if (PyType_IsSubtype(addl_base->type, existing_base->type) != 0) {
111+
bases.insert(it, addl_base);
112+
return;
113+
}
114+
}
115+
bases.push_back(addl_base);
116+
}
117+
105118
// Populates a just-created cache entry.
106119
PYBIND11_NOINLINE void all_type_info_populate(PyTypeObject *t, std::vector<type_info *> &bases) {
120+
assert(bases.empty());
107121
std::vector<PyTypeObject *> check;
108122
for (handle parent : reinterpret_borrow<tuple>(t->tp_bases)) {
109123
check.push_back((PyTypeObject *) parent.ptr());
@@ -136,7 +150,7 @@ PYBIND11_NOINLINE void all_type_info_populate(PyTypeObject *t, std::vector<type_
136150
}
137151
}
138152
if (!found) {
139-
bases.push_back(tinfo);
153+
all_type_info_add_base_most_derived_first(bases, tinfo);
140154
}
141155
}
142156
} else if (type->tp_bases) {
@@ -322,18 +336,29 @@ struct values_and_holders {
322336
explicit values_and_holders(instance *inst)
323337
: inst{inst}, tinfo(all_type_info(Py_TYPE(inst))) {}
324338

339+
explicit values_and_holders(PyObject *obj)
340+
: inst{nullptr}, tinfo(all_type_info(Py_TYPE(obj))) {
341+
if (!tinfo.empty()) {
342+
inst = reinterpret_cast<instance *>(obj);
343+
}
344+
}
345+
325346
struct iterator {
326347
private:
327348
instance *inst = nullptr;
328349
const type_vec *types = nullptr;
329350
value_and_holder curr;
330351
friend struct values_and_holders;
331-
iterator(instance *inst, const type_vec *tinfo)
332-
: inst{inst}, types{tinfo},
333-
curr(inst /* instance */,
334-
types->empty() ? nullptr : (*types)[0] /* type info */,
335-
0, /* vpos: (non-simple types only): the first vptr comes first */
336-
0 /* index */) {}
352+
iterator(instance *inst, const type_vec *tinfo) : inst{inst}, types{tinfo} {
353+
if (inst != nullptr) {
354+
assert(!types->empty());
355+
curr = value_and_holder(
356+
inst /* instance */,
357+
(*types)[0] /* type info */,
358+
0, /* vpos: (non-simple types only): the first vptr comes first */
359+
0 /* index */);
360+
}
361+
}
337362
// Past-the-end iterator:
338363
explicit iterator(size_t end) : curr(end) {}
339364

@@ -364,6 +389,16 @@ struct values_and_holders {
364389
}
365390

366391
size_t size() { return tinfo.size(); }
392+
393+
// Band-aid workaround to fix a subtle but serious bug in a minimalistic fashion. See PR #4762.
394+
bool is_redundant_value_and_holder(const value_and_holder &vh) {
395+
for (size_t i = 0; i < vh.index; i++) {
396+
if (PyType_IsSubtype(tinfo[i]->type, tinfo[vh.index]->type) != 0) {
397+
return true;
398+
}
399+
}
400+
return false;
401+
}
367402
};
368403

369404
/**

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ set(PYBIND11_TEST_FILES
144144
test_opaque_types
145145
test_operator_overloading
146146
test_pickling
147+
test_python_multiple_inheritance
147148
test_pytypes
148149
test_sequences_and_iterators
149150
test_smart_ptr

tests/test_class.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from unittest import mock
2+
13
import pytest
24

35
import env
@@ -203,6 +205,18 @@ def __init__(self):
203205
assert msg(exc_info.value) == expected
204206

205207

208+
@pytest.mark.parametrize(
209+
"mock_return_value", [None, (1, 2, 3), m.Pet("Polly", "parrot"), m.Dog("Molly")]
210+
)
211+
def test_mock_new(mock_return_value):
212+
with mock.patch.object(
213+
m.Pet, "__new__", return_value=mock_return_value
214+
) as mock_new:
215+
obj = m.Pet("Noname", "Nospecies")
216+
assert obj is mock_return_value
217+
mock_new.assert_called_once_with(m.Pet, "Noname", "Nospecies")
218+
219+
206220
def test_automatic_upcasting():
207221
assert type(m.return_class_1()).__name__ == "DerivedClass1"
208222
assert type(m.return_class_2()).__name__ == "DerivedClass2"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#include "pybind11_tests.h"
2+
3+
namespace test_python_multiple_inheritance {
4+
5+
// Copied from:
6+
// https://github.com/google/clif/blob/5718e4d0807fd3b6a8187dde140069120b81ecef/clif/testing/python_multiple_inheritance.h
7+
8+
struct CppBase {
9+
explicit CppBase(int value) : base_value(value) {}
10+
int get_base_value() const { return base_value; }
11+
void reset_base_value(int new_value) { base_value = new_value; }
12+
13+
private:
14+
int base_value;
15+
};
16+
17+
struct CppDrvd : CppBase {
18+
explicit CppDrvd(int value) : CppBase(value), drvd_value(value * 3) {}
19+
int get_drvd_value() const { return drvd_value; }
20+
void reset_drvd_value(int new_value) { drvd_value = new_value; }
21+
22+
int get_base_value_from_drvd() const { return get_base_value(); }
23+
void reset_base_value_from_drvd(int new_value) { reset_base_value(new_value); }
24+
25+
private:
26+
int drvd_value;
27+
};
28+
29+
} // namespace test_python_multiple_inheritance
30+
31+
TEST_SUBMODULE(python_multiple_inheritance, m) {
32+
using namespace test_python_multiple_inheritance;
33+
34+
py::class_<CppBase>(m, "CppBase")
35+
.def(py::init<int>())
36+
.def("get_base_value", &CppBase::get_base_value)
37+
.def("reset_base_value", &CppBase::reset_base_value);
38+
39+
py::class_<CppDrvd, CppBase>(m, "CppDrvd")
40+
.def(py::init<int>())
41+
.def("get_drvd_value", &CppDrvd::get_drvd_value)
42+
.def("reset_drvd_value", &CppDrvd::reset_drvd_value)
43+
.def("get_base_value_from_drvd", &CppDrvd::get_base_value_from_drvd)
44+
.def("reset_base_value_from_drvd", &CppDrvd::reset_base_value_from_drvd);
45+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Adapted from:
2+
# https://github.com/google/clif/blob/5718e4d0807fd3b6a8187dde140069120b81ecef/clif/testing/python/python_multiple_inheritance_test.py
3+
4+
from pybind11_tests import python_multiple_inheritance as m
5+
6+
7+
class PC(m.CppBase):
8+
pass
9+
10+
11+
class PPCC(PC, m.CppDrvd):
12+
pass
13+
14+
15+
def test_PC():
16+
d = PC(11)
17+
assert d.get_base_value() == 11
18+
d.reset_base_value(13)
19+
assert d.get_base_value() == 13
20+
21+
22+
def test_PPCC():
23+
d = PPCC(11)
24+
assert d.get_drvd_value() == 33
25+
d.reset_drvd_value(55)
26+
assert d.get_drvd_value() == 55
27+
28+
assert d.get_base_value() == 11
29+
assert d.get_base_value_from_drvd() == 11
30+
d.reset_base_value(20)
31+
assert d.get_base_value() == 20
32+
assert d.get_base_value_from_drvd() == 20
33+
d.reset_base_value_from_drvd(30)
34+
assert d.get_base_value() == 30
35+
assert d.get_base_value_from_drvd() == 30

0 commit comments

Comments
 (0)