python: fix nanobind Network reference leak via GC type slots
The Python Network holds each PyNode's callable, forming an uncollectable instance -> callable -> globals() -> instance cycle that tripped nanobind's leak check at interpreter shutdown. Implement tp_traverse/tp_clear type slots on the Network binding so Python's cyclic collector can see through the C++-held callables and break the cycle. PyNode exposes its callable; PyNetwork visits and clears them. Wired into both binding sites (auto_bind and the legacy register_py_network).
This commit is contained in:
parent
949c8134ef
commit
c4538f03ca
@ -247,7 +247,7 @@ void bind_network(nb::module_& m) {
|
|||||||
|
|
||||||
nb::class_<IVariantNode<Variant>>(m, "INode");
|
nb::class_<IVariantNode<Variant>>(m, "INode");
|
||||||
|
|
||||||
nb::class_<Net>(m, "Network")
|
nb::class_<Net>(m, "Network", nb::type_slots(network_type_slots<Variant>()))
|
||||||
.def("__init__", [](Net* self) {
|
.def("__init__", [](Net* self) {
|
||||||
new (self) Net();
|
new (self) Net();
|
||||||
register_all_converters<Registry>(*self);
|
register_all_converters<Registry>(*self);
|
||||||
|
|||||||
@ -35,6 +35,16 @@ public:
|
|||||||
using VNode = IVariantNode<Variant>;
|
using VNode = IVariantNode<Variant>;
|
||||||
using VChannel = IVariantChannel<Variant>;
|
using VChannel = IVariantChannel<Variant>;
|
||||||
|
|
||||||
|
// ── GC support ────────────────────────────────────────────────────────────
|
||||||
|
// Visit every Python object this network transitively holds (currently the
|
||||||
|
// callable of each PyNode). Used by the Network type's tp_traverse slot so
|
||||||
|
// Python's cyclic GC can discover instance → callable → globals() cycles.
|
||||||
|
// Defined out-of-line below, once PyNode is a complete type.
|
||||||
|
template<typename Fn>
|
||||||
|
void visit_python_objects(Fn&& visit) const;
|
||||||
|
// Drop all Python references held by nodes, breaking any cycle (tp_clear).
|
||||||
|
void clear_python_objects();
|
||||||
|
|
||||||
// ── Builder API ───────────────────────────────────────────────────────────
|
// ── Builder API ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
void add(std::string name, std::shared_ptr<VNode> node) {
|
void add(std::string name, std::shared_ptr<VNode> node) {
|
||||||
@ -367,6 +377,14 @@ public:
|
|||||||
out_channels_[i] = std::move(ch);
|
out_channels_[i] = std::move(ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── GC support (tp_traverse / tp_clear on the owning Network) ──────────────
|
||||||
|
// The node holds a Python callable, which typically forms an
|
||||||
|
// instance → callable → globals() → instance cycle. Expose the callable so
|
||||||
|
// the Network's GC slots can traverse and clear it. See bindings.hpp's
|
||||||
|
// network_tp_traverse/network_tp_clear.
|
||||||
|
const nb::object& python_callable() const { return callable_; }
|
||||||
|
void clear_python_callable() { callable_ = nb::object(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void run_loop() {
|
void run_loop() {
|
||||||
while (!stop_flag_.load(std::memory_order_relaxed)) {
|
while (!stop_flag_.load(std::memory_order_relaxed)) {
|
||||||
@ -439,6 +457,60 @@ private:
|
|||||||
NodeStats stats_;
|
NodeStats stats_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── PyNetwork GC helpers (defined here: PyNode is now complete) ────────────────
|
||||||
|
|
||||||
|
template<typename Variant>
|
||||||
|
template<typename Fn>
|
||||||
|
void PyNetwork<Variant>::visit_python_objects(Fn&& visit) const {
|
||||||
|
for (const auto& [name, node] : nodes_)
|
||||||
|
if (auto* py = dynamic_cast<const PyNode<Variant>*>(node.get()))
|
||||||
|
visit(py->python_callable());
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Variant>
|
||||||
|
void PyNetwork<Variant>::clear_python_objects() {
|
||||||
|
for (auto& [name, node] : nodes_)
|
||||||
|
if (auto* py = dynamic_cast<PyNode<Variant>*>(node.get()))
|
||||||
|
py->clear_python_callable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GC type slots for the Network binding ─────────────────────────────────────
|
||||||
|
// The Network holds Python callables (via PyNode), forming uncollectable
|
||||||
|
// instance → callable → globals() → instance cycles at interpreter shutdown.
|
||||||
|
// These slots let Python's cyclic collector traverse and break them, silencing
|
||||||
|
// nanobind's leak warnings. See the nanobind "Reference leaks" documentation.
|
||||||
|
|
||||||
|
template<typename Variant>
|
||||||
|
int network_tp_traverse(PyObject* self, visitproc visit, void* arg) {
|
||||||
|
Py_VISIT(Py_TYPE(self));
|
||||||
|
if (!nb::inst_ready(self))
|
||||||
|
return 0;
|
||||||
|
auto* net = nb::inst_ptr<PyNetwork<Variant>>(self);
|
||||||
|
int rv = 0;
|
||||||
|
net->visit_python_objects([&](const nb::object& obj) {
|
||||||
|
if (rv == 0 && obj.is_valid())
|
||||||
|
rv = visit(obj.ptr(), arg);
|
||||||
|
});
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Variant>
|
||||||
|
int network_tp_clear(PyObject* self) {
|
||||||
|
auto* net = nb::inst_ptr<PyNetwork<Variant>>(self);
|
||||||
|
net->clear_python_objects();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Variant>
|
||||||
|
PyType_Slot* network_type_slots() {
|
||||||
|
static PyType_Slot slots[] = {
|
||||||
|
{ Py_tp_traverse, reinterpret_cast<void*>(&network_tp_traverse<Variant>) },
|
||||||
|
{ Py_tp_clear, reinterpret_cast<void*>(&network_tp_clear<Variant>) },
|
||||||
|
{ 0, nullptr }
|
||||||
|
};
|
||||||
|
return slots;
|
||||||
|
}
|
||||||
|
|
||||||
// ── register_py_network (legacy helper) ───────────────────────────────────────
|
// ── register_py_network (legacy helper) ───────────────────────────────────────
|
||||||
// Registers PyNetwork<Variant> with the given nanobind module.
|
// Registers PyNetwork<Variant> with the given nanobind module.
|
||||||
// Prefer bind_network<Registry> from auto_bind.hpp for new code.
|
// Prefer bind_network<Registry> from auto_bind.hpp for new code.
|
||||||
@ -447,7 +519,7 @@ template<typename Variant>
|
|||||||
void register_py_network(nb::module_& m, const char* class_name = "Network") {
|
void register_py_network(nb::module_& m, const char* class_name = "Network") {
|
||||||
using Net = PyNetwork<Variant>;
|
using Net = PyNetwork<Variant>;
|
||||||
|
|
||||||
nb::class_<Net>(m, class_name)
|
nb::class_<Net>(m, class_name, nb::type_slots(network_type_slots<Variant>()))
|
||||||
.def(nb::init<>())
|
.def(nb::init<>())
|
||||||
.def("connect", &Net::connect,
|
.def("connect", &Net::connect,
|
||||||
nb::arg("src"), nb::arg("out_idx"),
|
nb::arg("src"), nb::arg("out_idx"),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user