File: lowlevel.rst

package info (click to toggle)
nanobind 2.9.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,060 kB
  • sloc: cpp: 11,838; python: 5,862; ansic: 4,820; makefile: 22; sh: 15
file content (323 lines) | stat: -rw-r--r-- 13,032 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
.. _lowlevel:

.. cpp:namespace:: nanobind

Low-level interface
===================

nanobind exposes a low-level interface to provide fine-grained control over
the sequence of steps that instantiates a Python object wrapping a C++
instance. This is useful when writing generic binding code that manipulates
nanobind-based objects of various types.

Given a previous :cpp:class:`nb::class_\<...\> <class_>` binding declaration,
the :cpp:func:`nb::type\<T\>() <type>` template function can be used to look up
the Python type object associated with a C++ class named ``MyClass``.

.. code-block:: cpp

   nb::handle py_type = nb::type<MyClass>();

In the case of failure, this line will return a ``nullptr`` pointer, which
can be checked via ``py_type.is_valid()``. We can verify that the type
lookup succeeded, and that the returned instance indeed represents a
nanobind-owned type (via :cpp:func:`nb::type_check() <type_check>`, which is
redundant in this case):

.. code-block:: cpp

   assert(py_type.is_valid() && nb::type_check(py_type));

nanobind knows the size, alignment, and C++ RTTI ``std::type_info`` record of
all bound types. They can be queried on the fly via :cpp:func:`nb::type_size()
<type_size>`, :cpp:func:`nb::type_align() <type_size>`, and
:cpp:func:`nb::type_info() <type_size>` in situations where this is useful.

.. code-block:: cpp

   assert(nb::type_size(py_type) == sizeof(MyClass) &&
          nb::type_align(py_type) == alignof(MyClass) &&
          nb::type_info(py_type) == typeid(MyClass));

Given a type object representing a C++ type, we can create an uninitialized
instance via :cpp:func:`nb::inst_alloc() <inst_alloc>`. This is an ordinary
Python object that can, however, not (yet) be passed to bound C++ functions
to prevent undefined behavior. It must first be initialized.

.. code-block:: cpp

   nb::object py_inst = nb::inst_alloc(py_type);

We can confirm via :cpp:func:`nb::inst_check() <inst_check>` that this newly
created instance is managed by nanobind, that it has the correct type in
Python. Calling :cpp:func:`nb::inst_ready() <inst_ready>` reveals that the
*ready* flag of the instance is set to ``false`` (i.e., it is still
uninitialized).

.. code-block:: cpp

   assert(nb::inst_check(py_inst) &&
          py_inst.type().is(py_type) &&
          !nb::inst_ready(py_inst));

For simple *plain old data* (POD) types, the :cpp:func:`nb::inst_zero()
<inst_zero>` function can be used to *zero-initialize* the object and mark it
as ready.

.. code-block:: cpp

   nb::inst_zero(py_inst);
   assert(nb::inst_ready(py_inst));

We can destruct this default instance via :cpp:func:`nb::inst_destruct()
<inst_destruct>` and convert it back to non-ready status. This memory region
can then be reinitialized once more.

.. code-block:: cpp

   nb::inst_destruct(py_inst);
   assert(!nb::inst_ready(py_inst));

What follows is a more interesting example, where we use a lesser-known feature
of C++ (the "`placement new <https://en.wikipedia.org/wiki/Placement_syntax>`_"
operator) to construct an instance *in-place* into the memory region allocated
by nanobind.

.. code-block:: cpp

   // Get a C++ pointer to the uninitialized instance data
   MyClass *ptr = nb::inst_ptr<MyClass>(py_inst);

   // Perform an in-place construction of the C++ object at address 'ptr'
   new (ptr) MyClass(/* constructor arguments go here */);

Following this constructor call, we must inform nanobind that the instance
object is now fully constructed via :cpp:func:`nb::inst_mark_ready()
<inst_mark_ready>`. When its reference count reaches zero, nanobind will then
automatically call the in-place destructor (``MyClass::~MyClass``).

.. code-block:: cpp

   nb::inst_mark_ready(py_inst);
   assert(nb::inst_ready(py_inst));

Let’s destroy this instance once more manually (which will, again, call
the C++ destructor and mark the Python object as non-ready).

.. code-block:: cpp

   nb::inst_destruct(py_inst);

Another useful feature is that nanobind can copy- or move-construct ``py_inst``
from another instance of the same type via :cpp:func:`nb::inst_copy()
<inst_copy>` and :cpp:func:`nb::inst_move() <inst_move>`. These functions call
the C++ copy or move constructor and transition ``py_inst`` back to ``ready``
status. This is equivalent to calling an in-place version of these constructors
followed by a call to :cpp:func:`nb::inst_mark_ready() <inst_mark_ready>` but
compiles to more compact code (the :cpp:class:`nb::class_\<MyClass\> <class_>`
declaration had already created bindings for both constructors, and this simply
calls those bindings).

.. code-block:: cpp

   if (copy_instance)
       nb::inst_copy(/* dst = */ py_inst, /* src = */ some_other_instance);
   else
       nb::inst_move(/* dst = */ py_inst, /* src = */ some_other_instance);

Both functions assume that the destination object is uninitialized. Two
alternative versions :cpp:func:`nb::inst_replace_copy() <inst_replace_copy>`
and :cpp:func:`nb::inst_replace_move() <inst_replace_move>` destruct an
initialized instance and replace it with the contents of another by either
copying or moving.

.. code-block:: cpp

   if (copy_instance)
       nb::inst_replace_copy(/* dst = */ py_inst, /* src = */ some_other_instance);
   else
       nb::inst_replace_move(/* dst = */ py_inst, /* src = */ some_other_instance);

Note that these functions are all *unsafe* in the sense that they do not
verify that their input arguments are valid. This is done for
performance reasons, and such checks (if needed) are therefore the
responsibility of the caller. Functions labeled ``nb::type_*`` should
only be called with nanobind type objects, and functions labeled
``nb::inst_*`` should only be called with nanobind instance objects.

The functions :cpp:func:`nb::type_check() <type_check>` and
:cpp:func:`nb::inst_check() <inst_check>` are exceptions to this rule:
they accept any Python object and test whether something is a nanobind type or
instance object.

Two further functions :cpp:func:`nb::type_name() <type_name>` and
:cpp:func:`nb::inst_name() <inst_name>` determine the type name associated with
a type or instance thereof. These also accept non-nanobind types and instances.

Even lower-level interface
--------------------------

Every nanobind object has two important flags that control its behavior:

1. ``ready``: is the object fully constructed? If set to ``false``,
   nanobind will raise an exception when the object is passed to a bound
   C++ function.

2. ``destruct``: Should nanobind call the C++ destructor when the
   instance is garbage collected?

The functions :cpp:func:`nb::inst_zero() <inst_zero>`,
:cpp:func:`nb::inst_mark_ready() <inst_mark_ready>`, :cpp:func:`nb::inst_move()
<inst_move>`, and :cpp:func:`nb::inst_copy() <inst_copy>` set both of these
flags to ``true``, and :cpp:func:`nb::inst_destruct() <inst_destruct>` sets
both of them to ``false``.

In rare situations, the destructor should *not* be invoked when the instance is
garbage collected, for example when working with a nanobind instance
representing a field of a parent instance created using the
:cpp:enumerator:`nb::rv_policy::reference_internal
<rv_policy::reference_internal>` return value policy. The library therefore
exposes two more functions :cpp:func:`nb::inst_state() <inst_state>` and
:cpp:func:`nb::inst_set_state() <inst_set_state>` that can be used to access
them individually.

Referencing existing instances
------------------------------

The above examples used the function :cpp:func:`nb::inst_alloc() <inst_alloc>`
to allocate a Python object along with space to hold a C++ instance associated
with the binding ``py_type``.

.. code-block:: cpp

   nb::object py_inst = nb::inst_alloc(py_type);

   // Next, perform a C++ in-place construction into the
   // address given by nb::inst_ptr<MyClass>(py_inst)
   ... omitted, see the previous examples ...

What if the C++ instance already exists? nanobind also supports this case via
the :cpp:func:`nb::inst_reference() <inst_reference>` and
:cpp:func:`nb::inst_take_ownership() <inst_take_ownership>` functions—in this
case, the Python object references the existing memory region, which is
potentially (slightly) less efficient due to the need for an extra indirection.

.. code-block:: cpp

   MyClass *inst = new MyClass();

   // Transfer ownership of 'inst' to Python (which will use a delete
   // expression to free it when the Python instance is garbage collected)
   nb::object py_inst = nb::inst_take_ownership(py_type, inst);

   // We can also wrap C++ instances that should not be destructed since
   // they represent offsets into another data structure. In this case,
   // the optional 'parent' parameter ensures that 'py_inst' remains alive
   // while 'py_subinst' exists to prevent undefined behavior.
   nb::object py_subinst = nb::inst_reference(
       py_field_type, &inst->field, /* parent = */ py_inst);

.. _supplement:

Supplemental type data
----------------------

nanobind can stash supplemental data *inside* the type object of bound types.
This involves the :cpp:class:`nb::supplement\<T\>() <supplement>` class binding
annotation to reserve space and :cpp:func:`nb::type_supplement\<T\>()
<type_supplement>` to access the reserved memory region.

An example use of this fairly advanced feature are libraries that register
large numbers of different types (e.g. flavors of tensors). A single
generically implemented function can then query the supplemental data block to
handle each tensor type slightly differently.

Here is what this might look like in an implementation:

.. code-block:: cpp

  struct MyTensorMetadata {
      bool stored_on_gpu;
      // ..
      // should be a POD (plain old data) type
  };

  // Register a new type MyTensor, and reserve space for sizeof(MyTensorMedadata)
  nb::class_<MyTensor> cls(m, "MyTensor", nb::supplement<MyTensorMedadata>())

  /// Mutable reference to 'MyTensorMedadata' portion in Python type object
  MyTensorMedadata &supplement = nb::type_supplement<MyTensorMedadata>(cls);
  supplement.stored_on_gpu = true;

The :cpp:class:`nb::supplement\<T\>() <supplement>` annotation implicitly also
passes :cpp:class:`nb::is_final() <is_final>` to ensure that type objects with
supplemental data cannot be subclassed in Python.

nanobind requires that the specified type ``T`` be trivially default
constructible. It zero-initializes the supplement when the type is first
created but does not perform any further custom initialization or destruction.
You can fill the supplement with different contents following the type
creation, e.g., using the placement new operator.

The contents of the supplemental data are not directly visible to Python's
cyclic garbage collector, which creates challenges if you want to reference
Python objects. The recommended workaround is to store the Python objects
as attributes of the type object (in its ``__dict__``) and store a borrowed
``PyObject*`` reference in the supplemental data. If you use an attribute
name that begins with the symbol ``@``, then nanobind will prevent Python
code from rebinding or deleting the attribute after it has been set, making
the borrowed reference reasonably safe.

.. _typeslots:

Customizing type creation
=========================

nanobind exposes a low-level interface to install custom *type slots*
(``PyType_Slot`` in the `CPython API
<https://docs.python.org/3/c-api/type.html#c.PyType_Slot>`_) in newly
constructed types. This provides an escape hatch to realize features that were
not foreseen in the design of this library.

To use this feature, specify the :cpp:class:`nb::type_slots() <type_slots>`
annotation when creating the type.

.. code-block:: cpp

   nb::class_<MyClass>(m, "MyClass", nb::type_slots(slots));

Here, ``slots`` should refer to an array of function pointers that are tagged
with a corresponding slot identifier. For example, here is an example
function that overrides the addition operator so that it behaves like a
multiplication.

.. code-block:: cpp

   PyObject *myclass_tp_add(PyObject *a, PyObject *b) {
       return PyNumber_Multiply(a, b);
   }

   PyType_Slot slots[] = {
       { Py_nb_add, (void *) myclass_tp_add },
       { 0, nullptr }
   };

The ``slots`` array specified in the previous
:cpp:class:`nb::class_\<MyClass\>() <class_>` declaration references the
function ``myclass_tp_add`` and is followed by a mandatory null terminator.
Information on type slots can be found in the CPython documentation sections
covering `type objects <https://docs.python.org/3/c-api/typeobj.html>`_ and
`type construction <https://docs.python.org/3/c-api/type.html>`_.

This example is contrived because it could have been accomplished using
builtin features:

.. code-block:: cpp

   nb::class_<MyClass>(m, "MyClass")
       .def("__add__",
            [](const MyClass &a, const MyClass &b) { return a * b; },
            nb::is_operator())

The documentation section on :ref:`reference leaks <refleaks>` discusses
another important use case of type slots.