Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Handle dangling objects and rv_policy changes robustly #889

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/api_core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2929,7 +2929,8 @@ The documentation below refers to two per-instance flags with the following mean
set to ``true`` and ``false``.

This is analogous to casting a C++ object with return value policy
:cpp:enumerator:`rv_policy::reference`.
:cpp:enumerator:`rv_policy::reference`, except that it will create a
new Python instance even if one wrapping this object already exists.

If a `parent` object is specified, the instance keeps this parent alive
while the newly created object exists. This is analogous to casting a C++
Expand All @@ -2946,7 +2947,8 @@ The documentation below refers to two per-instance flags with the following mean
``true``.

This is analogous to casting a C++ object with return value policy
:cpp:enumerator:`rv_policy::take_ownership`.
:cpp:enumerator:`rv_policy::take_ownership`, except that it will create a
new Python instance even if one wrapping this object already exists.

.. cpp:function:: void inst_zero(handle h)

Expand Down
7 changes: 7 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ Version TBD (not yet released)
binding abstractions that "feel like" the built-in ones.
(PR `#884 <https://github.com/wjakob/nanobind/pull/884>`__)

* Nanobind now avoids returning an existing non-owning Python instance when
an owning Python instance was requested. Instead, it will create a new
Python instance that holds (unique or shared) ownership of the C++ object,
and allow the existing instance to continue to hold a reference to it.
This also helps in situations where <TODO...>
(PR `#889 <https://github.com/wjakob/nanobind/pull/889>`__)

Version 2.4.0 (Dec 6, 2024)
---------------------------

Expand Down
15 changes: 13 additions & 2 deletions include/nanobind/intrusive/ref.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,23 @@ template <typename T> struct type_caster<nanobind::ref<T>> {

static handle from_cpp(const ref<T> &value, rv_policy policy,
cleanup_list *cleanup) noexcept {
bool force_new = policy == rv_policy::copy || policy == rv_policy::move;

if constexpr (std::is_base_of_v<intrusive_base, T>)
if (policy != rv_policy::copy && policy != rv_policy::move && value.get())
if (!force_new && value.get())
if (PyObject* obj = value->self_py())
return handle(obj).inc_ref();

return Caster::from_cpp(value.get(), policy, cleanup);
/* The combination of rv_policy::_shared_ownership + not asking
for an is_new response, is treated as requiring intrusive ownership.
We could pass any policy here except copy/move, but using this one
allows nanobind to check that the intrusive_ptr() annotation was
specified correctly. */
if (!force_new)
policy = rv_policy::_shared_ownership;

return Caster::from_cpp_raw((std::decay_t<T> *) value.get(), policy,
cleanup, nullptr);
}
};
NAMESPACE_END(detail)
Expand Down
13 changes: 10 additions & 3 deletions include/nanobind/nb_cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,14 @@ template <typename Type_> struct type_caster_base : type_caster_base_tag {
else
ptr = (Type *) &value;

policy = infer_policy<T>(policy);
return from_cpp_raw(ptr, infer_policy<T>(policy), cleanup, nullptr);
}

/* Version of from_cpp that uses the rv_policy you give it as-is,
without resolving through infer_policy */
NB_INLINE static handle from_cpp_raw(Type *ptr, rv_policy policy,
cleanup_list *cleanup,
bool *is_new) noexcept {
const std::type_info *type = &typeid(Type);

constexpr bool has_type_hook =
Expand All @@ -490,11 +497,11 @@ template <typename Type_> struct type_caster_base : type_caster_base_tag {
type = type_hook<Type>::get(ptr);

if constexpr (!std::is_polymorphic_v<Type>) {
return nb_type_put(type, ptr, policy, cleanup);
return nb_type_put(type, ptr, policy, cleanup, is_new);
} else {
const std::type_info *type_p =
(!has_type_hook && ptr) ? &typeid(*ptr) : nullptr;
return nb_type_put_p(type, type_p, ptr, policy, cleanup);
return nb_type_put_p(type, type_p, ptr, policy, cleanup, is_new);
}
}

Expand Down
17 changes: 16 additions & 1 deletion include/nanobind/nb_enums.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,24 @@ enum class rv_policy {
move,
reference,
reference_internal,
none
none,
/* Note to self: nb_func.h assumes that this value fits into 3 bits,
hence no further policies can be added. */

/* Special internal-use policy that can only be passed to `nb_type_put()`
or `type_caster_base<T>::from_cpp_raw()`. (To keep rv_policy fitting
in 3 bits, it aliases `automatic_reference`, which would always have
been converted into `copy` or `move` or `reference` before reaching
those functions.) It means we are creating a Python instance that holds
shared ownership of the C++ object we're casting, so we shouldn't reuse
a Python instance that only holds a reference to that object. The actual
enforcement of the shared ownership must be done separately after the
cast completes, via a keep_alive callback or similar, to ensure that
the C++ object lives at least as long as the new Python instance does.

This is an internal tool that is not part of nanobind's public API,
and may be changed without notice. */
_shared_ownership = automatic_reference
};

NAMESPACE_END(NB_NAMESPACE)
19 changes: 2 additions & 17 deletions include/nanobind/stl/shared_ptr.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,8 @@ template <typename T> struct type_caster<std::shared_ptr<T>> {
handle result;

Td *ptr = (Td *) value.get();
const std::type_info *type = &typeid(Td);

constexpr bool has_type_hook =
!std::is_base_of_v<std::false_type, type_hook<Td>>;
if constexpr (has_type_hook)
type = type_hook<Td>::get(ptr);

if constexpr (!std::is_polymorphic_v<Td>) {
result = nb_type_put(type, ptr, rv_policy::reference,
cleanup, &is_new);
} else {
const std::type_info *type_p =
(!has_type_hook && ptr) ? &typeid(*ptr) : nullptr;

result = nb_type_put_p(type, type_p, ptr, rv_policy::reference,
cleanup, &is_new);
}
result = Caster::from_cpp_raw(ptr, rv_policy::_shared_ownership,
cleanup, &is_new);

if (is_new) {
std::shared_ptr<void> pp;
Expand Down
10 changes: 8 additions & 2 deletions src/nb_internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ struct func_data : func_data_prelim<0> {
struct nb_inst { // usually: 24 bytes
PyObject_HEAD

/// Offset to the actual instance data
/// Offset to the actual C++ object. A value of zero here is special;
/// it means this instance was "detached" (assumed dangling) because its
/// C++ object address collided with a newly allocated internal instance.
/// Detached instances are not stored in the inst_c2p map.
int32_t offset;

/// State of the C++ object this instance points to: is it constructed?
Expand Down Expand Up @@ -87,8 +90,11 @@ struct nb_inst { // usually: 24 bytes
/// Does this instance use intrusive reference counting?
uint32_t intrusive : 1;

/// Does this instance hold partial/shared ownership of its C++ object?
uint32_t shared_ownership : 1;

// That's a lot of unused space. I wonder if there is a good use for it..
uint32_t unused : 24;
uint32_t unused : 23;
};

static_assert(sizeof(nb_inst) == sizeof(PyObject) + sizeof(uint32_t) * 2);
Expand Down
Loading
Loading