diff --git a/docs/api_extra.rst b/docs/api_extra.rst index e71d049d1..2769b3d09 100644 --- a/docs/api_extra.rst +++ b/docs/api_extra.rst @@ -441,7 +441,7 @@ section `. returns different array types could call it to convert ``ndarray`` 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 &) @@ -944,3 +944,244 @@ section `. .. 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 + #include + +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 + +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() ` 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() ` 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 ref + + RAII scoped reference counting helper class + + :cpp:class:`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) diff --git a/docs/ownership.rst b/docs/ownership.rst index 600853e55..d62119340 100644 --- a/docs/ownership.rst +++ b/docs/ownership.rst @@ -364,9 +364,9 @@ 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 @@ -374,9 +374,27 @@ atomic counter to keep track of the number of outstanding references. ... private: mutable std::atomic 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 ` 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 + + class Object { + ... + private: + IntrusiveCounter m_ref_count; + }; + +Please read the dedicated :ref:`section on intrusive reference counting +` for more details on how to set this up. diff --git a/docs/ownership_adv.rst b/docs/ownership_adv.rst index 237069628..ca367bc23 100644 --- a/docs/ownership_adv.rst +++ b/docs/ownership_adv.rst @@ -14,10 +14,10 @@ pointer conversion is implemented in nanobind. Intrusive reference counting ---------------------------- -nanobind provides a custom intrusive reference counting solution that is -both efficient and also completely solves the issue of shared C++/Python -object ownership. It addresses :ref:`annoying corner cases -` that exist with shared pointers. +nanobind provides a custom intrusive reference counting solution that +completely solves the issue of shared C++/Python object ownership, while +avoiding the overheads and complexities of traditional C++ shared pointers +(``std::shared_ptr``). The main limitation is that it requires adapting the base class of an object hierarchy according to the needs of nanobind, which may not always be possible. @@ -80,29 +80,131 @@ We can solve the problem by using just one counter: created from Python, or by being returned from a bound C++ function), lifetime management switches over to Python. -The files `tests/object.h -`_ and -`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`` -(8 bytes) to store *either* a reference counter or a pointer to a -``PyObject*``. The example class is designed to work even when used in a -context where Python is not available. +The file `nanobind/intrusive/counter.h +`_ +includes an official sample implementation of this functionality. It contains an extra optimization to pack *either* +a reference counter or a pointer to a ``PyObject*`` into a single +``sizeof(void*)``-sized field. + +The most basic interface, :cpp:class:`IntrusiveCounter` represents an atomic +counter that can be increased (via :cpp:func:`IntrusiveCounter::inc_ref()`) or +decreased (via :cpp:func:`IntrusiveCounter::dec_ref()`). When the counter +reaches zero, the object should be deleted, which ``dec_ref()`` indicates by +returning ``true``. + +In addition to this simple counting mechanism, ownership of the object can also +be transferred to Python (via :cpp:func:`IntrusiveCounter::set_self_py()`). In +this case, subsequent calls to ``inc_ref()`` and ``dec_ref()`` modify the +reference count of the underlying Python object. + +To incorporate intrusive reference counting into your own project, you would +usually add an :cpp:class:`IntrusiveCounter`-typed member to the base class of an object +hierarchy and expose it as follows: + +.. code-block:: cpp + + #include + + class Object { + public: + void inc_ref() noexcept { m_ref_count.inc_ref(); } + bool dec_ref() noexcept { return m_ref_count.dec_ref(); } + + // Important: must declare virtual destructor + virtual ~Object() = default; + + void set_self_py(PyObject *self) noexcept { + m_ref_count.set_self_py(self); + } + + private: + IntrusiveCounter m_ref_count; + }; + + // Convenience function for increasing the reference count of an instance + inline void inc_ref(Object *o) noexcept { + if (o) + o->inc_ref(); + } + + // Convenience function for decreasing the reference count of an instance + // and potentially deleting it when the count reaches zero + inline void dec_ref(Object *o) noexcept { + if (o && o->dec_ref()) + delete o; + } + +Alternatively, you could also inherit from :cpp:class:`IntrusiveBase`, which +obviates the need for all of the above declarations: + +.. code-block:: cpp + + class Object : public IntrusiveBase { + public: + // ... + }; The main change in the bindings is that the base class must specify a :cpp:class:`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. In the linked example, this is done via the ``Object::set_self_py()`` method that we can now call from the class -binding annotation. +binding annotation: .. code-block:: cpp nb::class_( - m, "Object", - nb::intrusive_ptr( - [](Object *o, PyObject *po) noexcept { o->set_self_py(po); })); + m, "Object", + nb::intrusive_ptr( + [](Object *o, PyObject *po) noexcept { o->set_self_py(po); })); + +Also, somewhere in your binding initialization code, you must register Python +reference counting hooks with the intrusive reference counter class. This +allows its implementation of the code in ``nanobind/intrusive/counter.h`` to +*not* depend on Python (this means that it can be used in projects where Python +bindings are an optional component). + +.. code-block:: cpp + + intrusive_init( + [](PyObject *o) noexcept { + nb::gil_scoped_acquire guard; + Py_INCREF(o); + }, + [](PyObject *o) noexcept { + nb::gil_scoped_acquire guard; + Py_DECREF(o); + }); + +These ``counter.h`` include file references several functions that must be +compiled somewhere inside the project, which can be accomplished by including +the following file from a single ``.cpp`` file. + +.. code-block:: cpp + + #include + +Having to call :cpp:func:`inc_ref()` and :cpp:func:`dec_ref()` many times to +perform manual reference counting in project code can quickly become tedious. +Nanobind also ships with a :cpp:class:`ref\ ` RAII helper class to +help with this. + +.. code-block:: cpp + + #include + + void foo() { + /// Assignment to ref automatically increases the object's reference count + ref x = new MyObject(); + + // ref can be used like a normal pointer + x->func(); + + } // <-- ref::~ref() calls dec_ref(), which deletes the now-unreferenced instance + +When the file ``nanobind/intrusive/ref.h`` is included following +``nanobind/nanobind.h``, it also exposes a custom type caster to bind functions +taking or returning ``ref``-typed values. That's it. If you use this approach, any potential issues involving shared pointers, return value policies, reference leaks with trampolines, etc., can diff --git a/include/nanobind/intrusive/counter.h b/include/nanobind/intrusive/counter.h new file mode 100644 index 000000000..f30298c33 --- /dev/null +++ b/include/nanobind/intrusive/counter.h @@ -0,0 +1,249 @@ +/* + nanobind/intrusive/counter.h: Intrusive reference counting sample + implementation. + + Intrusive reference counting is a simple solution for various lifetime and + ownership-related issues that can arise in Python bindings of C++ code. The + implementation here represents one of many ways in which intrusive + reference counting can be realized and is included for convenience. + + The code in this file is designed to be truly minimal: it depends neither + on Python, nanobind, nor the STL. This enables its use in small projects + with a 100% optional Python interface. + + Two section of nanobind's documentation discuss intrusive reference + counting in general: + + - https://nanobind.readthedocs.io/en/latest/ownership.html + - https://nanobind.readthedocs.io/en/latest/ownership_adv.html + + Comments below are specific to this sample implementation. + + Copyright (c) 2023 Wenzel Jakob + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#pragma once + +#include + +// Override this definition to specify DLL export/import declarations +#if !defined(NB_INTRUSIVE_EXPORT) +# define NB_INTRUSIVE_EXPORT +#endif + +#if !defined(Py_PYTHON_H) +/* While the implementation below does not directly depend on Python, the + PyObject type occurs in a few function interfaces (in a fully opaque + manner). The lines below forward-declare it. */ +extern "C" { + struct _object; + typedef _object PyObject; +}; +#endif + +/** \brief Simple intrusive reference counter. + * + * Intrusive reference counting is a simple solution for various lifetime and + * ownership-related issues that can arise in Python bindings of C++ code. The + * implementation here represents one of many ways in which intrusive reference + * counting can be realized and is included for convenience. + * + * The ``IntrusiveCounter`` class represents an atomic counter that can be + * increased (via ``inc_ref()``) or decreased (via ``dec_ref()``). When the + * counter reaches zero, the object should be deleted, which ``dec_ref()`` + * indicates by returning ``true``. + * + * In addition to this simple counting mechanism, ownership of the object can + * also be transferred to Python (via ``set_self_py()``). In this case, + * subsequent calls to ``inc_ref()`` and ``dec_ref()`` modify the reference + * count of the underlying Python object. The ``IntrusiveCounter`` class + * supports both cases using only ``sizeof(void*)`` bytes of storage. + * + * To incorporate intrusive reference counting into your own project, you would + * usually add an ``IntrusiveCounter``-typed member to the base class of an + * object hierarchy and expose it as follows: + * + * ```cpp + * #include + * + * class Object { + * public: + * void inc_ref() noexcept { m_ref_count.inc_ref(); } + * bool dec_ref() noexcept { return m_ref_count.dec_ref(); } + * + * // Important: must declare virtual destructor + * virtual ~Object() = default; + * + * void set_self_py(PyObject *self) noexcept { + * m_ref_count.set_self_py(self); + * } + * + * private: + * IntrusiveCounter m_ref_count; + * }; + * + * // Convenience function for increasing the reference count of an instance + * inline void inc_ref(Object *o) noexcept { + * if (o) + * o->inc_ref(); + * } + * + * // Convenience function for decreasing the reference count of an instance + * // and potentially deleting it when the count reaches zero + * inline void dec_ref(Object *o) noexcept { + * if (o && o->dec_ref()) + * delete o; + * } + * ``` + * + * Alternatively, you could also inherit from ``IntrusiveBase``, which obviates + * the need for all of the above declarations: + * + * ```cpp + * class Object : public IntrusiveBase { + * public: + * // ... + * }; + * ``` + * + * When binding the base class in Python, you must indicate to nanobind that + * this type uses intrusive reference counting and expose the ``set_self_py`` + * member. This must only be done once, as the attribute is automatically + * inherited by subclasses. + * + * ```cpp + * nb::class_( + * m, "Object", + * nb::intrusive_ptr( + * [](Object *o, PyObject *po) noexcept { o->set_self_py(po); })); + * ``` + * + * Also, somewhere in your binding initialization code, you must call + * + * ```cpp + * intrusive_init( + * [](PyObject *o) noexcept { + * nb::gil_scoped_acquire guard; + * Py_INCREF(o); + * }, + * [](PyObject *o) noexcept { + * nb::gil_scoped_acquire guard; + * Py_DECREF(o); + * }); + * ``` + * + * For this all to compile, a single one of your .cpp files must include this + * header file from somewhere as follows: + * + * ```cpp + * #include + * ``` + * + * Calling the ``inc_ref()`` and ``dec_ref()`` members many times throughout + * the code can quickly become tedious. Nanobind also ships with a ``ref`` + * RAII helper class to help with this. + * + * ```cpp + * #include + * + * { + * ref x = new MyObject(); // <-- assigment to ref<..> automatically calls inc_ref() + * x->func(); // ref<..> can be used like a normal pointer + * } // <-- Destruction of ref<..> calls dec_ref(), deleting the instance in this example. + * ``` + * + * When the file ``nanobind/intrusive/ref.h`` is included following + * ``nanobind/nanobind.h``, it also exposes a custom type caster to bind + * functions taking or returning ``ref``-typed values. + */ +struct NB_INTRUSIVE_EXPORT IntrusiveCounter { +public: + IntrusiveCounter() noexcept = default; + + // The counter value is not affected by copy/move assignment/construction + IntrusiveCounter(const IntrusiveCounter &) noexcept { } + IntrusiveCounter(IntrusiveCounter &&) noexcept { } + IntrusiveCounter &operator=(const IntrusiveCounter &) noexcept { return *this; } + IntrusiveCounter &operator=(IntrusiveCounter &&) noexcept { return *this; } + + /// Increase the object's reference count + void inc_ref() const noexcept; + + /// Decrease the object's reference count, return ``true`` if it should be deallocated + bool dec_ref() const noexcept; + + /// Return the Python object associated with this instance (or NULL) + PyObject *self_py() const noexcept; + + /// Set the Python object associated with this instance + void set_self_py(PyObject *self) noexcept; + +protected: + /** + * \brief Mutable counter. Note that the value ``1`` actually encodes + * a zero reference count (see the file ``counter.inl`` for details). + */ + mutable uintptr_t m_state = 1; +}; + +static_assert( + sizeof(IntrusiveCounter) == sizeof(void *), + "The IntrusiveCounter class should always have the same size as a pointer."); + +/// Reference-counted base type of an object hierarchy +class NB_INTRUSIVE_EXPORT IntrusiveBase { +public: + /// Increase the object's reference count + void inc_ref() noexcept { m_ref_count.inc_ref(); } + + /// Decrease the object's reference count, return ``true`` if it should be deallocated + bool dec_ref() noexcept { return m_ref_count.dec_ref(); } + + /// Return the Python object associated with this instance (or NULL) + void set_self_py(PyObject *self) noexcept { m_ref_count.set_self_py(self); } + + /// Set the Python object associated with this instance + PyObject *self_py() const noexcept { return m_ref_count.self_py(); } + + /// Virtual destructor + virtual ~IntrusiveBase() = default; + +private: + IntrusiveCounter m_ref_count; +}; + +/** + * \brief Increase the reference count of an intrusively reference-counted + * object ``o`` if ``o`` is non-NULL. + */ +inline void inc_ref(IntrusiveBase *o) noexcept { + if (o) + o->inc_ref(); +} + +/** + * \brief Decrease the reference count and potentially delete an intrusively + * reference-counted object ``o`` if ``o`` is non-NULL. + */ +inline void dec_ref(IntrusiveBase *o) noexcept { + if (o && o->dec_ref()) + delete o; +} + +/** + * \brief Install Python reference counting handlers + * + * The ``IntrusiveCounter`` class is designed so that the dependency on Python is + * *optional*: the code compiles in ordinary C++ projects, in which case the + * Python reference counting functionality will simply not be used. + * + * Python binding code must invoke ``intrusive_init`` once to supply two + * functions that increase and decrease the reference count of a Python object, + * while ensuring that the GIL is held. + */ +extern NB_INTRUSIVE_EXPORT +void intrusive_init(void (*intrusive_inc_ref_py)(PyObject *) noexcept, + void (*intrusive_dec_ref_py)(PyObject *) noexcept); diff --git a/include/nanobind/intrusive/counter.inl b/include/nanobind/intrusive/counter.inl new file mode 100644 index 000000000..d0a98ce49 --- /dev/null +++ b/include/nanobind/intrusive/counter.inl @@ -0,0 +1,139 @@ +/* + nanobind/intrusive/counter.inl: Intrusive reference counting sample + implementation; see 'counter.h' for an explanation of the interface. + + Copyright (c) 2023 Wenzel Jakob + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#include "counter.h" +#include +#include + +// The code below uses intrinsics for atomic operations. This is not as nice +// and portable as ``std::atomic`` but avoids pulling in large amounts of +// STL header code + +#if !defined(_MSC_VER) +#define NB_ATOMIC_LOAD(ptr) __atomic_load_n(ptr, 0) +#define NB_ATOMIC_STORE(ptr, v) __atomic_store_n(ptr, v, 0) +#define NB_ATOMIC_CMPXCHG(ptr, cmp, xchg) \ + __atomic_compare_exchange_n(ptr, cmp, xchg, true, 0, 0) +#else +extern "C" void *_InterlockedCompareExchangePointer( + void *volatile *Destination, + void *Exchange, void *Comparand); +#pragma intrinsic(_InterlockedCompareExchangePointer) + +#define NB_ATOMIC_LOAD(ptr) *((volatile const uintptr_t *) ptr) +#define NB_ATOMIC_STORE(ptr, v) *((volatile uintptr_t *) ptr) = v; +#define NB_ATOMIC_CMPXCHG(ptr, cmp, xchg) nb_cmpxchg(ptr, cmp, xchg) + +static bool nb_cmpxchg(uintptr_t *ptr, uintptr_t *cmp, uintptr_t xchg) { + uintptr_t cmpv = *cmp; + uintptr_t prev = (uintptr_t) _InterlockedCompareExchangePointer( + (void * volatile *) ptr, (void *) xchg, (void *) cmpv); + if (prev == cmpv) { + return true; + } else { + *cmp = prev; + return false; + } +} +#endif + +static void (*intrusive_inc_ref_py)(PyObject *) noexcept = nullptr, + (*intrusive_dec_ref_py)(PyObject *) noexcept = nullptr; + +void intrusive_init(void (*intrusive_inc_ref_py_)(PyObject *) noexcept, + void (*intrusive_dec_ref_py_)(PyObject *) noexcept) { + intrusive_inc_ref_py = intrusive_inc_ref_py_; + intrusive_dec_ref_py = intrusive_dec_ref_py_; +} + +/** A few implementation details: + * + * The ``IntrusiveCounter`` constructor sets the ``m_state`` field to ``1``, + * which indicates that the instance is owned by C++. Bits 2..63 of this + * field are used to store the actual reference count value. The + * ``inc_ref()`` and ``dec_ref()`` functions increment or decrement this + * number. When ``dec_ref()`` removes the last reference, the instance + * returns ``true`` to indicate that it should be deallocated using a + * *delete expression* that would typically be handled using a polymorphic + * destructor. + * + * When an class with intrusive reference counting is returned from C++ to + * Python, nanobind will invoke ``set_self_py()``, which hands ownership + * over to Python/nanobind. Any remaining references will be moved from the + * ``m_state`` field to the Python reference count. In this mode, + * ``inc_ref()`` and ``dec_ref()`` wrap Python reference counting + * primitives (``Py_INCREF()`` / ``Py_DECREF()``) which must be made + * available by calling the function ``intrusive_init`` once during module + * initialization. Note that the `m_state` field is also used to store a + * pointer to the `PyObject *`. Python instance pointers are always aligned + * (i.e. bit 1 is zero), which disambiguates between the two possible + * configurations. + */ + +void IntrusiveCounter::inc_ref() const noexcept { + uintptr_t v = NB_ATOMIC_LOAD(&m_state); + + while (true) { + if (v & 1) { + if (!NB_ATOMIC_CMPXCHG(&m_state, &v, v + 2)) + continue; + } else { + intrusive_inc_ref_py((PyObject *) v); + } + + break; + } +} + +bool IntrusiveCounter::dec_ref() const noexcept { + uintptr_t v = NB_ATOMIC_LOAD(&m_state); + + while (true) { + if (v & 1) { + if (v == 1) { + fprintf(stderr, "IntrusiveCounter::dec_ref(%p): reference count underflow!", this); + abort(); + } else if (v == 3) { + return true; + } + + if (!NB_ATOMIC_CMPXCHG(&m_state, &v, v - 2)) + continue; + } else { + intrusive_dec_ref_py((PyObject *) v); + } + + return false; + } +} + +void IntrusiveCounter::set_self_py(PyObject *o) noexcept { + uintptr_t v = NB_ATOMIC_LOAD(&m_state); + + if (v & 1) { + v >>= 1; + for (uintptr_t i = 0; i < v; ++i) + intrusive_inc_ref_py(o); + + NB_ATOMIC_STORE(&m_state, (uintptr_t) o); + } else { + fprintf(stderr, "IntrusiveCounter::set_self_py(%p): a Python object was already present!", this); + abort(); + } +} + +PyObject *IntrusiveCounter::self_py() const noexcept { + uintptr_t v = NB_ATOMIC_LOAD(&m_state); + + if (v & 1) + return nullptr; + else + return (PyObject *) v; +} diff --git a/include/nanobind/intrusive/ref.h b/include/nanobind/intrusive/ref.h new file mode 100644 index 000000000..411e43e1b --- /dev/null +++ b/include/nanobind/intrusive/ref.h @@ -0,0 +1,136 @@ +/* + nanobind/intrusive/ref.h: This file defines the ``ref`` RAII scoped + reference counting helper class. + + When included following ``nanobind/nanobind.h``, the code below also + exposes a custom type caster to bind functions taking or returning + ``ref``-typed values. + + Copyright (c) 2023 Wenzel Jakob + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#pragma once + +#include "counter.h" + +/** + * \brief RAII scoped reference counting helper class + * + * ``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 ``inc_ref()`` and ``dec_ref()`` must + * be defined before including this file. Default implementations for + * subclasses of the type ``IntrusiveBase`` are already provided as part of the + * file ``counter.h``. + */ +template class ref { +public: + /// Create a null reference + ref() = default; + + /// Construct a reference from a pointer + ref(T *ptr) : m_ptr(ptr) { inc_ref(m_ptr); } + + /// Copy a reference, increases the reference count + ref(const ref &r) : m_ptr(r.m_ptr) { inc_ref(m_ptr); } + + /// Move a reference witout changing the reference count + ref(ref &&r) noexcept : m_ptr(r.m_ptr) { r.m_ptr = nullptr; } + + /// Destroy this reference + ~ref() { dec_ref(m_ptr); } + + /// Move-assign another reference into this one + ref &operator=(ref &&r) noexcept { + dec_ref(m_ptr); + m_ptr = r.m_ptr; + r.m_ptr = nullptr; + return *this; + } + + /// Copy-assign another reference into this one + ref &operator=(const ref &r) { + inc_ref(r.m_ptr); + dec_ref(m_ptr); + m_ptr = r.m_ptr; + return *this; + } + + /// Overwrite this reference with a pointer to another object + ref &operator=(T *ptr) { + inc_ref(ptr); + dec_ref(m_ptr); + m_ptr = ptr; + return *this; + } + + /// Compare this reference with another reference + bool operator==(const ref &r) const { return m_ptr == r.m_ptr; } + + /// Compare this reference with another reference + bool operator!=(const ref &r) const { return m_ptr != r.m_ptr; } + + /// Compare this reference with a pointer + bool operator==(const T *ptr) const { return m_ptr == ptr; } + + /// Compare this reference with a pointer + bool operator!=(const T *ptr) const { return m_ptr != ptr; } + + /// Access the object referenced by this reference + T *operator->() { return m_ptr; } + + /// Access the object referenced by this reference + const T *operator->() const { return m_ptr; } + + /// Return a C++ reference to the referenced object + T &operator*() { return *m_ptr; } + + /// Return a const C++ reference to the referenced object + const T &operator*() const { return *m_ptr; } + + /// Return a pointer to the referenced object + explicit operator T *() { return m_ptr; } + + /// Return a const pointer to the referenced object + T *get() { return m_ptr; } + + /// Return a pointer to the referenced object + const T *get() const { return m_ptr; } + +private: + T *m_ptr = nullptr; +}; + +// Registar a type caster for ``ref`` if nanobind was previously #included +#if defined(NB_VERSION_MAJOR) +namespace nanobind::detail { + template struct type_caster> { + using Caster = make_caster; + static constexpr bool IsClass = true; + NB_TYPE_CASTER(ref, Caster::Name); + + bool from_python(handle src, uint8_t flags, + cleanup_list *cleanup) noexcept { + Caster caster; + if (!caster.from_python(src, flags, cleanup)) + return false; + + value = Value(caster.operator T *()); + return true; + } + + static handle from_cpp(const ref &value, rv_policy policy, + cleanup_list *cleanup) noexcept { + return Caster::from_cpp(value.get(), policy, cleanup); + } + }; +}; + +#endif diff --git a/include/nanobind/stl/complex.h b/include/nanobind/stl/complex.h index 3c4c8a1c9..d69b84675 100644 --- a/include/nanobind/stl/complex.h +++ b/include/nanobind/stl/complex.h @@ -32,14 +32,16 @@ template struct type_caster> { return true; } - if (Recursive && !PyFloat_CheckExact(src.ptr()) && - !PyLong_CheckExact(src.ptr()) && - PyObject_HasAttrString(src.ptr(), "imag")) { - try { - object tmp = handle(&PyComplex_Type)(src); - return from_python(tmp, flags, cleanup); - } catch (...) { - return false; + if constexpr (Recursive) { + if (!PyFloat_CheckExact(src.ptr()) && + !PyLong_CheckExact(src.ptr()) && + PyObject_HasAttrString(src.ptr(), "imag")) { + try { + object tmp = handle(&PyComplex_Type)(src); + return from_python(tmp, flags, cleanup); + } catch (...) { + return false; + } } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a339454d5..f899ce9e5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -31,7 +31,7 @@ nanobind_add_module(test_chrono_ext test_chrono.cpp ${NB_EXTRA_ARGS}) nanobind_add_module(test_enum_ext test_enum.cpp ${NB_EXTRA_ARGS}) nanobind_add_module(test_eval_ext test_eval.cpp ${NB_EXTRA_ARGS}) nanobind_add_module(test_ndarray_ext test_ndarray.cpp ${NB_EXTRA_ARGS}) -nanobind_add_module(test_intrusive_ext test_intrusive.cpp object.cpp object.h ${NB_EXTRA_ARGS}) +nanobind_add_module(test_intrusive_ext test_intrusive.cpp test_intrusive_impl.cpp ${NB_EXTRA_ARGS}) nanobind_add_module(test_exception_ext test_exception.cpp ${NB_EXTRA_ARGS}) nanobind_add_module(test_make_iterator_ext test_make_iterator.cpp ${NB_EXTRA_ARGS}) nanobind_add_module(test_issue_ext test_issue.cpp ${NB_EXTRA_ARGS}) diff --git a/tests/object.cpp b/tests/object.cpp deleted file mode 100644 index 45d0e2d42..000000000 --- a/tests/object.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "object.h" -#include -#include - -static void (*object_inc_ref_py)(PyObject *) noexcept = nullptr; -static void (*object_dec_ref_py)(PyObject *) noexcept = nullptr; - -void Object::inc_ref() const noexcept { - 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 noexcept { - uintptr_t value = m_state.load(std::memory_order_relaxed); - - while (true) { - if (value & 1) { - if (value == 1) { - fprintf(stderr, "Object::dec_ref(%p): reference count underflow!", this); - abort(); - } 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) noexcept { - 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 { - fprintf(stderr, "Object::set_self_py(%p): a Python object was already present!", this); - abort(); - } -} - -PyObject *Object::self_py() const noexcept { - 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 *) noexcept, - void (*object_dec_ref_py_)(PyObject *) noexcept) { - object_inc_ref_py = object_inc_ref_py_; - object_dec_ref_py = object_dec_ref_py_; -} diff --git a/tests/object.h b/tests/object.h deleted file mode 100644 index a707750df..000000000 --- a/tests/object.h +++ /dev/null @@ -1,212 +0,0 @@ -/** - * This file contains an exemplary base object class with intrusive reference - * counting. The implementation is designed so that it does not have a direct - * dependency on Python or nanobind. It can therefore be used in codebases - * where a Python interface is merely an optional component. - * - * nanobind's documentation on object ownership explains the rationale of - * intrusive reference counting, while comments in this file and 'object.cpp' - * explain technical aspects. - */ - -#include -#pragma once - -/* While the implementation does not directly depend on Python, the PyObject* - type occurs in a few function interfaces (in a fully opaque manner). We must - therefore forward-declare it. */ -extern "C" { - struct _object; - typedef _object PyObject; -}; - -/** - * \brief Object base class with intrusive reference counting - * - * The Object class provides a convenient foundation of a class hierarchy that - * will ease lifetime and ownership-related issues whenever Python bindings are - * involved. - * - * Internally, its constructor sets the `m_state` field to `1`, which indicates - * that the instance is owned by C++. Bits 2..63 of this field are used to - * store the actual reference count value. The `inc_ref()` and `dec_ref()` - * functions can be used to increment or decrement this reference count. When - * `dec_ref()` removes the last reference, the instance will be deallocated - * using a `delete` expression handled using a polymorphic destructor. - * - * When a subclass of `Object` is constructed to Python or returned from C++ to - * Python, nanobind will invoke `Object::set_self_py()`, which hands ownership - * over to Python/nanobind. Any remaining references will be moved from the - * `m_state` field to the Python reference count. In this mode, `inc_ref()` and - * `dec_ref()` wrap Python reference counting primitives (`Py_INCREF()` / - * `Py_DECREF()`) which must be made available by calling the function - * `object_init_py` once during module initialization. Note that the `m_state` - * field is also used to store a pointer to the `PyObject *`. Python instance - * pointers are always aligned (i.e. bit 1 is zero), which disambiguates - * between the two possible configurations. - * - * Within C++, the RAII helper class `ref` (defined below) can be used to keep - * instances alive. This removes the need to call the `inc_ref()` / `dec_ref()` - * functions explicitly. - * - * ``` - * { - * ref inst = new MyClass(); - * inst->my_function(); - * ... - * } // end of scope, 'inst' automatically deleted if no longer referenced - * ``` - * - * A separate optional file ``object_py.h`` provides a nanobind type caster - * to bind functions taking/returning values of type `ref`. - */ -class Object { -public: - Object() = default; - - /* The following move/assignment constructors/operators are no-ops. They - intentionally do not change the reference count field (m_state) that - is associated with a fixed address in memory */ - Object(const Object &) : Object() { } - Object(Object &&) : Object() { } - Object &operator=(const Object &) { return *this; } - Object &operator=(Object &&) { return *this; } - - // Polymorphic default destructor - virtual ~Object() = default; - - /// Increase the object's reference count - void inc_ref() const noexcept; - - /// Decrease the object's reference count and potentially deallocate it - void dec_ref() const noexcept; - - /// Return the Python object associated with this instance (or NULL) - PyObject *self_py() const noexcept; - - /// Set the Python object associated with this instance - void set_self_py(PyObject *self) noexcept; - -private: - mutable std::atomic m_state { 1 }; -}; - -/** - * \brief Install Python reference counting handlers - * - * The `Object` class is designed so that the dependency on Python is - * *optional*: the code compiles in ordinary C++ projects, in which case the - * Python reference counting functionality will simply not be used. - * - * Python binding code must invoke `object_init_py` and provide functions that - * can be used to increase/decrease the Python reference count of an instance - * (i.e., `Py_INCREF` / `Py_DECREF`). - */ -void object_init_py(void (*object_inc_ref_py)(PyObject *) noexcept, - void (*object_dec_ref_py)(PyObject *) noexcept); - - -/** - * \brief RAII reference counting helper class - * - * ``ref`` is a simple RAII wrapper class that stores a pointer to a subclass - * of ``Object``. It takes care of increasing and decreasing the reference - * count of the underlying instance. When the last reference goes out of scope, - * the associated object will be deallocated. - * - * A separate optional file ``object_py.h`` provides a nanobind type caster - * to bind functions taking/returning values of type `ref`. - */ -template class ref { -public: - /// Create a nullptr reference - ref() : m_ptr(nullptr) { } - - /// Construct a reference from a pointer - ref(T *ptr) : m_ptr(ptr) { - if (ptr) - ((Object *) ptr)->inc_ref(); - } - - /// Copy constructor - ref(const ref &r) : m_ptr(r.m_ptr) { - if (m_ptr) - ((Object *) m_ptr)->inc_ref(); - } - - /// Move constructor - ref(ref &&r) noexcept : m_ptr(r.m_ptr) { - r.m_ptr = nullptr; - } - - /// Destroy this reference - ~ref() { - if (m_ptr) - ((Object *) m_ptr)->dec_ref(); - } - - /// Move another reference into the current one - ref &operator=(ref &&r) noexcept { - if (m_ptr) - ((Object *) m_ptr)->dec_ref(); - m_ptr = r.m_ptr; - r.m_ptr = nullptr; - return *this; - } - - /// Overwrite this reference with another reference - ref &operator=(const ref &r) { - if (r.m_ptr) - ((Object *) r.m_ptr)->inc_ref(); - if (m_ptr) - ((Object *) m_ptr)->dec_ref(); - m_ptr = r.m_ptr; - return *this; - } - - /// Overwrite this reference with a pointer to another object - ref &operator=(T *ptr) { - if (ptr) - ((Object *) ptr)->inc_ref(); - if (m_ptr) - ((Object *) m_ptr)->dec_ref(); - m_ptr = ptr; - return *this; - } - - /// Compare this reference with another reference - bool operator==(const ref &r) const { return m_ptr == r.m_ptr; } - - /// Compare this reference with another reference - bool operator!=(const ref &r) const { return m_ptr != r.m_ptr; } - - /// Compare this reference with a pointer - bool operator==(const T *ptr) const { return m_ptr == ptr; } - - /// Compare this reference with a pointer - bool operator!=(const T *ptr) const { return m_ptr != ptr; } - - /// Access the object referenced by this reference - T *operator->() { return m_ptr; } - - /// Access the object referenced by this reference - const T *operator->() const { return m_ptr; } - - /// Return a C++ reference to the referenced object - T &operator*() { return *m_ptr; } - - /// Return a const C++ reference to the referenced object - const T &operator*() const { return *m_ptr; } - - /// Return a pointer to the referenced object - explicit operator T *() { return m_ptr; } - - /// Return a const pointer to the referenced object - T *get() { return m_ptr; } - - /// Return a pointer to the referenced object - const T *get() const { return m_ptr; } - -private: - T *m_ptr; -}; diff --git a/tests/test_intrusive.cpp b/tests/test_intrusive.cpp index 08fe902fc..25208c414 100644 --- a/tests/test_intrusive.cpp +++ b/tests/test_intrusive.cpp @@ -1,8 +1,8 @@ #include #include #include -#include "object.h" -#include "object_py.h" +#include +#include namespace nb = nanobind; using namespace nb::literals; @@ -10,15 +10,11 @@ using namespace nb::literals; static int test_constructed = 0; static int test_destructed = 0; -class Test : Object { +class Test : public IntrusiveBase { public: - Test() { - test_constructed++; - } + Test() { test_constructed++; } - virtual ~Test() { - test_destructed++; - } + virtual ~Test() { test_destructed++; } virtual int value(int i) const { return 123 + i; } @@ -34,7 +30,7 @@ class PyTest : Test { }; NB_MODULE(test_intrusive_ext, m) { - object_init_py( + intrusive_init( [](PyObject *o) noexcept { nb::gil_scoped_acquire guard; Py_INCREF(o); @@ -44,12 +40,12 @@ NB_MODULE(test_intrusive_ext, m) { Py_DECREF(o); }); - nb::class_( - m, "Object", - nb::intrusive_ptr( - [](Object *o, PyObject *po) noexcept { o->set_self_py(po); })); + nb::class_( + m, "IntrusiveBase", + nb::intrusive_ptr( + [](IntrusiveBase *o, PyObject *po) noexcept { o->set_self_py(po); })); - nb::class_(m, "Test") + nb::class_(m, "Test") .def(nb::init<>()) .def("value", &Test::value) .def_static("create_raw", &Test::create_raw) diff --git a/tests/test_intrusive_impl.cpp b/tests/test_intrusive_impl.cpp new file mode 100644 index 000000000..e9eade069 --- /dev/null +++ b/tests/test_intrusive_impl.cpp @@ -0,0 +1 @@ +#include