Skip to content

Commit 97a4dd3

Browse files
committed
Disallow inheriting from multiple C++ bases in Python
* Also rename `pybind11_init` in `class_support.h` to avoid ambiguity with the one in `common.h`. * Make `py::metaclass` constructor explicit.
1 parent c00de80 commit 97a4dd3

File tree

3 files changed

+50
-9
lines changed

3 files changed

+50
-9
lines changed

include/pybind11/attr.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ struct metaclass {
6161
metaclass() = default;
6262

6363
/// Override pybind11's default metaclass
64-
metaclass(handle value) : value(value) { }
64+
explicit metaclass(handle value) : value(value) { }
6565
};
6666

6767
/// Annotation to mark enums as an arithmetic type

include/pybind11/class_support.h

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,34 @@ inline PyTypeObject *make_static_property_type() {
8585

8686
#endif // PYPY
8787

88+
/** Inheriting from multiple C++ types in Python is not supported -- set an error instead.
89+
A Python definition (`class C(A, B): pass`) will call `tp_new` so we check for multiple
90+
C++ bases here. On the other hand, C++ type definitions (`py::class_<C, A, B>(m, "C")`)
91+
don't not use `tp_new` and will not trigger this error. */
92+
extern "C" inline PyObject *pybind11_meta_new(PyTypeObject *metaclass, PyObject *args,
93+
PyObject *kwargs) {
94+
PyObject *bases = PyTuple_GetItem(args, 1); // arguments: (name, bases, dict)
95+
if (!bases)
96+
return nullptr;
97+
98+
auto &internals = get_internals();
99+
auto num_cpp_bases = 0;
100+
for (auto base : reinterpret_borrow<tuple>(bases)) {
101+
auto base_type = (PyTypeObject *) base.ptr();
102+
auto instance_size = static_cast<size_t>(base_type->tp_basicsize);
103+
if (PyObject_IsSubclass(base.ptr(), internals.get_base(instance_size)))
104+
++num_cpp_bases;
105+
}
106+
107+
if (num_cpp_bases > 1) {
108+
PyErr_SetString(PyExc_TypeError, "Can't inherit from multiple C++ classes in Python."
109+
"Use py::class_ to define the class in C++ instead.");
110+
return nullptr;
111+
} else {
112+
return PyType_Type.tp_new(metaclass, args, kwargs);
113+
}
114+
}
115+
88116
/** Types with static properties need to handle `Type.static_prop = x` in a specific way.
89117
By default, Python replaces the `static_property` itself, but for wrapped C++ types
90118
we need to call `static_property.__set__()` in order to propagate the new value to
@@ -135,6 +163,8 @@ inline PyTypeObject* make_default_metaclass() {
135163
type->tp_name = name;
136164
type->tp_base = &PyType_Type;
137165
type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE;
166+
167+
type->tp_new = pybind11_meta_new;
138168
type->tp_setattro = pybind11_meta_setattro;
139169

140170
if (PyType_Ready(type) < 0)
@@ -145,7 +175,7 @@ inline PyTypeObject* make_default_metaclass() {
145175

146176
/// Instance creation function for all pybind11 types. It only allocates space for the
147177
/// C++ object, but doesn't call the constructor -- an `__init__` function must do that.
148-
extern "C" inline PyObject *pybind11_new(PyTypeObject *type, PyObject *, PyObject *) {
178+
extern "C" inline PyObject *pybind11_object_new(PyTypeObject *type, PyObject *, PyObject *) {
149179
PyObject *self = type->tp_alloc(type, 0);
150180
auto instance = (instance_essentials<void> *) self;
151181
auto tinfo = get_type_info(type);
@@ -159,7 +189,7 @@ extern "C" inline PyObject *pybind11_new(PyTypeObject *type, PyObject *, PyObjec
159189
/// An `__init__` function constructs the C++ object. Users should provide at least one
160190
/// of these using `py::init` or directly with `.def(__init__, ...)`. Otherwise, the
161191
/// following default function will be used which simply throws an exception.
162-
extern "C" inline int pybind11_init(PyObject *self, PyObject *, PyObject *) {
192+
extern "C" inline int pybind11_object_init(PyObject *self, PyObject *, PyObject *) {
163193
PyTypeObject *type = Py_TYPE(self);
164194
std::string msg;
165195
#if defined(PYPY_VERSION)
@@ -173,7 +203,7 @@ extern "C" inline int pybind11_init(PyObject *self, PyObject *, PyObject *) {
173203

174204
/// Instance destructor function for all pybind11 types. It calls `type_info.dealloc`
175205
/// to destroy the C++ object itself, while the rest is Python bookkeeping.
176-
extern "C" inline void pybind11_dealloc(PyObject *self) {
206+
extern "C" inline void pybind11_object_dealloc(PyObject *self) {
177207
auto instance = (instance_essentials<void> *) self;
178208
if (instance->value) {
179209
auto type = Py_TYPE(self);
@@ -190,7 +220,7 @@ extern "C" inline void pybind11_dealloc(PyObject *self) {
190220
}
191221
}
192222
if (!found)
193-
pybind11_fail("pybind11_dealloc(): Tried to deallocate unregistered instance!");
223+
pybind11_fail("pybind11_object_dealloc(): Tried to deallocate unregistered instance!");
194224

195225
if (instance->weakrefs)
196226
PyObject_ClearWeakRefs(self);
@@ -230,9 +260,9 @@ inline PyObject *make_object_base_type(size_t instance_size) {
230260
type->tp_basicsize = static_cast<ssize_t>(instance_size);
231261
type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE;
232262

233-
type->tp_new = pybind11_new;
234-
type->tp_init = pybind11_init;
235-
type->tp_dealloc = pybind11_dealloc;
263+
type->tp_new = pybind11_object_new;
264+
type->tp_init = pybind11_object_init;
265+
type->tp_dealloc = pybind11_object_dealloc;
236266

237267
/* Support weak references (needed for the keep_alive feature) */
238268
type->tp_weaklistoffset = offsetof(instance_essentials<void>, weakrefs);
@@ -429,7 +459,7 @@ inline PyObject* make_new_python_type(const type_record &rec) {
429459
type->tp_bases = bases.release().ptr();
430460

431461
/* Don't inherit base __init__ */
432-
type->tp_init = pybind11_init;
462+
type->tp_init = pybind11_object_init;
433463

434464
/* Supported protocols */
435465
type->tp_as_number = &heap_type->as_number;

tests/test_multiple_inheritance.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ def __init__(self, i, j):
5252
assert mt.bar() == 4
5353

5454

55+
def test_multiple_inheritance_error():
56+
"""Inheriting from multiple C++ bases in Python is not supported"""
57+
from pybind11_tests import Base1, Base2
58+
59+
with pytest.raises(TypeError) as excinfo:
60+
# noinspection PyUnusedLocal
61+
class MI(Base1, Base2):
62+
pass
63+
assert "Can't inherit from multiple C++ classes in Python" in str(excinfo.value)
64+
65+
5566
def test_multiple_inheritance_virtbase():
5667
from pybind11_tests import Base12a, bar_base2a, bar_base2a_sharedptr
5768

0 commit comments

Comments
 (0)