From 1c174348395d62fadf9230b0419f039d571f7642 Mon Sep 17 00:00:00 2001 From: Wenzel Jakob Date: Fri, 14 Oct 2022 17:08:55 +0200 Subject: [PATCH] nb_inst_wrap(): addendum to low-level interface --- docs/lowlevel.md | 34 +++++++++++++++++++++++++++++++--- include/nanobind/nb_class.h | 1 + include/nanobind/nb_lib.h | 3 +++ src/nb_type.cpp | 8 ++++++++ tests/test_classes.cpp | 8 +++++++- tests/test_classes.py | 9 +++++---- 6 files changed, 55 insertions(+), 8 deletions(-) diff --git a/docs/lowlevel.md b/docs/lowlevel.md index 09bdbe984..cb0b79a6b 100644 --- a/docs/lowlevel.md +++ b/docs/lowlevel.md @@ -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); @@ -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: @@ -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 @@ -141,3 +141,31 @@ flags individually. void inst_set_state(handle h, bool ready, bool destruct); std::pair 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(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); +``` diff --git a/include/nanobind/nb_class.h b/include/nanobind/nb_class.h index 362fec842..f002398a0 100644 --- a/include/nanobind/nb_class.h +++ b/include/nanobind/nb_class.h @@ -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 inst_state(handle h) { return detail::nb_inst_state(h.ptr()); } diff --git a/include/nanobind/nb_lib.h b/include/nanobind/nb_lib.h index 7fb2bffcb..0ccf864a0 100644 --- a/include/nanobind/nb_lib.h +++ b/include/nanobind/nb_lib.h @@ -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; diff --git a/src/nb_type.cpp b/src/nb_type.cpp index 4ca73a687..e8c3a699a 100644 --- a/src/nb_type.cpp +++ b/src/nb_type.cpp @@ -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); } @@ -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 nb_inst_state(PyObject *o) noexcept { diff --git a/tests/test_classes.cpp b/tests/test_classes.cpp index 94c12a818..06c4dafb4 100644 --- a/tests/test_classes.cpp +++ b/tests/test_classes.cpp @@ -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 diff --git a/tests/test_classes.py b/tests/test_classes.py index 5cb4a5406..765bdcc0c 100644 --- a/tests/test_classes.py +++ b/tests/test_classes.py @@ -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 )