Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f3044cf

Browse files
committedJun 14, 2022
Intrusive pointer support
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.
1 parent b08cade commit f3044cf

17 files changed

+622
-12
lines changed
 

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
libnanobind-static.a
22
libnanobind.dylib
33
libnanobind.so
4+
libnanobind-abi3.dylib
5+
libnanobind-abi3.so
46
nanobind.dll
7+
nanobind-abi3.dll
58

69
/.ninja_deps
710
/.ninja_log

‎README.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,12 @@ changes are detailed below.
264264
- **Shared pointers and holders**. _nanobind_ removes the concept of a _holder
265265
type_, which caused inefficiencies and introduced complexity in _pybind11_.
266266
This has implications on object ownership, shared ownership, and interactions
267-
with C++ shared/unique pointers. Please see the following [separate
268-
document](docs/ownership.md) for the nitty-gritty details.
267+
with C++ shared/unique pointers.
268+
269+
Please see the following [separate page](docs/ownership.md) for the
270+
nitty-gritty details on shared and unique pointers. Classes with _intrusive_
271+
reference counting also continue to be supported, please see the [linked
272+
page](docs/intrusive.md) for details.
269273

270274
The gist is that use of shared/unique pointers requires one or both of the
271275
following optional header files:

‎docs/intrusive.md

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Intrusive reference counting
2+
3+
Like _pybind11_, _nanobind_ provides a way of binding classes with builtin
4+
("intrusive") reference counting. This is the most general and cheapest way of
5+
handling shared ownership between C++ and Python, but it requires that the base
6+
class of an object hierarchy is adapted according to the needs of _nanobind_.
7+
8+
Ordinarily, a simple class with intrusive reference counting might look as
9+
follows:
10+
11+
```cpp
12+
class Object {
13+
public:
14+
void inc_ref() const { ++m_ref_count; }
15+
16+
void dec_ref() const {
17+
if (--m_ref_count == 0)
18+
delete this;
19+
}
20+
21+
private:
22+
mutable std::atomic<size_t> m_ref_count { 0 };
23+
};
24+
```
25+
26+
The advantage of this over standard approaches like `std::shared_ptr<T>` is
27+
that no separate control block must be allocated. Subtle technical band-aids
28+
like `std::enable_shared_from_this<T>` to avoid undefined behavior are also
29+
no longer necessary.
30+
31+
However, one issue that tends to arise when a type like `Object` is wrapped
32+
using _nanobind_ is that there are now *two* separate reference counts
33+
referring to the same object: one in Python's `PyObject`, and one in `Object`.
34+
This can lead to a problematic reference cycle:
35+
36+
- Python's `PyObject` needs to keep `Object` alive so that the instance can be
37+
safely passed to C++ functions.
38+
39+
- The C++ `Object` may in turn need to keep the `PyObject` alive. This is the
40+
case when a subclass uses `NB_TRAMPOLINE` and `NB_OVERRIDE` features to route
41+
C++ virtual function calls back to a Python implementation.
42+
43+
The source of the problem is that there are *two* separate counters that try to
44+
reason about the reference count of *one* instance. The solution is to reduce
45+
this to just one counter:
46+
47+
- if an instance lives purely on the C++ side, the `m_ref_count` field is
48+
used to reason about the number of references.
49+
50+
- The first time that an instance is exposed to Python (by being created from
51+
Python, or by being returned from a bound C++ function), lifetime management
52+
is delegated to Python.
53+
54+
The files
55+
[`tests/object.h`](https://github.com/wjakob/nanobind/blob/master/tests/object.h)
56+
and
57+
[`tests/object.cpp`](https://github.com/wjakob/nanobind/blob/master/tests/object.cpp)
58+
contain an example implementation of a suitable base class named `Object`. It
59+
contains an extra optimization to use a single field of type
60+
`std::atomic<uintptr_t> m_state;` (8 bytes) to store *either* a reference
61+
counter or a pointer to a `PyObject*`.
62+
63+
The main change in _nanobind_-based bindings is that the base class must
64+
specify a `nb::intrusive_ptr` annotation to inform an instance that lifetime
65+
management has been taken over by Python. This annotation is automatically
66+
inherited by all subclasses.
67+
68+
```cpp
69+
nb::class_<Object>(
70+
m, "Object",
71+
nb::intrusive_ptr<Object>(
72+
[](Object *o, PyObject *po) { o->set_self_py(po); }));
73+
```
74+

‎docs/ownership.md

+11-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ complexity; they were therefore removed in _nanobind_. This has implications on
1414
object ownership, shared ownership, and interactions with C++ shared/unique
1515
pointers.
1616

17+
- **Intrusive reference counting**: Like _pybind11_, _nanobind_ provides a way
18+
of binding classes with builtin ("intrusive") reference counting. This is the
19+
most general and cheapest way of handling shared ownership between C++ and
20+
Python, but it requires that the base class of an object hierarchy is adapted
21+
according to the needs of _nanobind_. Details on using intrusive reference
22+
counting can be found
23+
[here](https://github.com/wjakob/nanobind/blob/master/docs/intrusive.md).
24+
1725
- **Shared pointers**: It is possible to bind functions that receive and return
1826
`std::shared_ptr<T>` by including the optional type caster
1927
[`nanobind/stl/shared_ptr.h`](https://github.com/wjakob/nanobind/blob/master/include/nanobind/stl/shared_ptr.h)
@@ -23,13 +31,12 @@ pointers.
2331
ownership must be shared between Python and C++. _nanobind_ does this by
2432
increasing the reference count of the `PyObject` and then creating a
2533
`std::shared_ptr<T>` with a new control block containing a custom deleter
26-
that reduces the Python reference count upon destruction of the shared
27-
pointer.
34+
that will in turn reduce the Python reference count upon destruction of the
35+
shared pointer.
2836

2937
When a C++ function returns a `std::shared_ptr<T>`, _nanobind_ checks if the
3038
instance already has a `PyObject` counterpart (nothing needs to be done in
31-
this case). Otherwise, it creates a compact `PyObject` wrapping a pointer to
32-
the instance data. It indicates shared ownership by creating a temporary
39+
this case). Otherwise, it indicates shared ownership by creating a temporary
3340
`std::shared_ptr<T>` on the heap that will be destructed when the `PyObject`
3441
is garbage collected.
3542

‎include/nanobind/nb_attr.h

+6
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ struct is_enum { bool is_signed; };
5757

5858
template <size_t /* Nurse */, size_t /* Patient */> struct keep_alive {};
5959
template <typename T> struct supplement {};
60+
template <typename T> struct intrusive_ptr {
61+
intrusive_ptr(void (*set_self_py)(T *, PyObject *))
62+
: set_self_py(set_self_py) {}
63+
void (*set_self_py)(T *, PyObject *);
64+
};
65+
6066
struct type_callback {
6167
type_callback(void (*value)(PyType_Slot **) noexcept) : value(value) {}
6268
void (*value)(PyType_Slot **) noexcept;

‎include/nanobind/nb_class.h

+11-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ enum class type_flags : uint32_t {
6666
has_supplement = (1 << 18),
6767

6868
/// Instances of this type support dynamic attribute assignment
69-
has_dynamic_attr = (1 << 19)
69+
has_dynamic_attr = (1 << 19),
70+
71+
/// The class uses an intrusive reference counting approach
72+
intrusive_ptr = (1 << 20)
7073
};
7174

7275
struct type_data {
@@ -87,6 +90,7 @@ struct type_data {
8790
bool (**implicit_py)(PyTypeObject *, PyObject *, cleanup_list *) noexcept;
8891
void (*type_callback)(PyType_Slot **) noexcept;
8992
void *supplement;
93+
void (*set_self_py)(void *, PyObject *);
9094
#if defined(Py_LIMITED_API)
9195
size_t dictoffset;
9296
#endif
@@ -107,6 +111,12 @@ NB_INLINE void type_extra_apply(type_data &t, type_callback c) {
107111
t.type_callback = c.value;
108112
}
109113

114+
template <typename T>
115+
NB_INLINE void type_extra_apply(type_data &t, intrusive_ptr<T> ip) {
116+
t.flags |= (uint32_t) type_flags::intrusive_ptr;
117+
t.set_self_py = (void (*)(void *, PyObject *)) ip.set_self_py;
118+
}
119+
110120
NB_INLINE void type_extra_apply(type_data &t, is_enum e) {
111121
if (e.is_signed)
112122
t.flags |= (uint32_t) type_flags::is_signed_enum;

‎include/nanobind/stl/shared_ptr.h

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
/*
2+
nanobind/stl/shared_ptr.h: Type caster for std::shared_ptr<T>
3+
4+
Copyright (c) 2022 Wenzel Jakob <wenzel.jakob@epfl.ch>
5+
6+
All rights reserved. Use of this source code is governed by a
7+
BSD-style license that can be found in the LICENSE file.
8+
*/
9+
110
#pragma once
211

312
#include <nanobind/nanobind.h>

‎include/nanobind/stl/unique_ptr.h

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
/*
2+
nanobind/stl/unique_ptr.h: Type caster for std::unique_ptr<T>
3+
4+
Copyright (c) 2022 Wenzel Jakob <wenzel.jakob@epfl.ch>
5+
6+
All rights reserved. Use of this source code is governed by a
7+
BSD-style license that can be found in the LICENSE file.
8+
*/
9+
110
#pragma once
211

312
#include <nanobind/nanobind.h>

‎src/nb_func.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,10 @@ static PyObject *nb_func_vectorcall_complex(PyObject *self,
567567
nb_inst *self_arg_nb = (nb_inst *) self_arg;
568568
self_arg_nb->destruct = true;
569569
self_arg_nb->ready = true;
570+
571+
const type_data *t = nb_type_data(Py_TYPE(self_arg));
572+
if (t->flags & (uint32_t) type_flags::intrusive_ptr)
573+
t->set_self_py(inst_ptr(self_arg_nb), self_arg);
570574
}
571575

572576
goto done;
@@ -690,6 +694,10 @@ static PyObject *nb_func_vectorcall_simple(PyObject *self,
690694
nb_inst *self_arg_nb = (nb_inst *) self_arg;
691695
self_arg_nb->destruct = true;
692696
self_arg_nb->ready = true;
697+
698+
const type_data *t = nb_type_data(Py_TYPE(self_arg));
699+
if (t->flags & (uint32_t) type_flags::intrusive_ptr)
700+
t->set_self_py(inst_ptr(self_arg_nb), self_arg);
693701
}
694702

695703
goto done;

‎src/nb_internals.cpp

+7-1
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,14 @@
6262
# define NB_BUILD_ABI ""
6363
#endif
6464

65+
#if defined(Py_LIMITED_API)
66+
# define NB_LIMITED_API "_limited"
67+
#else
68+
# define NB_LIMITED_API ""
69+
#endif
70+
6571
#define NB_INTERNALS_ID "__nb_internals_v" \
66-
NB_TOSTRING(NB_INTERNALS_VERSION) NB_COMPILER_TYPE NB_STDLIB NB_BUILD_ABI NB_BUILD_TYPE "__"
72+
NB_TOSTRING(NB_INTERNALS_VERSION) NB_COMPILER_TYPE NB_STDLIB NB_BUILD_ABI NB_BUILD_TYPE NB_LIMITED_API "__"
6773

6874
NAMESPACE_BEGIN(NB_NAMESPACE)
6975
NAMESPACE_BEGIN(detail)

‎src/nb_type.cpp

+19-4
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,8 @@ PyObject *nb_type_new(const type_data *t) noexcept {
307307
has_base_py = t->flags & (uint32_t) type_flags::has_base_py,
308308
has_type_callback = t->flags & (uint32_t) type_flags::has_type_callback,
309309
has_supplement = t->flags & (uint32_t) type_flags::has_supplement,
310-
has_dynamic_attr = t->flags & (uint32_t) type_flags::has_dynamic_attr;
310+
has_dynamic_attr = t->flags & (uint32_t) type_flags::has_dynamic_attr,
311+
intrusive_ptr = t->flags & (uint32_t) type_flags::intrusive_ptr;
311312

312313
nb_internals &internals = internals_get();
313314
str name(t->name), qualname = name;
@@ -351,9 +352,10 @@ PyObject *nb_type_new(const type_data *t) noexcept {
351352
base = (PyObject *) it->second->type_py;
352353
}
353354

355+
type_data *tb = nullptr;
354356
if (base) {
355357
// Check if the base type already has dynamic attributes
356-
type_data *tb = nb_type_data((PyTypeObject *) base);
358+
tb = nb_type_data((PyTypeObject *) base);
357359
if (tb->flags & (uint32_t) type_flags::has_dynamic_attr)
358360
has_dynamic_attr = true;
359361

@@ -486,6 +488,12 @@ PyObject *nb_type_new(const type_data *t) noexcept {
486488
type_data *to = nb_type_data((PyTypeObject *) result);
487489
*to = *t;
488490

491+
if (!intrusive_ptr && tb &&
492+
(tb->flags & (uint32_t) type_flags::intrusive_ptr)) {
493+
to->flags |= (uint32_t) type_flags::intrusive_ptr;
494+
to->set_self_py = tb->set_self_py;
495+
}
496+
489497
to->name = name_copy;
490498
to->type_py = (PyTypeObject *) result;
491499

@@ -776,9 +784,13 @@ PyObject *nb_type_put(const std::type_info *cpp_type, void *value,
776784
if (rvp == rv_policy::reference_internal && (!cleanup || !cleanup->self()))
777785
return nullptr;
778786

779-
bool store_in_obj = rvp == rv_policy::copy || rvp == rv_policy::move;
780-
781787
type_data *t = it2->second;
788+
const bool intrusive = t->flags & (uint32_t) type_flags::intrusive_ptr;
789+
if (intrusive)
790+
rvp = rv_policy::take_ownership;
791+
792+
const bool store_in_obj = rvp == rv_policy::copy || rvp == rv_policy::move;
793+
782794
nb_inst *inst =
783795
(nb_inst *) inst_new_impl(t->type_py, store_in_obj ? nullptr : value);
784796
if (!inst)
@@ -837,6 +849,9 @@ PyObject *nb_type_put(const std::type_info *cpp_type, void *value,
837849
if (rvp == rv_policy::reference_internal)
838850
keep_alive((PyObject *) inst, cleanup->self());
839851

852+
if (intrusive)
853+
t->set_self_py(new_value, (PyObject *) inst);
854+
840855
return (PyObject *) inst;
841856
}
842857

‎tests/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ nanobind_add_module(test_holders_ext test_holders.cpp)
44
nanobind_add_module(test_stl_ext test_stl.cpp)
55
nanobind_add_module(test_enum_ext test_enum.cpp)
66
nanobind_add_module(test_tensor_ext test_tensor.cpp)
7+
nanobind_add_module(test_intrusive_ext test_intrusive.cpp object.cpp object.h)
78

89
set(TEST_FILES
910
test_functions.py
@@ -12,6 +13,7 @@ set(TEST_FILES
1213
test_stl.py
1314
test_enum.py
1415
test_tensor.py
16+
test_intrusive.py
1517
)
1618

1719
if (NOT (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_CURRENT_BINARY_DIR) OR MSVC)

‎tests/object.cpp

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#include "object.h"
2+
#include <stdexcept>
3+
4+
static void (*object_inc_ref_py)(PyObject *) = nullptr;
5+
static void (*object_dec_ref_py)(PyObject *) = nullptr;
6+
7+
void Object::inc_ref() const {
8+
uintptr_t value = m_state.load(std::memory_order_relaxed);
9+
10+
while (true) {
11+
if (value & 1) {
12+
if (!m_state.compare_exchange_weak(value,
13+
value + 2,
14+
std::memory_order_relaxed,
15+
std::memory_order_relaxed))
16+
continue;
17+
} else {
18+
object_inc_ref_py((PyObject *) value);
19+
}
20+
21+
break;
22+
}
23+
}
24+
25+
void Object::dec_ref() const {
26+
uintptr_t value = m_state.load(std::memory_order_relaxed);
27+
28+
while (true) {
29+
if (value & 1) {
30+
if (value == 1) {
31+
throw std::runtime_error("Object::dec_ref(): reference count underflow!");
32+
} else if (value == 3) {
33+
delete this;
34+
} else {
35+
if (!m_state.compare_exchange_weak(value,
36+
value - 2,
37+
std::memory_order_relaxed,
38+
std::memory_order_relaxed))
39+
continue;
40+
}
41+
} else {
42+
object_dec_ref_py((PyObject *) value);
43+
}
44+
break;
45+
}
46+
}
47+
48+
void Object::set_self_py(PyObject *o) {
49+
uintptr_t value = m_state.load(std::memory_order_relaxed);
50+
if (value & 1) {
51+
value >>= 1;
52+
for (uintptr_t i = 0; i < value; ++i)
53+
object_inc_ref_py(o);
54+
55+
m_state.store((uintptr_t) o);
56+
} else {
57+
throw std::runtime_error("Object::set_self_py(): a Python object was already present!");
58+
}
59+
value = m_state.load(std::memory_order_relaxed);
60+
}
61+
62+
PyObject *Object::self_py() const {
63+
uintptr_t value = m_state.load(std::memory_order_relaxed);
64+
if (value & 1)
65+
return nullptr;
66+
else
67+
return (PyObject *) value;
68+
}
69+
70+
void object_init_py(void (*object_inc_ref_py_)(PyObject *),
71+
void (*object_dec_ref_py_)(PyObject *)) {
72+
object_inc_ref_py = object_inc_ref_py_;
73+
object_dec_ref_py = object_dec_ref_py_;
74+
}

‎tests/object.h

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/**
2+
* This file contains an exemplary base object class with intrusive reference
3+
* counting. The implementation is designed so that it does not have a direct
4+
* dependency on Python or nanobind. It can therefore be used in codebases
5+
* where a Python interface is merely an optional component.
6+
*
7+
* The file 'docs/intrusive.md' explains the basic rationale of intrusive
8+
* reference counting, while comments in this file and 'object.cpp' explain
9+
* technical aspects.
10+
*/
11+
12+
#include <atomic>
13+
#pragma once
14+
15+
/* While the implementation does not directly depend on Python, the PyObject*
16+
type occurs in a few function interfaces (in a fully opaque manner). We must
17+
therefore forward-declare it. */
18+
extern "C" {
19+
struct _object;
20+
typedef _object PyObject;
21+
};
22+
23+
/**
24+
* \brief Object base class with intrusive reference counting
25+
*
26+
* The Object class provides a convenient foundation of a class hierarchy that
27+
* will ease lifetime and ownership-related issues whenever Python bindings are
28+
* involved.
29+
*
30+
* Internally, its constructor sets the `m_state` field to `1`, which indicates
31+
* that the instance is owned by C++. Bits 2..63 of this field are used to
32+
* store the actual reference count value. The `inc_ref()` and `dec_ref()`
33+
* functions can be used to increment or decrement this reference count. When
34+
* `dec_ref()` removes the last reference, the instance will be deallocated
35+
* using a `delete` expression handled using a polymorphic destructor.
36+
*
37+
* When a subclass of `Object` is constructed to Python or returned from C++ to
38+
* Python, nanobind will invoke `Object::set_self_py()`, which hands ownership
39+
* over to Python/nanobind. Any remaining references will be moved from the
40+
* `m_state` field to the Python reference count. In this mode, `inc_ref()` and
41+
* `dec_ref()` wrap Python reference counting primitives (`Py_INCREF()` /
42+
* `Py_DECREF()`) which must be made available by calling the function
43+
* `object_init_py` once during module initialization. Note that the `m_state`
44+
* field is also used to store a pointer to the `PyObject *`. Python instance
45+
* pointers are always aligned (i.e. bit 1 is zero), which disambiguates
46+
* between the two possible configurations.
47+
*
48+
* Within C++, the RAII helper class `ref` (defined below) can be used to keep
49+
* instances alive. This removes the need to call the `inc_ref()` / `dec_ref()`
50+
* functions explicitly.
51+
*
52+
* ```
53+
* {
54+
* ref<MyClass> inst = new MyClass();
55+
* inst->my_function();
56+
* ...
57+
* } // end of scope, 'inst' automatically deleted if no longer referenced
58+
* ```
59+
*
60+
* A separate optional file ``object_py.h`` provides a nanobind type caster
61+
* to bind functions taking/returning values of type `ref<T>`.
62+
*/
63+
class Object {
64+
public:
65+
Object() = default;
66+
67+
/* The following move/assignment constructors/operators are no-ops. They
68+
intentionally do not change the reference count field (m_state) that
69+
is associated with a fixed address in memory */
70+
Object(const Object &) : Object() { }
71+
Object(Object &&) : Object() { }
72+
Object &operator=(const Object &) { return *this; }
73+
Object &operator=(Object &&) { return *this; }
74+
75+
// Polymorphic default destructor
76+
virtual ~Object() = default;
77+
78+
/// Increase the object's reference count
79+
void inc_ref() const;
80+
81+
/// Decrease the object's reference count and potentially deallocate it
82+
void dec_ref() const;
83+
84+
/// Return the Python object associated with this instance (or NULL)
85+
PyObject *self_py() const;
86+
87+
/// Set the Python object associated with this instance
88+
void set_self_py(PyObject *self);
89+
90+
private:
91+
mutable std::atomic<uintptr_t> m_state { 1 };
92+
};
93+
94+
/**
95+
* \brief Install Python reference counting handlers
96+
*
97+
* The `Object` class is designed so that the dependency on Python is
98+
* *optional*: the code compiles in ordinary C++ projects, in which case the
99+
* Python reference counting functionality will simply not be used.
100+
*
101+
* Python binding code must invoke `object_init_py` and provide functions that
102+
* can be used to increase/decrease the Python reference count of an instance
103+
* (i.e., `Py_INCREF` / `Py_DECREF`).
104+
*/
105+
void object_init_py(void (*object_inc_ref_py)(PyObject *),
106+
void (*object_dec_ref_py)(PyObject *));
107+
108+
109+
/**
110+
* \brief RAII reference counting helper class
111+
*
112+
* ``ref`` is a simple RAII wrapper class that stores a pointer to a subclass
113+
* of ``Object``. It takes care of increasing and decreasing the reference
114+
* count of the underlying instance. When the last reference goes out of scope,
115+
* the associated object will be deallocated.
116+
*
117+
* A separate optional file ``object_py.h`` provides a nanobind type caster
118+
* to bind functions taking/returning values of type `ref<T>`.
119+
*/
120+
template <typename T> class ref {
121+
public:
122+
/// Create a nullptr reference
123+
ref() : m_ptr(nullptr) { }
124+
125+
/// Construct a reference from a pointer
126+
ref(T *ptr) : m_ptr(ptr) {
127+
if (ptr)
128+
((Object *) ptr)->inc_ref();
129+
}
130+
131+
/// Copy constructor
132+
ref(const ref &r) : m_ptr(r.m_ptr) {
133+
if (m_ptr)
134+
((Object *) m_ptr)->inc_ref();
135+
}
136+
137+
/// Move constructor
138+
ref(ref &&r) noexcept : m_ptr(r.m_ptr) {
139+
r.m_ptr = nullptr;
140+
}
141+
142+
/// Destroy this reference
143+
~ref() {
144+
if (m_ptr)
145+
((Object *) m_ptr)->dec_ref();
146+
}
147+
148+
/// Move another reference into the current one
149+
ref &operator=(ref &&r) noexcept {
150+
if (m_ptr)
151+
((Object *) m_ptr)->dec_ref();
152+
m_ptr = r.m_ptr;
153+
r.m_ptr = nullptr;
154+
return *this;
155+
}
156+
157+
/// Overwrite this reference with another reference
158+
ref &operator=(const ref &r) {
159+
if (r.m_ptr)
160+
((Object *) r.m_ptr)->inc_ref();
161+
if (m_ptr)
162+
((Object *) m_ptr)->dec_ref();
163+
m_ptr = r.m_ptr;
164+
return *this;
165+
}
166+
167+
/// Overwrite this reference with a pointer to another object
168+
ref &operator=(T *ptr) {
169+
if (ptr)
170+
((Object *) ptr)->inc_ref();
171+
if (m_ptr)
172+
((Object *) m_ptr)->dec_ref();
173+
m_ptr = ptr;
174+
return *this;
175+
}
176+
177+
/// Compare this reference with another reference
178+
bool operator==(const ref &r) const { return m_ptr == r.m_ptr; }
179+
180+
/// Compare this reference with another reference
181+
bool operator!=(const ref &r) const { return m_ptr != r.m_ptr; }
182+
183+
/// Compare this reference with a pointer
184+
bool operator==(const T *ptr) const { return m_ptr == ptr; }
185+
186+
/// Compare this reference with a pointer
187+
bool operator!=(const T *ptr) const { return m_ptr != ptr; }
188+
189+
/// Access the object referenced by this reference
190+
T *operator->() { return m_ptr; }
191+
192+
/// Access the object referenced by this reference
193+
const T *operator->() const { return m_ptr; }
194+
195+
/// Return a C++ reference to the referenced object
196+
T &operator*() { return *m_ptr; }
197+
198+
/// Return a const C++ reference to the referenced object
199+
const T &operator*() const { return *m_ptr; }
200+
201+
/// Return a pointer to the referenced object
202+
explicit operator T *() { return m_ptr; }
203+
204+
/// Return a const pointer to the referenced object
205+
T *get() { return m_ptr; }
206+
207+
/// Return a pointer to the referenced object
208+
const T *get() const { return m_ptr; }
209+
210+
private:
211+
T *m_ptr;
212+
};

‎tests/object_py.h

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#include <nanobind/nanobind.h>
2+
3+
NAMESPACE_BEGIN(nanobind)
4+
NAMESPACE_BEGIN(detail)
5+
6+
template <typename T> struct type_caster<ref<T>> {
7+
using Value = ref<T>;
8+
using Caster = make_caster<T>;
9+
static constexpr auto Name = Caster::Name;
10+
static constexpr bool IsClass = true;
11+
12+
template <typename T_> using Cast = movable_cast_t<T_>;
13+
14+
Value value;
15+
16+
bool from_python(handle src, uint8_t flags,
17+
cleanup_list *cleanup) noexcept {
18+
Caster caster;
19+
if (!caster.from_python(src, flags, cleanup))
20+
return false;
21+
22+
value = Value(caster.operator T *());
23+
24+
return true;
25+
}
26+
27+
static handle from_cpp(const ref<T> &value, rv_policy policy,
28+
cleanup_list *cleanup) noexcept {
29+
return Caster::from_cpp(value.get(), policy, cleanup);
30+
}
31+
32+
explicit operator Value *() { return &value; }
33+
explicit operator Value &() { return value; }
34+
explicit operator Value &&() && { return (Value &&) value; }
35+
};
36+
37+
NAMESPACE_END(detail)
38+
NAMESPACE_END(nanobind)

‎tests/test_intrusive.cpp

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#include <nanobind/nanobind.h>
2+
#include <nanobind/stl/pair.h>
3+
#include <nanobind/trampoline.h>
4+
#include "object.h"
5+
#include "object_py.h"
6+
7+
namespace nb = nanobind;
8+
using namespace nb::literals;
9+
10+
static int test_constructed = 0;
11+
static int test_destructed = 0;
12+
13+
class Test : Object {
14+
public:
15+
Test() {
16+
test_constructed++;
17+
}
18+
19+
virtual ~Test() {
20+
test_destructed++;
21+
}
22+
23+
virtual int value() const { return 123; }
24+
25+
static Test *create_raw() { return new Test(); }
26+
static ref<Test> create_ref() { return new Test(); }
27+
};
28+
29+
class PyTest : Test {
30+
NB_TRAMPOLINE(Test, 1);
31+
virtual int value() const {
32+
NB_OVERRIDE(int, Test, value);
33+
}
34+
};
35+
36+
NB_MODULE(test_intrusive_ext, m) {
37+
object_init_py(
38+
[](PyObject *o) {
39+
nb::gil_scoped_acquire guard;
40+
Py_INCREF(o);
41+
},
42+
[](PyObject *o) {
43+
nb::gil_scoped_acquire guard;
44+
Py_DECREF(o);
45+
});
46+
47+
nb::class_<Object>(
48+
m, "Object",
49+
nb::intrusive_ptr<Object>(
50+
[](Object *o, PyObject *po) { o->set_self_py(po); }));
51+
52+
nb::class_<Test, Object, PyTest>(m, "Test")
53+
.def(nb::init<>())
54+
.def("value", &Test::value)
55+
.def_static("create_raw", &Test::create_raw)
56+
.def_static("create_ref", &Test::create_ref);
57+
58+
m.def("reset", [] {
59+
test_constructed = 0;
60+
test_destructed = 0;
61+
});
62+
63+
m.def("stats", []() -> std::pair<int, int> {
64+
return { test_constructed, test_destructed };
65+
});
66+
67+
m.def("get_value_1", [](Test *o) { ref<Test> x(o); return x->value(); });
68+
m.def("get_value_2", [](ref<Test> x) { return x->value(); });
69+
m.def("get_value_3", [](const ref<Test> &x) { return x->value(); });
70+
}

‎tests/test_intrusive.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import test_intrusive_ext as t
2+
import pytest
3+
import gc
4+
5+
@pytest.fixture
6+
def clean():
7+
gc.collect()
8+
gc.collect()
9+
t.reset()
10+
11+
def test01_construct(clean):
12+
o = t.Test()
13+
assert o.value() == 123
14+
assert t.get_value_1(o) == 123
15+
assert t.get_value_2(o) == 123
16+
assert t.get_value_3(o) == 123
17+
del o
18+
gc.collect()
19+
gc.collect()
20+
assert t.stats() == (1, 1)
21+
22+
23+
def test02_factory(clean):
24+
o = t.Test.create_raw()
25+
assert o.value() == 123
26+
assert t.get_value_1(o) == 123
27+
assert t.get_value_2(o) == 123
28+
assert t.get_value_3(o) == 123
29+
del o
30+
gc.collect()
31+
gc.collect()
32+
assert t.stats() == (1, 1)
33+
34+
35+
def test03_factory_ref(clean):
36+
o = t.Test.create_ref()
37+
assert o.value() == 123
38+
assert t.get_value_1(o) == 123
39+
assert t.get_value_2(o) == 123
40+
assert t.get_value_3(o) == 123
41+
del o
42+
gc.collect()
43+
gc.collect()
44+
assert t.stats() == (1, 1)
45+
46+
def test04_subclass(clean):
47+
class MyTest(t.Test):
48+
def __init__(self, x):
49+
super().__init__()
50+
self.x = x
51+
52+
def value(self):
53+
return self.x
54+
55+
o = MyTest(456)
56+
assert o.value() == 456
57+
assert t.get_value_1(o) == 456
58+
assert t.get_value_2(o) == 456
59+
assert t.get_value_3(o) == 456
60+
del o
61+
gc.collect()
62+
gc.collect()
63+
assert t.stats() == (1, 1)

0 commit comments

Comments
 (0)
Please sign in to comment.