Skip to content

Commit

Permalink
Officially support intrusive reference counting sample implementation
Browse files Browse the repository at this point in the history
This commit turns what was previously just an example implementation of
intrusive reference counting for testing into an officially supported
feature that can now be accessed through 3 headers:

- ``include/nanobind/intrusive/counter.h``: Declarations
- ``include/nanobind/intrusive/counter.inl``: Implementation
- ``include/nanobind/intrusive/ref.h``: Reference counting RAII helper
  class and Python bindings

The documentation was updated to explain how to use these classes.

This commit does not create a dependence on this particular
implementation of intrusive reference counting---existing code should
remain unaffected.
  • Loading branch information
wjakob committed Oct 2, 2023
1 parent 79e042e commit 58bc0d0
Show file tree
Hide file tree
Showing 12 changed files with 933 additions and 337 deletions.
243 changes: 242 additions & 1 deletion docs/api_extra.rst
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ section <ndarrays>`.
returns different array types could call it to convert ``ndarray<T>`` to
``ndarray<>``. When adding constraints, the constructor is only safe to
use following a runtime check to ensure that newly created array actually
possesses the advertised properties.
possesses the advertised properties.

.. cpp:function:: ndarray(const ndarray &)

Expand Down Expand Up @@ -944,3 +944,244 @@ section <utilities_eval>`.
.. cpp:function:: inline void exec(const str &expr, handle global = handle(), handle local = handle())

Execute the given Python code in the given global/local scopes.

Intrusive reference counting helpers
------------------------------------

The following functions and classes can be used to augment user-provided
classes with intrusive reference counting that greatly simplifies shared
ownership in larger C++/Python binding projects.

This functionality requires the following include directives:

.. code-block:: cpp
#include <nanobind/intrusive/counter.h>
#include <nanobind/intrusive/ref.h>
These headers reference several functions, whose implementation must be
provided. You can do so by including the following file from a single ``.cpp``
file of your project:

.. code-block:: cpp
#include <nanobind/intrusive/counter.inl>
The functionality in these files consist of the following classes and
functions:

.. cpp:class:: IntrusiveCounter

Simple atomic reference counter that can optionally switch over to
Python-based reference counting.

The various copy/move assignment/constructors intentionally don't transfer
the reference count. This is so that the contents of classes containing an
``IntrusiveCounter`` can be copied/moved without disturbing the reference
counts of the associated instances.

.. cpp:function:: IntrusiveCounter() noexcept = default

Initialize with a reference count of zero.

.. cpp:function:: IntrusiveCounter(const IntrusiveCounter &o)

Copy constructor, which produces a zero-initialized counter.
Does *not* copy the reference count from `o`.

.. cpp:function:: IntrusiveCounter(IntrusiveCounter &&o)

Move constructor, which produces a zero-initialized counter.
Does *not* copy the reference count from `o`.

.. cpp:function:: IntrusiveCounter &operator=(const IntrusiveCounter &o)

Copy assignment operator. Does *not* copy the reference count from `o`.

.. cpp:function:: IntrusiveCounter &operator=(IntrusiveCounter &&o)

Move assignment operator. Does *not* copy the reference count from `o`.

.. cpp:function:: void inc_ref() const noexcept

Increase the reference count. When the counter references an object
managed by Python, the operation calls ``Py_INCREF()`` to increase
the reference count of the Python object instead.

The :cpp:func:`inc_ref() <nanobind::inc_ref>` top-level function
encapsulates this logic for subclasses of :cpp:class:`IntrusiveBase`.

.. cpp:function:: bool dec_ref() const noexcept

Decrease the reference count. When the counter references an object
managed by Python, the operation calls ``Py_DECREF()`` to decrease
the reference count of the Python object instead.

When the C++-managed reference count reaches zero, the operation returns
``true`` to signal to the caller that it should use a *delete expression*
to destroy the instance.

The :cpp:func:`dec_ref() <nanobind::dec_ref>` top-level function
encapsulates this logic for subclasses of :cpp:class:`IntrusiveBase`.

.. cpp:function:: void set_self_py(PyObject * self)

Set the Python object associated with this instance. This operation
is usually called by nanobind when ownership is transferred to the
Python side.

Any references from prior calls to
:cpp:func:`IntrusiveCounter::inc_ref()` are converted into Python
references by calling ``Py_INCREF()`` repeatedly.

.. cpp:function:: PyObject * self_py()

Return the Python object associated with this instance (or ``nullptr``).

.. cpp:class:: IntrusiveBase

Simple polymorphic base class for a intrusively reference-counted object
hierarchy. The member functions expose corresponding functionality of
:cpp:class:`IntrusiveCounter`.

.. cpp:function:: void inc_ref() const noexcept

See :cpp:func:`IntrusiveCounter::inc_ref()`.

.. cpp:function:: bool dec_ref() const noexcept

See :cpp:func:`IntrusiveCounter::dec_ref()`.

.. cpp:function:: void set_self_py(PyObject * self)

See :cpp:func:`IntrusiveCounter::set_self_py()`.

.. cpp:function:: PyObject * self_py()

See :cpp:func:`IntrusiveCounter::self_py()`.

.. cpp:function:: void intrusive_init(void (* intrusive_inc_ref_py)(PyObject * ) noexcept, void (* intrusive_dec_ref_py)(PyObject * ) noexcept)

Function to register reference counting hooks with the intrusive reference
counter class. This allows its implementation to not depend on Python.

You would usually call this function as follows from the initialization
routine of a Python extension:

.. code-block:: cpp
NB_MODULE(my_ext, m) {
intrusive_init(
[](PyObject * o) noexcept {
nb::gil_scoped_acquire guard;
Py_INCREF(o);
},
[](PyObject * o) noexcept {
nb::gil_scoped_acquire guard;
Py_DECREF(o);
});
// ...
}
.. cpp:function:: inline void inc_ref(IntrusiveBase * o) noexcept

Reference counting helper function that calls ``o->inc_ref()`` if ``o`` is
not equal to ``nullptr``.

.. cpp:function:: inline void dec_ref(IntrusiveBase * o) noexcept

Reference counting helper function that calls ``o->dec_ref()`` if ``o`` is
not equal to ``nullptr`` and ``delete o`` when the reference count reaches
zero.

.. cpp:class:: template <typename T> ref

RAII scoped reference counting helper class

:cpp:class:`ref\<T\> <ref>` is a simple RAII wrapper class that encapsulates a
pointer to an instance with intrusive reference counting.

It takes care of increasing and decreasing the reference count as needed and
deleting the instance when the count reaches zero.

For this to work, compatible functions :cpp:func:`inc_ref()` and
:cpp:func:`dec_ref()` must be defined before including the file
``nanobind/intrusive/ref.h``. Default implementations for subclasses of the
type :cpp:class:`IntrusiveBase` are already provided as part of the file
``counter.h``.

.. cpp:function:: ref() = default

Create a null reference

.. cpp:function:: ref(T * ptr)

Create a reference from a pointer. Increases the reference count of the
object (if not ``nullptr``).

.. cpp:function:: ref(const ref &r)

Copy a reference. Increase the reference count of the object (if not
``nullptr``).

.. cpp:function:: ref(ref &&r) noexcept

Move a reference. Object reference counts are unaffected by this operation.

.. cpp:function:: ~ref()

Destroy a reference. Decreases the reference count of the object (if not
``nullptr``).

.. cpp:function:: ref& operator=(ref &&r) noexcept

Move-assign another reference into this one.

.. cpp:function:: ref& operator=(const ref &r)

Copy-assign another reference into this one.

.. cpp:function:: ref& operator=(const T * ptr)

Overwrite this reference with a pointer to another object

.. cpp:function:: bool operator==(const ref &r) const

Compare this reference with another reference (pointer equality)

.. cpp:function:: bool operator!=(const ref &r) const

Compare this reference with another reference (pointer inequality)

.. cpp:function:: bool operator==(const T * ptr) const

Compare this reference with another object (pointer equality)

.. cpp:function:: bool operator!=(const T * ptr) const

Compare this reference with another object (pointer inequality)

.. cpp:function:: T * operator->()

Access the object referenced by this reference

.. cpp:function:: const T * operator->() const

Access the object referenced by this reference (const version)

.. cpp:function:: T& operator*()

Return a C++ reference to the referenced object

.. cpp:function:: const T& operator*() const

Return a C++ reference to the referenced object (const version)

.. cpp:function:: T* get()

Return a C++ pointer to the referenced object

.. cpp:function:: const T* get() const

Return a C++ pointer to the referenced object (const version)
32 changes: 25 additions & 7 deletions docs/ownership.rst
Original file line number Diff line number Diff line change
Expand Up @@ -364,19 +364,37 @@ Intrusive reference counting is the most flexible and efficient way of handling
shared ownership. The main downside is that you must adapt the base class of
your object hierarchy to the needs of nanobind.

The core idea is to define base class (e.g. ``Object``) that is common to all bound
types requiring shared ownership. That class contains a builtin
atomic counter to keep track of the number of outstanding references.
The core idea is to define base class (e.g. ``Object``) common to all bound
types requiring shared ownership. That class contains a builtin atomic counter
(e.g., ``m_ref_count``) and a Python object pointer (e.g., ``m_py_object``).

.. code-block:: cpp
class Object {
...
private:
mutable std::atomic<size_t> m_ref_count { 0 };
PyObject *m_py_object = nullptr;
};
With a few extra modifications, nanobind can unify this reference count so that
it accounts for references in both languages. Please see the :ref:`detailed
section on intrusive reference counting <intrusive>` for a concrete example on
how to set this up.
The core idea is that such ``Object`` instances can either be managed by C++ or
Python. In the former case, the ``m_ref_count`` field keeps track of the number
of outstanding references. In the latter case, reference counting is handled by
Python, and the ``m_ref_count`` field remains unused.

This is actually little wasteful---nanobind therefore ships with a more
efficient reference counter sample implementation that supports both use cases
while requiring only ``sizeof(void*)`` bytes of storage:

.. code-block:: cpp
#include <nanobind/intrusive/counter.h>
class Object {
...
private:
IntrusiveCounter m_ref_count;
};
Please read the dedicated :ref:`section on intrusive reference counting
<intrusive>` for more details on how to set this up.
Loading

0 comments on commit 58bc0d0

Please sign in to comment.