Skip to content

Commit

Permalink
nb_inst_wrap(): addendum to low-level interface
Browse files Browse the repository at this point in the history
  • Loading branch information
wjakob committed Oct 14, 2022
1 parent e33d9c8 commit a2b8b20
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 8 deletions.
34 changes: 31 additions & 3 deletions docs/lowlevel.md
Expand Up @@ -32,7 +32,8 @@ assert(nb::type_size(py_type) == sizeof(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 be passed to bound C++ functions to prevent undefined behavior.
however, not (yet) be passed to bound C++ functions to prevent undefined
behavior. It must first be initialized.

```cpp
nb::object py_inst = nb::inst_alloc(py_type);
Expand Down Expand Up @@ -115,7 +116,6 @@ 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.
# Even lower-level interface
Every nanobind object has two important flags that control its behavior:
Expand All @@ -130,7 +130,7 @@ 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 is invoked when the instance
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
Expand All @@ -141,3 +141,31 @@ flags individually.
void inst_set_state(handle h, bool ready, bool destruct);
std::pair<bool, bool> inst_state(handle h);
```

# Referencing existing instances

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

```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 ``nb::inst_wrap()`` function—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.

```cpp
MyClass *inst = new MyClass();
nb::object py_inst = nb::inst_wrap(py_type, inst);

// Mark as ready, garbage-collecting 'py_inst' will
// cause 'inst' to be deleted as well
nb::inst_mark_ready(py_inst);
```
1 change: 1 addition & 0 deletions include/nanobind/nb_class.h
Expand Up @@ -471,6 +471,7 @@ inline T &type_supplement(handle h) { return *(T *) detail::nb_type_supplement(h
// Low level access to nanobind instance objects
inline bool inst_check(handle h) { return type_check(h.type()); }
inline object inst_alloc(handle h) { return steal(detail::nb_inst_alloc((PyTypeObject *) h.ptr())); }
inline object inst_wrap(handle h, void *p) { return steal(detail::nb_inst_wrap((PyTypeObject *) h.ptr(), p)); }
inline void inst_zero(handle h) { detail::nb_inst_zero(h.ptr()); }
inline void inst_set_state(handle h, bool ready, bool destruct) { detail::nb_inst_set_state(h.ptr(), ready, destruct); }
inline std::pair<bool, bool> inst_state(handle h) { return detail::nb_inst_state(h.ptr()); }
Expand Down
3 changes: 3 additions & 0 deletions include/nanobind/nb_lib.h
Expand Up @@ -258,6 +258,9 @@ NB_CORE PyObject *nb_type_lookup(const std::type_info *t) noexcept;
/// Allocate an instance of type 't'
NB_CORE PyObject *nb_inst_alloc(PyTypeObject *t);

/// Allocate an instance of type 't' referencing the existing 'ptr'
NB_CORE PyObject *nb_inst_wrap(PyTypeObject *t, void *ptr);

/// Call the destructor of the given python object
NB_CORE void nb_inst_destruct(PyObject *o) noexcept;

Expand Down
8 changes: 8 additions & 0 deletions src/nb_type.cpp
Expand Up @@ -1010,6 +1010,13 @@ PyObject *nb_inst_alloc(PyTypeObject *t) {
return result;
}

PyObject *nb_inst_wrap(PyTypeObject *t, void *ptr) {
PyObject *result = inst_new_impl(t, ptr);
if (!result)
raise_python_error();
return result;
}

void *nb_inst_ptr(PyObject *o) noexcept {
return inst_ptr((nb_inst *) o);
}
Expand All @@ -1025,6 +1032,7 @@ void nb_inst_set_state(PyObject *o, bool ready, bool destruct) noexcept {
nb_inst *nbi = (nb_inst *) o;
nbi->ready = ready;
nbi->destruct = destruct;
nbi->cpp_delete = destruct && !nbi->internal;
}

std::pair<bool, bool> nb_inst_state(PyObject *o) noexcept {
Expand Down
8 changes: 7 additions & 1 deletion tests/test_classes.cpp
Expand Up @@ -359,7 +359,13 @@ NB_MODULE(test_classes_ext, m) {
if (!nb::inst_ready(py_inst))
throw std::runtime_error("Internal error! (7)");

return std::make_pair(py_inst, py_inst_2);
nb::object py_inst_3 = nb::inst_wrap(py_type, new Struct(345));
if (!(nb::inst_check(py_inst_3) && py_inst_3.type().is(py_type) &&
!nb::inst_ready(py_inst_3)))
throw std::runtime_error("Internal error! (2)");
nb::inst_mark_ready(py_inst_3);

return nb::make_tuple(py_inst, py_inst_2, py_inst_3);
});

// test22_handle_t
Expand Down
9 changes: 5 additions & 4 deletions tests/test_classes.py
Expand Up @@ -413,15 +413,16 @@ def test20_type_callback():


def test21_low_level(clean):
s1, s2 = t.test_lowlevel()
assert s1.value() == 123 and s2.value() == 0
s1, s2, s3 = t.test_lowlevel()
assert s1.value() == 123 and s2.value() == 0 and s3.value() == 345
del s1
del s2
del s3
assert_stats(
value_constructed=1,
value_constructed=2,
copy_constructed=1,
move_constructed=1,
destructed=3
destructed=4
)


Expand Down

0 comments on commit a2b8b20

Please sign in to comment.