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 nb::class_<...>
binding declaration,
the nb::type<T>()
template function can be used to look up
the Python type object associated with a C++ class named MyClass
.
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 nb::type_check()
, which is
redundant in this case):
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 nb::type_size()
, nb::type_align()
, and
nb::type_info()
in situations where this is useful.
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 nb::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.
nb::object py_inst = nb::inst_alloc(py_type);
We can confirm via nb::inst_check()
that this newly
created instance is managed by nanobind, that it has the correct type in
Python. Calling nb::inst_ready()
reveals that the
ready flag of the instance is set to false
(i.e., it is still
uninitialized).
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 nb::inst_zero()
function can be used to zero-initialize the object and mark it
as ready.
nb::inst_zero(py_inst);
assert(nb::inst_ready(py_inst));
We can destruct this default instance via nb::inst_destruct()
and convert it back to non-ready status. This memory region
can then be reinitialized once more.
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” operator) to construct an instance in-place into the memory region allocated by nanobind.
// 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 nb::inst_mark_ready()
. When its reference count reaches zero, nanobind will then
automatically call the in-place destructor (MyClass::~MyClass
).
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).
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 nb::inst_copy()
and nb::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 nb::inst_mark_ready()
but
compiles to more compact code (the nb::class_<MyClass>
declaration had already created bindings for both constructors, and this simply
calls those bindings).
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 nb::inst_replace_copy()
and nb::inst_replace_move()
destruct an
initialized instance and replace it with the contents of another by either
copying or moving.
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 nb::type_check()
and
nb::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 nb::type_name()
and
nb::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:
ready
: is the object fully constructed? If set tofalse
, nanobind will raise an exception when the object is passed to a bound C++ function.destruct
: Should nanobind call the C++ destructor when the instance is garbage collected?
The functions nb::inst_zero()
,
nb::inst_mark_ready()
, nb::inst_move()
, and nb::inst_copy()
set both of these
flags to true
, and nb::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
nb::rv_policy::reference_internal
return value policy. The library therefore
exposes two more functions nb::inst_state()
and
nb::inst_set_state()
that can be used to access
them individually.
Referencing existing instances¶
The above examples used the function nb::inst_alloc()
to allocate a Python object along with space to hold a C++ instance associated
with the binding py_type
.
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 nb::inst_reference()
and
nb::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.
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);
Supplemental type data¶
nanobind can stash supplemental data inside the type object of bound types.
This involves the nb::supplement<T>()
class binding
annotation to reserve space and nb::type_supplement<T>()
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:
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 nb::supplement<T>()
annotation implicitly also
passes nb::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.