-
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: a long-standing bug in the handling of Python multiple inheritance #4762
Conversation
Logging a very surprising observation:
- assert d.get_base_value() == 11
+ assert d.get_base_value() == 12
assert d.get_base_value_from_drvd() == 12
d.reset_base_value(20)
assert d.get_base_value() == 20
- assert d.get_base_value_from_drvd() == 12
+ assert d.get_base_value_from_drvd() == 20
d.reset_base_value_from_drvd(30)
- assert d.get_base_value() == 20
+ assert d.get_base_value() == 30
|
``` 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] ```
``` pytypes.h:1615:12: error: missing space between '""' and suffix identifier ```
This reverts commit 1d4f9ff.
Follow-on to the previous comment: The test also passes with The source of the order dependence is the algorithm here:
With Python code in this order
the result of the algorithm is:
With Python code in this order
the result of the algorithm is:
For completeness, code to reproduce: d37b540 Note that the type of
I.e. the order of the keys is undetermined, which is totally fine, it doesn't matter. However, the order of the bases for a given key is completely deterministic in both cases above. This is a subtle but very serious flaw: Assume some complex user code currently works as expected. It happens to call |
This reverts commit d37b540.
… way, more like a proof of concept.
…the best way, more like a proof of concept." This reverts commit eb09c6c.
…t__` overrides when they are not needed.
… *>()` introduced with PR pybind#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.
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]} {} ```
* Copy clang 17 compatibility fixes from PR #4762 to a separate PR. * Add gcc:13 C++20 * Add silkeh/clang:16-bullseye C++20
@@ -203,6 +205,18 @@ def __init__(self): | |||
assert msg(exc_info.value) == expected | |||
|
|||
|
|||
@pytest.mark.parametrize( |
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.
Is this a new feature enabled by this PR, or a bug fixed along with the PR?
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.
It is a bug fixed along with this PR.
.def("get_base_value", &CppBase::get_base_value) | ||
.def("reset_base_value", &CppBase::reset_base_value); | ||
|
||
py::class_<CppDrvd, CppBase>(m, "CppDrvd") |
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.
By convention, do we require users to list all of the base classes in the template arg list? I thought the usual way is
py::class_, but maybe just doing that Python won't be aware the Py object should have a base class of Py.CppBase?
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.
Yes the convention is to list all of the base classes if we want the base class methods to surface to the Pybind class.
Interesting diamand case.
|
90e8914
to
86ca4e1
Compare
@rainwoodman FYI After our meeting reviewing this PR I decided it's worth the effort catching all ill-behaved situations (not just fix the particular situation I stumbled over). 0a9599f captures some of them, but not all. To get all, we need to bring in the C++ bases as well. I'll try to work on that asap. Intermediate results: I ran Google global testing with 0a9599f:
So plugging this hole could turn out to be relatively high effort for very little practical gain, but we can only be sure after putting in the effort. :-/ For my own reference: Global testing just changing pybind11: OCL:579584090:BASE:579672844:1699221331111:c625089a Global testing changing PyCLIF-pybind11: OCL:579673181:BASE:579726990:1699249978254:6ab60a42 |
Regarding 5f5fd6a: This bug goes pretty deep: The current state of this PR is definitely not meant to be for merging. It's only meant to enable global testing, to see if there are hidden problems in the wild. For that I have to move this work to the pywrapcc branch/repo, because I have to apply a similar patch in smart_holder_type_casters.h. |
``` 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() ```
That's under google/pybind11clif#30077 now. Unit testing with all sanitizers in the Google toolchain passes. Global testing initiated. Results will probably only be available very late today. Note: the divergence check in |
No failures.
That's the only situation we're still not sure about. |
Conclusion from a text chat with @rainwoodman: I will revert the last 3 commits here (not counting merge commits), to get back to the state reviewed in a meeting with @rainwoodman last week (Oct 31), then merge. But also: Create a new PR bringing back the 3 commits (0a9599f, 5f5fd6a, df27188), and do follow-on work separately, later. Rationale: I think it's fair to defer work on catching "diverging" (best term I could think of) inheritance situations because I could not find any problems in the wild (Google global testing). The new PR will serve as documentation and reminder. |
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.
Thanks!
I agree with your assessment: we can revisit the divergence case when there is evidence that someone actually wants to use it. |
The follow-on PR is #4928. |
Description
The equivalent of this PR was merged as google/pybind11clif#30056 on September 12, 2023.
COPY of the google/pybind11clif#30056 PR description (for easy reference):
The core of the added test is this:
Without this PR, the runtime behavior depends on the order in which
PC
orPPCC
are first instantiated, which can be highly surprising and potentially very difficult to debug. See #4762 (comment) for a full analysis.However, use cases appear to be surprisingly rare (#4762 (comment)). The only known use cases are through PyCLIF.
More-or-less as a side-effect, this PR also:
__init__
unnecessarilty.reinterpret_cast
here:pybind11/include/pybind11/detail/class.h
Line 193 in 8c7b8dd
Original PR description:
The core of the test is this:
pybind11 forces adding an
__init__
override (which happens to not disturb PyCLIF or Boost.Python).A wrapped
PPPC
instance evidently has two independent C++ objects (CppBase
andCppDerived
) with pybind11, while it has only one with PyCLIF and Boost.Python (CppDerived
).The nanobind metaclass refuses to create the
PPCC
subtype.Suggested changelog entry: