Skip to content

[smart_holder] .def_readonly, .def_readwrite adaptors #3581

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

Closed
wants to merge 39 commits into from

Conversation

rwgk
Copy link
Collaborator

@rwgk rwgk commented Dec 29, 2021

Description

NOTE: The continuation of this work is under PR #3844.

The original motivation for this PR was to fix the issue observed here: disowning creates a dangling pointer.

To ensure internal consistency down the road, the scope of this PR was expanded to cover .def_readonly and .def_readwrite behavior for

  • value
  • raw pointer
  • std::unique_ptr
  • std::shared_ptr

members (mutable & const for the pointer types).

For shared_ptr members, the existing behavior is as desired (no work needed).

For value and raw-pointer members, the PyCLIF approach here was adopted: aliasing shared_ptr tying the lifetime of the "outer" object to the lifetime of the Python object pointing to the member. In this way the existing mechanisms prevent the "outer" object from being disowned while pointers to members are still alive.

For unique_ptr members, the situation is complex. This PR only establishes a proof of concept. See the code and comments in tests/test_class_sh_property.py for details. Productizing the proof of concept is left for a future separate PR, as needed.

@rwgk rwgk force-pushed the sh_property branch 2 times, most recently from 2ca30c8 to 0b821ba Compare January 4, 2022 00:54
@rwgk
Copy link
Collaborator Author

rwgk commented Jan 21, 2022

To track progress (FYI @wangxf123456):

I've verified that the PyCLIF TODO here can be removed when using this PR (ASAN clean).

     a = nested_fields.AA()
     a.b.c.i = 123
     c = a.b.c
-    if 'pybind11' not in nested_fields.__doc__:
-      with self.assertRaises(ValueError):
-        # Cannot give up |a| as |c| still is alive
-        nested_fields.ConsumeAA(a)
-      del c
-      # Since |c| is now gone, we can give up |a|
+    with self.assertRaises(ValueError):
+      # Cannot give up |a| as |c| still is alive
       nested_fields.ConsumeAA(a)
-    else:
-      # TODO(rwgk): This disowns |a| even though |c| is still alive. Accessing
-      # |c| beyond this point generates an ASAN heap-use-after-free error.
-      nested_fields.ConsumeAA(a)
+    del c
+    # Since |c| is now gone, we can give up |a|
+    nested_fields.ConsumeAA(a)

     a = nested_fields.AA()
     a.b.c.i = 123

@rwgk rwgk changed the title [smart_holder] WIP property & disowning [smart_holder] .def_readonly, .def_readwrite adaptors Jan 22, 2022
@rwgk rwgk marked this pull request as ready for review January 22, 2022 02:18
@rwgk rwgk requested a review from henryiii as a code owner January 22, 2022 02:18
Comment on lines 1446 to 1457
detail::enable_if_t<detail::type_uses_smart_holder_type_caster<T>::value
&& detail::type_uses_smart_holder_type_caster<D>::value
&& !std::is_pointer<D>::value //
&& !detail::is_std_unique_ptr<D>::value //
&& !detail::is_std_shared_ptr<D>::value>> {
Copy link
Collaborator

@Skylion007 Skylion007 Jan 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
detail::enable_if_t<detail::type_uses_smart_holder_type_caster<T>::value
&& detail::type_uses_smart_holder_type_caster<D>::value
&& !std::is_pointer<D>::value //
&& !detail::is_std_unique_ptr<D>::value //
&& !detail::is_std_shared_ptr<D>::value>> {
detail::enable_if_t<detail::all_of<
detail::type_uses_smart_holder_type_caster<T>,
detail::type_uses_smart_holder_type_caster<D>,
!std::is_pointer<D>, //
!detail::is_std_unique_ptr<D>, //
!detail::is_std_shared_ptr<D>>::value>> {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out! It looks a lot nicer.

using drp = typename std::remove_pointer<D>::type;

template <typename PM>
static cpp_function readonly(PM pm, const handle &hdl) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static cpp_function readonly(PM pm, const handle &hdl) {
static cpp_function readonly(PM &pm, const handle &hdl) {

and capture by reference in the lambda. At the very least, the PM should be forwarded otherwise.

I think original pybind11 it's a pointer. being passed around.

This could invoke copy or moves depending on the type PMs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't get this to work. I tried a few variations of sprinkling in &, &&, std::forward<PM>, but either it didn't build or I got segfaults. Then I decided it's better to just make it obvious that we're only dealing with pointers anyway, and the perfect forwarding doesn't buy us anything. See the long added comment for the new must_be_member_function_pointer SFINAE helper.

Copy link
Collaborator

@Skylion007 Skylion007 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rwgk Made some nits about potentially bugs that could arise from the typing of the templates and that all the helpers could benefit from using the detail::all_of helper defined in details/commons.h

@rwgk
Copy link
Collaborator Author

rwgk commented Jan 28, 2022

The CI failures are sure to be flakes or (hopefully transient) infrastructure failures. The previous CI run had no failures. I only added a couple comments (and ran pre-commit run --all-files before pushing).

assert field.num == -99
with pytest.raises(ValueError) as excinfo:
m.DisownOuter(outer)
assert msg(excinfo.value) == "Cannot disown use_count != 1 (loaded_as_unique_ptr)."

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test case is clear and well defined behavior.

My concern is on the practical side -- it is hard to know where the reference to the object is. field could have been passed and kept in many remote part of the application.

A very canonical pattern I know of, for this type of situation is to mark the object as stale, and all uses of it errors. A very typical example is the 'NFS stale handle' error. Then the responsibility is distributed to the consumers of the inner objects, rather than the owner needing to reason a large code base 'who is reference my inner parts?'.

assert msg(excinfo.value) == "Cannot disown use_count != 1 (loaded_as_unique_ptr)."
del field
m.DisownOuter(outer)
with pytest.raises(ValueError) as excinfo:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest.raises(ValueError, match=r".* 123 .*")?


struct Outer {
// The compact 4-character naming matches that in test_class_sh_basic.cpp
// (c = const, m = mutable).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe comment sh = shared_ptr and uq = unique_ptr as well.

int num = -88;
};

struct ClassicOuter {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the Classic prefix imply that this lass in not wrapped in PYBIND11_SMART_HOLDER_TYPE_CASTERS. A comment above each of these would help with readability.

@@ -0,0 +1,90 @@
#include "pybind11_tests.h"

#include "pybind11/smart_holder.h"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen
#include <pybind11/smart_holder.h>
Elsewhere is there a consistent choice we should make with these includes?

@charlesbeattie
Copy link

Made some small style comments othwise it looks good to me.

rwgk added a commit to rwgk/pybind11 that referenced this pull request Apr 2, 2022
rwgk added a commit to rwgk/pybind11 that referenced this pull request Apr 2, 2022
@rwgk
Copy link
Collaborator Author

rwgk commented Apr 2, 2022

I'm continuing this work under PR #3844. See the Note at top of the PR description there.

@rwgk rwgk closed this Apr 2, 2022
rwgk added a commit to rwgk/pybind11 that referenced this pull request May 13, 2022
rwgk added a commit to rwgk/pybind11 that referenced this pull request May 13, 2022
rwgk added a commit that referenced this pull request May 17, 2022
…f PR #3581). (#3844)

* Transferred net diff from PR #3581, as-is.

* Automatic `pre-commit run --all-files` fixes. NO manual changes.

* Removing trailing `//` (originally added to manipulate clang-format), as suggested by @charlesbeattie back in Jan/Feb under PR #3581.

* Renaming `xetter_cpp_function` to `property_cpp_function` as suggested by @rainwoodman

* Fully explain the terse variable naming scheme in test_class_sh_property (as suggested by @rainwoodman)

* Also use parametrize for readonly, readwrite (as suggested by @rainwoodman)

* Apply change suggested by @Skylion007 (with clang-format).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants