Skip to content

Detect and fail if using mismatched holders #2644

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

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions tests/test_smart_ptr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ PYBIND11_DECLARE_HOLDER_TYPE(T, std::shared_ptr<T>);
// holder size to trigger the non-simple-layout internal instance layout for single inheritance with
// large holder type:
template <typename T> class huge_unique_ptr {
std::unique_ptr<T> ptr;
uint64_t padding[10];
std::unique_ptr<T> ptr;
public:
huge_unique_ptr(T *p) : ptr(p) {};
T *get() { return ptr.get(); }
T *get() const { return ptr.get(); }
};
PYBIND11_DECLARE_HOLDER_TYPE(T, huge_unique_ptr<T>);

Expand Down Expand Up @@ -330,12 +330,6 @@ TEST_SUBMODULE(smart_ptr, m) {
.def_readwrite("value", &TypeForMoveOnlyHolderWithAddressOf::value)
.def("print_object", [](const TypeForMoveOnlyHolderWithAddressOf *obj) { py::print(obj->toString()); });

// test_smart_ptr_from_default
struct HeldByDefaultHolder { };
py::class_<HeldByDefaultHolder>(m, "HeldByDefaultHolder")
.def(py::init<>())
.def_static("load_shared_ptr", [](std::shared_ptr<HeldByDefaultHolder>) {});

// test_shared_ptr_gc
// #187: issue involving std::shared_ptr<> return value policy & garbage collection
struct ElementBase {
Expand Down Expand Up @@ -367,4 +361,32 @@ TEST_SUBMODULE(smart_ptr, m) {
list.append(py::cast(e));
return list;
});

// test_holder_mismatch
// Tests the detection of trying to use mismatched holder types around the same instance type
struct HeldByShared {};
struct HeldByUnique {};
// HeldByShared declared with shared_ptr holder, but used with unique_ptr later
py::class_<HeldByShared, std::shared_ptr<HeldByShared>>(m, "HeldByShared");
m.def("register_mismatch_return", [](py::module m) {
Copy link
Collaborator

@EricCousineau-TRI EricCousineau-TRI Dec 22, 2020

Choose a reason for hiding this comment

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

My brief reading of this PR leads me to believe that there's a sharp edge: a user could still hit a segfault if they call py::cast<mismatched_holder_type>(my_object). Totes get the idea of wanting to shift overhead from py::cast<>() (runtime) to function declarations (binding-time, e.g. .def()), but it now means there are now distinct entry points to this type-checking that users can trigger :(

Possible resolutions:

  • if it's worth the risk, then just mention it in the holder / smart pointer docs (as a diff in this PR)
  • If it's not worth the risk, somehow enable raw py::cast<>() to do a runtime check. This may make for some crazy dumb plumbing, though :(
  • Alternatively, eat the cost of runtime casts and funnel it through type_caster<>. That's what we do for our fork (RobotLocomotion/pybind11), and we haven't yet had to point fingers at performance there (though our use case may be simpler).

@YannickJadoul or @rwgk Any chance y'all have a good (and mebbe easy?) timing performance benchmark for this, to see what the risk is in these terms?

EDIT: Hm... for docs/benchmark.py, it's only for compilation time and size, and doesn't really dip into the more nuanced things (e.g. inheritance, custom type casters).

Copy link
Collaborator

Choose a reason for hiding this comment

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

@EricCousineau-TRI wrote:

Any chance y'all have a good (and mebbe easy?) timing performance benchmark for this, to see what the risk is in these terms?

The TensorFlow core team has very sophisticated pybind11 benchmarks, but it's currently Google-internal only. The author already gave me permission to extract most of it for external view, including sources, but it may take me a few days (I want to show what I extract to the author for approval).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sweet!!! That'd be awesome! I'll see if I can gather some common "complex" patterns in Drake to see if I can get some patterns we have (but maintain feature-parity with upstream).

Have you thought any about where your benchmarks might live? I think the ones I'd generate would be simple enough (i.e. just pybind11 bits) to be in a benchmarking subdirectory.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I explicitly converted my previous code doing compatibility checks at cast time to this code, doing these tests at definition time only, because of efficiency concerns raised elsewhere.

// Fails: the class was already registered with a shared_ptr holder
m.def("bad1", []() { return std::unique_ptr<HeldByShared>(new HeldByShared()); });
});
m.def("register_mismatch_class", [](py::module m) {
// Fails: the class was already registered with a shared_ptr holder
py::class_<HeldByShared, std::unique_ptr<HeldByShared>>(m, "bad");
});

// HeldByUnique declared with unique_ptr holder, but used with shared_ptr before / later
m.def("register_return_shared", [](py::module m) {
// Fails if HeldByUnique is not yet registered or, if registered, due to mismatching holder
m.def("bad2", []() { return std::make_shared<HeldByUnique>(); });
});
m.def("register_consume_shared", [](py::module m) {
// Fails if HeldByUnique is not yet registered or, if registered, due to mismatching holder
m.def("bad3", [](std::shared_ptr<HeldByUnique>) {});
});
m.def("register_HeldByUnique", [](py::module m) {
py::class_<HeldByUnique>(m, "HeldByUnique");
});
}