-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add pybind_lifecycle and test example
- Loading branch information
1 parent
42729b0
commit cfd1f53
Showing
3 changed files
with
183 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
load("//tools/skylark:pybind.bzl", "pybind_py_library") | ||
|
||
pybind_py_library( | ||
name = "pybind_lifecycle_py", | ||
cc_so_name = "pybind_lifecycle", | ||
cc_srcs = ["pybind_lifecycle_py.cc"], | ||
) | ||
|
||
py_test( | ||
name = "pybind_lifecycle_test", | ||
srcs = ["test/pybind_lifecycle_test.py"], | ||
deps = [ | ||
":pybind_lifecycle_py", | ||
"//bindings/pydrake", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/** | ||
Manual lifecycle management. For more information, see | ||
https://github.com/RobotLocomotion/drake/issues/14387 | ||
and accompanying `pybind_lifecycle_test.py`. | ||
*/ | ||
|
||
#include <vector> | ||
|
||
#include <pybind11/pybind11.h> | ||
|
||
namespace py = pybind11; | ||
|
||
namespace drake { | ||
namespace { | ||
|
||
using PatientList = std::vector<PyObject*>; | ||
|
||
/** | ||
Returns pointer to patients for an object, if they exist. | ||
Returns null otherwise. | ||
*/ | ||
PatientList* GetPatients(PyObject* obj) { | ||
auto* instance = reinterpret_cast<py::detail::instance *>(obj); | ||
auto& patients_map = py::detail::get_internals().patients; | ||
auto pos = patients_map.find(obj); | ||
if (pos == patients_map.end()) { | ||
return nullptr; | ||
} | ||
if (!instance->has_patients) { | ||
return nullptr; | ||
} | ||
return &pos->second; | ||
} | ||
|
||
/** pybind wrapper of the above. */ | ||
py::list GetPatientsPy(py::object obj) { | ||
py::list out; | ||
PatientList* patients = GetPatients(obj.ptr()); | ||
if (patients) { | ||
for (PyObject* patient : *patients) { | ||
out.append(py::handle(patient)); | ||
} | ||
} | ||
return out; | ||
} | ||
|
||
void ClearPatientsImpl(PatientList* patients) { | ||
for (PyObject *patient : *patients) { | ||
Py_CLEAR(patient); | ||
} | ||
patients->clear(); | ||
} | ||
|
||
/** | ||
Like pybind11::detail::clear_patients(); however: | ||
- This simply "resets" patients. | ||
- This will skip any Python object that is either not a pybind-registered | ||
object or does not have any lifesupport enabled (whereas the original will | ||
fail fast). | ||
WARNING: This is for *expert* users only. If you are unsure what this does, do | ||
not change calls to this function. You may cause segfaults. If you do, you may | ||
be on your own. | ||
*/ | ||
void ClearPatients(py::handle obj) { | ||
auto* patients = GetPatients(obj.ptr()); | ||
if (!patients) { | ||
return; | ||
} | ||
ClearPatientsImpl(patients); | ||
} | ||
|
||
PYBIND11_MODULE(pybind_lifecycle, m) { | ||
m.def("GetPatients", &GetPatientsPy, py::arg("obj")); | ||
m.def("ClearPatients", &ClearPatients, py::arg("obj")); | ||
} | ||
|
||
} // namespace | ||
} // namespace drake |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
""" | ||
Tests for pybind lifecycle issues. | ||
DO NOT USE unless you acknowledge that this may cause segfaults that you are | ||
responsible for debugging. | ||
""" | ||
|
||
import gc | ||
import unittest | ||
import weakref | ||
|
||
from pydrake.systems.framework import DiagramBuilder, LeafSystem | ||
|
||
from drake.tmp.pybind_lifecycle import ClearPatients, GetPatients | ||
|
||
|
||
def make_diagram(): | ||
|
||
def inner(): | ||
builder = DiagramBuilder() | ||
system = LeafSystem() | ||
system.set_name("system") | ||
builder.AddSystem(system) | ||
diagram = builder.Build() | ||
# Return references so that things should go out of scope normally, but | ||
# will not due to reference cycles. | ||
# The references cycles produced here are mentioned in the following | ||
# Anzu issue; see the issue for linked Drake isues: | ||
# https://github.shared-services.aws.tri.global/robotics/anzu/issues/13065 | ||
# Note: The weakref's here are generally *only* for testing purposes. | ||
# If your code has direct control over the lifetime, you should not | ||
# use weakref's. | ||
return weakref.ref(builder), weakref.ref(diagram), weakref.ref(system) | ||
|
||
refs = inner() | ||
# Garbage collect to prove cycles keep items alive. | ||
gc.collect() | ||
return refs | ||
|
||
|
||
class Test(unittest.TestCase): | ||
def assertListIs(self, a, b): | ||
a_ids = [id(a_i) for a_i in a] | ||
b_ids = [id(b_i) for b_i in b] | ||
self.assertEqual(a_ids, b_ids, f"{a} != {b}") | ||
|
||
def test_make_diagram(self): | ||
builder_ref, diagram_ref, system_ref = make_diagram() | ||
# Dereference weakref's. | ||
builder = builder_ref() | ||
diagram = diagram_ref() | ||
system = system_ref() | ||
# Reference cycle prevents objects from being GC'd. | ||
self.assertIsNotNone(builder) | ||
self.assertIsNotNone(diagram) | ||
self.assertIsNotNone(system) | ||
|
||
def test_get_patients(self): | ||
builder_ref, diagram_ref, system_ref = make_diagram() | ||
# Dereference weakref's. | ||
builder = builder_ref() | ||
diagram = diagram_ref() | ||
system = system_ref() | ||
# As shown here, the cycle is formed between `builder` and `system`. | ||
self.assertListIs(GetPatients(builder), [system, diagram]) | ||
self.assertListIs(GetPatients(diagram), []) | ||
self.assertListIs(GetPatients(system), [builder]) | ||
|
||
def test_clear_patients(self): | ||
builder_ref, diagram_ref, system_ref = make_diagram() | ||
# Clear cycles for builder. | ||
# WARNING: This may cause use-after-free errors. Use this with caution! | ||
# Notes: | ||
# - Depending on how your code operates, and what accessor you use, | ||
# you may need to clear patients on other objects as well. | ||
# - It be difficult to free everything if you have sub-builders / | ||
# diagrams that are constructed in Python. | ||
# - Lifetime cycles may not occur if `builder.Build()` is called in | ||
# C++. | ||
ClearPatients(builder_ref()) | ||
# Now objects are GC'd. | ||
self.assertIsNone(builder_ref()) | ||
self.assertIsNone(diagram_ref()) | ||
self.assertIsNone(system_ref()) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |