-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix multiple inheritance with static properties #679
Conversation
I haven't looked at the code yet, but from the description: Wow, very nice! |
I agree -- this looks just fantastic! Suspecting a problem of this sort, I've also been working on a potential workaround by extending the Just one quick clarification: since you modified the way the property itself works, there is no need to touch the (global) metaclass to add a new static property to some class, right? This would have been the case for the previous scheme if all classes shared the same metaclass, which could potentially lead to nasty long overload chains if many static properties have the same name. I'm wondering if there is some use in not quite deprecating I'll need a bit more time to go through the nitty gritty details, but overall the approach sounds very sensible, and de-cluttering pybind11.h is also a good idea. |
That's right. The metatype is no longer mutated when adding new static properties. It's only required to override the default class pybind11_type(type):
def __setattr__(cls, name, value):
prop = cls.__dict__[name]
if isinstance(prop, pybind11_static_property):
pybind11_static_property.__set__(prop, cls, value)
else:
type.__setattr__(cls, name, value) And this is only needed since
I think that shouldn't be a problem. The default |
Wow, that looks excellent! I also haven't had time to go over the code, but it looks like a great solution. I'll see if I have time to look at it in more detail, but in the mean time will test it out on our (quite large) codebase to see if there are any issues. |
@pschella: that sounds great -- please keep us posted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this looks fantastic! I left a few minor comments scattered throughout.
One more that came to me just now is that the differences in this patch also potentially create new opportunities for reference leaks (for instance, are the special static properties collected when a type goes out of scope? This is relevant in cases where types are created dynamically.)
CMakeLists.txt
Outdated
@@ -70,6 +70,7 @@ set(PYBIND11_HEADERS | |||
include/pybind11/stl.h | |||
include/pybind11/stl_bind.h | |||
include/pybind11/typeid.h | |||
include/pybind11/detail/class_.h |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Many of these header files are actually implementation details, so the separation wrt. class_.h
seems artificial. I'd either leave it at the top level (and rename to something without a trailing underscore like class_support.h
) or refactor the header files more significantly (though that doesn't necessarily need to be in this commit).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this PR probably shouldn't introduce a one-off change like that. Renamed detail/class_.h
-> class_support.h
. I squashed back the change right away to keep the commits clean.
include/pybind11/attr.h
Outdated
/// DEPRECATED: Annotation which requests that a special metaclass is created for a type | ||
struct metaclass { | ||
PYBIND11_DEPRECATED("py::metaclass() is no longer required. It's turned on by default now.") | ||
metaclass() = default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Propose changing to
handle value;
PYBIND11_DEPRECATED("py::metaclass() is no longer required. It's turned on by default now.")
metaclass() { }
metaclass(handle value) : value(value) { }
@@ -188,9 +190,6 @@ struct type_record { | |||
/// Does the class implement the buffer protocol? | |||
bool buffer_protocol : 1; | |||
|
|||
/// Does the class require its own metaclass? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I propose adding
/// Custom metaclass (optional)
handle metaclass;
below the doc
member
include/pybind11/attr.h
Outdated
@@ -354,11 +353,8 @@ struct process_attribute<buffer_protocol> : process_attribute_default<buffer_pro | |||
static void init(const buffer_protocol &, type_record *r) { r->buffer_protocol = true; } | |||
}; | |||
|
|||
template <> | |||
struct process_attribute<metaclass> : process_attribute_default<metaclass> { | |||
static void init(const metaclass &, type_record *r) { r->metaclass = true; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Proposal:
template <>
struct process_attribute<metaclass> : process_attribute_default<metaclass> {
static void init(const metaclass &m, type_record *r) { r->metaclass = m.value; }
};
include/pybind11/detail/class_.h
Outdated
|
||
#if !defined(PYPY_VERSION) | ||
|
||
extern "C" inline PyObject *static_descr_get(PyObject *self, PyObject * /*obj*/, PyObject *type) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potentially this function (and many others below) could be lambda functions? Or is the point to force "C" linkage, which may be tricky in a Lambda function? (though AFAIK there is no difference on any platform that we support)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the intent was to stand by the letter of the standard with regard to linkage. The current compilers don't make a distinction between C and C++ linkage, but I'm a bit apprehensive about how stable that is.
I made a separate commit here to see what the lambda approach would look like: dean0x7d@0c44e74 It's a pretty nice readability improvement for short functions like tp_descr_get
/tp_descr_set
, but I'm not sure that the longer ones benefit quite as much.
Overall, my vote would still be to err on the side of portability since it doesn't look like a huge readability improvement to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok -- I think it is fairly readable even with the separation. In case you're still willing to make a few changes, then one thing that would be nice to have is a 1-line comment for each hook that says roughly why its' needed. E.g.
/// Traverse the internal dictionary of an instance. Used to implement garbage collection on types with the dynamic_attr tag.
extern "C" inline int object_traverse(PyObject *self, visitproc visit, void *arg) {
(This one is especially confusing since the surrounding context is missing in the non-lambda implementation.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I'll add extra comments for the functions.
include/pybind11/detail/class_.h
Outdated
/// By default, Python replaces the `static_property` itself, but for wrapped C++ types | ||
/// we need to call `static_property.__set__()` in order to propagate the new value to | ||
/// the underlying C++ data structure. | ||
extern "C" inline int metatype_setattro(PyObject* obj, PyObject* name, PyObject* value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
include/pybind11/detail/class_.h
Outdated
return (PyObject *) heap_type; | ||
} | ||
|
||
inline PyObject *internals::get_base(size_t instance_size) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit of documentation would be nice here.
include/pybind11/detail/class_.h
Outdated
issue no Python C API calls which could potentially invoke the | ||
garbage collector (the GC will call type_traverse(), which will in | ||
turn find the newly constructed type in an invalid state) */ | ||
auto heap_type = (PyHeapTypeObject *) internals.metatype->tp_alloc(internals.metatype, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switch over to rec.metaclass
if specified here.
aa5b557
to
69a91ab
Compare
Regarding the metaclass proposal: Users can now override the default metaclass using Renamed
AFAIK there shouldn't be any reference leaks with regard to |
I like the |
ok, that sounds good. Regarding metaclass vs metatype: the former is used much more frequently at least according to a google search, so I suggest that we stick with that terminology :) |
@jagerman, did you still plan to do a review of this PR? Let me know in case you already did, and I'll go ahead and merge it. |
59861c4
to
3154dc3
Compare
OK, I added some docs for the various PyTypeObject functions. I factored out |
Yeah, I'm taking a detailed look at it now. |
I was particular excited by this part:
but it doesn't seem to be working properly: inh_test.cpp:#include <pybind11/pybind11.h>
#include <iostream>
class B {
public:
B(int v) : v{v} { std::cout << "B ctor @ " << this << "\n"; }
int v;
virtual ~B() = default;
};
class C {
public:
C(double v) : v{v} { std::cout << "C ctor @ " << this << "\n"; }
double v;
virtual ~C() = default;
};
class BC : public B, public C {
public:
BC(int v1, double v2) : B(v1), C(v2) { std::cout << "BC ctor @ " << this << "\n"; }
};
namespace py = pybind11;
PYBIND11_PLUGIN(inh_test) {
py::module m("inh_test");
py::class_<B>(m, "B")
.def(py::init<int>())
;
py::class_<C>(m, "C")
.def(py::init<double>())
;
py::class_<BC, B, C>(m, "BC")
.def(py::init<int, double>())
;
m.def("get_bv", [](B &b) { return b.v; });
m.def("get_cv", [](C &c) { return c.v; });
return m.ptr();
} inh_test.py: from inh_test import B, C, BC, get_bv, get_cv
class X4(B, C):
def __init__(self):
B.__init__(self, 42)
C.__init__(self, 42.5)
class X5(C, B):
def __init__(self):
C.__init__(self, 43.5)
B.__init__(self, 43)
x4 = X4()
x5 = X5()
bc = BC(123, 4.56)
print("Getting X4 b-val:", get_bv(x4))
print("Getting X4 c-val:", get_cv(x4))
print("Getting BC b-val:", get_bv(bc))
print("Getting BC c-val:", get_cv(bc))
print("Getting X5 b-val:", get_bv(x5))
print("Getting X5 c-val:", get_cv(x5)) Outputs:
Note how the two C++ instances are being wrongly initialized at the same location in the MI-from-Python version (which is also apparent from the wrong (due to being overwritten) |
I think there are two issues here: the first is to get the class layout to pass through |
(Boost.Python seems to offer this feature, but their dispatch mechanism is really complicated -- we don't want to go there) |
I'm also seeing some strange behaviour. It seems that base class constructors now seem to take part in the overload resolution of the derived class. This was not the case before and is unexpected both in C++ and Python. |
Yeah, I was a bit eager with that one. I realized I don't think it can be solved without complicating the whole type handling mechanism in pybind11. I'm not sure it's worth pursuing (at least not within this PR). What would be good is to make the type definition an error in Python in order to avoid confusion. I'm trying to put something together now. @pschella Are you also referring to inheritance in Python in your last comment? (The C++ side should be OK.) |
I think @pschella refers to something a bit more worrisome: that in a class definition such as py::class_<Base>(..)
.def(py::init<int>());
py::class_<Derived, Base>(..)
.def(py::init<std::string>()); The |
Yes (I think). What I'm seeing if |
Where |
I believe that should fix the constructor inheritance. (Also, I now realized that a |
include/pybind11/attr.h
Outdated
metaclass() = default; | ||
|
||
/// Override pybind11's default metaclass | ||
metaclass(handle value) : value(value) { } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
explicit
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, good catch.
OK, that last commit should prevent multiple inheritance from C++ bases in Python + a couple of smaller things. Let me know if there is anything else. If not, I'll clean up the commit history a little. |
Ok, so that change seems to have fixed one issue. I am however seeing another, likely unrelated, problem that is preventing me from running this test on all our code. It looks like constructors (and probably functions) taking |
That seems to be an unrelated issue. I'll report it separately. |
Shall we wait for @pschella's issue to be cleared up or merge this now? Either is fine to me. |
Instead of creating a new unique metaclass for each type, the builtin `property` type is subclassed to support static properties. The new setter/getters always pass types instead of instances in their `self` argument. A metaclass is still required to support this behavior, but it doesn't store any data anymore, so a new one doesn't need to be created for each class. There is now only one common metaclass which is shared by all pybind11 types.
In order to fully satisfy Python's inheritance type layout requirements, all types should have a common 'solid' base. A solid base is one which has the same instance size as the derived type (not counting the space required for the optional `dict_ptr` and `weakrefs_ptr`). Thus, `object` does not qualify as a solid base for pybind11 types and this can lead to issues with multiple inheritance. To get around this, new base types are created: one per unique instance size. There is going to be very few of these bases. They ensure Python's MRO checks will pass when multiple bases are involved.
Now that only one shared metaclass is ever allocated, it's extremely cheap to enable it for all pybind11 types. * Deprecate the default py::metaclass() since it's not needed anymore. * Allow users to specify a custom metaclass via py::metaclass(handle).
97a4dd3
to
394b69b
Compare
Rebased and squashed a bit to clean up the history. I guess it would be good to merge now to help out #693. |
Merging now sounds good to me. I went through much of this pretty thoroughly in #693 and didn't notice any issues. |
OK -- merged, thank you! |
Thanks! Together with #693 I can now confirm that this works for our codebase. |
Just to clarify: Are you sure that you need #693 for your project? Or is #679 enough? (The static properties issue should be fixed by the latter) Pushing out a 2.1.0 point release of the master branch fairly soon sounds absolutely doable, but #693 will still require a redesign and time for stabilization? |
Now also tested successfully on 5143989 |
This fixes #597 and also (unintentionally) resolves #594.
@pschella: This fixes the original test code that you posted in #597 and I think the fix is quite thorough. But it might be good if you could test these changes in case there are any missing corner cases.
Changes
The main issue for multiple inheritance is that Python not only requires that all base types are layout compatible, but also that they have a common metaclass. This resulted in problems when mixing classes with different
py::metaclass
andpy::dynamic_attr
specifications. This PR attempts to bring harmony to the different type hierarchies. This is a bit involved but most of the changes are organizational, not fundamental (the types are just stacked a bit differently).Benefits
Multiple inheritance now works with combinations of
py::metaclass
,py::dynamic_attr
and 'vanilla' classes, independent of base list order. This works for multiple inheritance in C++:py::class_<T, CppType1, CppType2>(m, "T")
.The new uniform layout also makes it possible to inherit from multiple C++ types in Python: defining(Not so much this part. See comments below.)class PyType(CppType1, CppType2)
works just likepy::class_<T, CppType1, CppType2>
.The new metaclass implementation is cheap (essentially free) making it possible to enable
py::metaclass
for all classes by default.py::metaclass
is thus deprecated.The new metaclass/static property implementation also brings a minor semantics improvement: it's now possible to access static properties via instances (
x = instance.static_prop
) where this was previously type-only (x = Type.static_prop
).Cost
There's some extra global data (see
internals
) and a an extra pointer in the per-type data (type_info
). But the per-type unique metaclass is now replaced with a single shared metaclass which reduces overall memory requirements.Not counting the new tests, the binary size of the test module stays about the same (+140 bytes = +0.01% on clang/macOS).
Implementation
I hope the following quick ASCII diagrams will help illustrate the implementation change. A general relationship between instance, type and metatype looks something like this:
Here
Type
is some user-define class andinstance = Type()
.Metatype
is the type's type and the inheritance is shown top-down, separately for the type and the metatype.Typically, this is pretty simple in Python:
Here
object
andtype
are Python builtins and the above just illustrates the following in Python3:Now, the existing pybind11 implementation for classes with static properties (
py::metaclass
) looks a bit like this:Regular properties (
instance.prop
) are stored inType
while static properties (Type.prop
) are stored in a metaclass which is unique for eachType
. This doubles the amount of data allocated per type which makes it expensive and thus guarded bypy::metaclass
.This PR changes things around to look like this:
This approach is inspired by
boost.python
but with a slimmer implementation (most notably it doesn't replicate any CPython internal structures). The basic idea is that we extend Python's builtinproperty
type and adapt it for static data. The following Python code is the exact implementation, but we just create it using the C API:The
static_property
still needs a bit of support from a metaclass (see the code for details), but this is only a uniform behavior change and the metaclass doesn't need to store any data. Therefore, the metaclass doesn't need to be unique per type. We create a single one and share it among all the types which need it. And since that's all there is, we can also just let all types point to it and have "free" static properties.This resolves one part of the multiple inheritance MRO issues: having a common metaclass for all types makes them compatible while the unique metatypes were blockers. However, there is an additional hurdle. In order to fully satisfy Python's type layout requirements, all types should have a common 'solid' base. A solid base is one which has the same instance size as the derived type (not counting the space required for the optional
dict_ptr
andweakrefs_ptr
). Thus,object
does not qualify as a solid base for pybind11 types and this can lead to issues with multiple inheritance.In the example above,
instance_size(Pybind11Type1) > instance_size(object)
andinstance_size(Pybind11Type1) == instance_size(Pybind11Type2)
soobject
is not considered a solid base ofMIType
. This can result inPyType_Ready()
complaining about layout and refusing to finalize the type for some base combinations.The solution is to create a new base type:
Here
instance_size(pybind11_object) == instance_size(Pybind11Type1, Pybind11Type2)
and thusPyType_Ready()
is perfectly happy to support it.Notes
Since this changes a good bit of the
py::class_
implementation, I've also used this opportunity to move most of the details into a separate file (detail/class_.h
) since the mainpybind11.h
is loaded enough as it is.