Skip to content

Commit

Permalink
Add pybind_lifecycle and test example
Browse files Browse the repository at this point in the history
  • Loading branch information
EricCousineau-TRI committed Sep 18, 2024
1 parent 42729b0 commit cfd1f53
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 0 deletions.
16 changes: 16 additions & 0 deletions tmp/BUILD.bazel
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",
],
)
79 changes: 79 additions & 0 deletions tmp/pybind_lifecycle_py.cc
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
88 changes: 88 additions & 0 deletions tmp/test/pybind_lifecycle_test.py
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()

0 comments on commit cfd1f53

Please sign in to comment.