-
Notifications
You must be signed in to change notification settings - Fork 153
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit adds support for object hierarchies with builtin reference counting. This provides an alternative to STL's `std::unique_ptr<>` / `std::shared_ptr<>` that is more general *and* more efficient in binding code.
- Loading branch information
Showing
17 changed files
with
622 additions
and
12 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
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
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,74 @@ | ||
# Intrusive reference counting | ||
|
||
Like _pybind11_, _nanobind_ provides a way of binding classes with builtin | ||
("intrusive") reference counting. This is the most general and cheapest way of | ||
handling shared ownership between C++ and Python, but it requires that the base | ||
class of an object hierarchy is adapted according to the needs of _nanobind_. | ||
|
||
Ordinarily, a simple class with intrusive reference counting might look as | ||
follows: | ||
|
||
```cpp | ||
class Object { | ||
public: | ||
void inc_ref() const { ++m_ref_count; } | ||
|
||
void dec_ref() const { | ||
if (--m_ref_count == 0) | ||
delete this; | ||
} | ||
|
||
private: | ||
mutable std::atomic<size_t> m_ref_count { 0 }; | ||
}; | ||
``` | ||
|
||
The advantage of this over standard approaches like `std::shared_ptr<T>` is | ||
that no separate control block must be allocated. Subtle technical band-aids | ||
like `std::enable_shared_from_this<T>` to avoid undefined behavior are also | ||
no longer necessary. | ||
|
||
However, one issue that tends to arise when a type like `Object` is wrapped | ||
using _nanobind_ is that there are now *two* separate reference counts | ||
referring to the same object: one in Python's `PyObject`, and one in `Object`. | ||
This can lead to a problematic reference cycle: | ||
|
||
- Python's `PyObject` needs to keep `Object` alive so that the instance can be | ||
safely passed to C++ functions. | ||
|
||
- The C++ `Object` may in turn need to keep the `PyObject` alive. This is the | ||
case when a subclass uses `NB_TRAMPOLINE` and `NB_OVERRIDE` features to route | ||
C++ virtual function calls back to a Python implementation. | ||
|
||
The source of the problem is that there are *two* separate counters that try to | ||
reason about the reference count of *one* instance. The solution is to reduce | ||
this to just one counter: | ||
|
||
- if an instance lives purely on the C++ side, the `m_ref_count` field is | ||
used to reason about the number of references. | ||
|
||
- The first time that an instance is exposed to Python (by being created from | ||
Python, or by being returned from a bound C++ function), lifetime management | ||
is delegated to Python. | ||
|
||
The files | ||
[`tests/object.h`](https://github.com/wjakob/nanobind/blob/master/tests/object.h) | ||
and | ||
[`tests/object.cpp`](https://github.com/wjakob/nanobind/blob/master/tests/object.cpp) | ||
contain an example implementation of a suitable base class named `Object`. It | ||
contains an extra optimization to use a single field of type | ||
`std::atomic<uintptr_t> m_state;` (8 bytes) to store *either* a reference | ||
counter or a pointer to a `PyObject*`. | ||
|
||
The main change in _nanobind_-based bindings is that the base class must | ||
specify a `nb::intrusive_ptr` annotation to inform an instance that lifetime | ||
management has been taken over by Python. This annotation is automatically | ||
inherited by all subclasses. | ||
|
||
```cpp | ||
nb::class_<Object>( | ||
m, "Object", | ||
nb::intrusive_ptr<Object>( | ||
[](Object *o, PyObject *po) { o->set_self_py(po); })); | ||
``` | ||
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
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
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
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
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
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
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
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
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
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,74 @@ | ||
#include "object.h" | ||
#include <stdexcept> | ||
|
||
static void (*object_inc_ref_py)(PyObject *) = nullptr; | ||
static void (*object_dec_ref_py)(PyObject *) = nullptr; | ||
|
||
void Object::inc_ref() const { | ||
uintptr_t value = m_state.load(std::memory_order_relaxed); | ||
|
||
while (true) { | ||
if (value & 1) { | ||
if (!m_state.compare_exchange_weak(value, | ||
value + 2, | ||
std::memory_order_relaxed, | ||
std::memory_order_relaxed)) | ||
continue; | ||
} else { | ||
object_inc_ref_py((PyObject *) value); | ||
} | ||
|
||
break; | ||
} | ||
} | ||
|
||
void Object::dec_ref() const { | ||
uintptr_t value = m_state.load(std::memory_order_relaxed); | ||
|
||
while (true) { | ||
if (value & 1) { | ||
if (value == 1) { | ||
throw std::runtime_error("Object::dec_ref(): reference count underflow!"); | ||
} else if (value == 3) { | ||
delete this; | ||
} else { | ||
if (!m_state.compare_exchange_weak(value, | ||
value - 2, | ||
std::memory_order_relaxed, | ||
std::memory_order_relaxed)) | ||
continue; | ||
} | ||
} else { | ||
object_dec_ref_py((PyObject *) value); | ||
} | ||
break; | ||
} | ||
} | ||
|
||
void Object::set_self_py(PyObject *o) { | ||
uintptr_t value = m_state.load(std::memory_order_relaxed); | ||
if (value & 1) { | ||
value >>= 1; | ||
for (uintptr_t i = 0; i < value; ++i) | ||
object_inc_ref_py(o); | ||
|
||
m_state.store((uintptr_t) o); | ||
} else { | ||
throw std::runtime_error("Object::set_self_py(): a Python object was already present!"); | ||
} | ||
value = m_state.load(std::memory_order_relaxed); | ||
} | ||
|
||
PyObject *Object::self_py() const { | ||
uintptr_t value = m_state.load(std::memory_order_relaxed); | ||
if (value & 1) | ||
return nullptr; | ||
else | ||
return (PyObject *) value; | ||
} | ||
|
||
void object_init_py(void (*object_inc_ref_py_)(PyObject *), | ||
void (*object_dec_ref_py_)(PyObject *)) { | ||
object_inc_ref_py = object_inc_ref_py_; | ||
object_dec_ref_py = object_dec_ref_py_; | ||
} |
Oops, something went wrong.