Skip to content

Commit e80edb1

Browse files
committed
nb::[try_]cast: improved handling of implicit conversions
The ``nb::cast`` and ``nb::try_cast`` function may perform implicit conversions of their input argument when called with ``convert=true``. However, more complex chains of conversions that requiring lifetime from the ``detail::cleanup_list`` were rejected. This commit fixes this confusing corner case.
1 parent 5259872 commit e80edb1

File tree

3 files changed

+72
-34
lines changed

3 files changed

+72
-34
lines changed

docs/porting.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,9 @@ changes are needed:
284284

285285
Note that the cleanup list is only available when ``from_python()`` or
286286
``from_cpp()`` are called as part of function dispatch, while usage by
287-
:cpp:func:`nb::cast() <cast>` sets ``cleanup`` to ``nullptr``. This case should
288-
be handled gracefully by refusing the conversion if the cleanup list is
289-
absolutely required.
287+
:cpp:func:`nb::cast() <cast>` may set ``cleanup`` to ``nullptr`` if implicit
288+
conversions are not enabled. This case should be handled gracefully by refusing
289+
the conversion if the cleanup list is absolutely required.
290290

291291
Type casters may not raise C++ exceptions. Both ``from_python()`` and
292292
``from_cpp()`` must be annotated with ``noexcept``. Exceptions or failure

include/nanobind/eigen/dense.h

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ struct type_caster<T, enable_if_t<is_eigen_plain_v<T> &&
198198
owner = capsule(temp, [](void *p) noexcept { delete (T *) p; });
199199
ptr = temp->data();
200200
policy = rv_policy::reference;
201-
} else if (policy == rv_policy::reference_internal) {
201+
} else if (policy == rv_policy::reference_internal && cleanup->self()) {
202202
owner = borrow(cleanup->self());
203203
policy = rv_policy::reference;
204204
}
@@ -417,15 +417,17 @@ struct type_caster<Eigen::Ref<T, Options, StrideType>,
417417
if constexpr (MaybeConvert) {
418418
/* Generating an implicit copy requires some object to assume
419419
ownership. During a function call, ``dcaster`` can serve that
420-
role (this case is detected by checking whether ``cleanup`` is
421-
defined). When used in other situatons (e.g. ``nb::cast()``),
422-
the created ``Eigen::Ref<..>`` must take ownership of the copy.
423-
This is only guranteed to work if DMapConstructorOwnsData.
420+
role (this case is detected by checking whether ``flags`` has
421+
the ``manual`` flag set). When used in other situations (e.g.
422+
``nb::cast()``), the created ``Eigen::Ref<..>`` must take
423+
ownership of the copy. This is only guranteed to work if
424+
DMapConstructorOwnsData.
424425
425426
If neither of these is possible, we disable implicit
426427
conversions. */
427428

428-
if (!cleanup && !DMapConstructorOwnsData)
429+
if ((flags & (uint8_t) cast_flags::manual) &&
430+
!DMapConstructorOwnsData)
429431
flags &= ~(uint8_t) cast_flags::convert;
430432

431433
if (dcaster.from_python_(src, flags, cleanup))

include/nanobind/nb_cast.h

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ enum cast_flags : uint8_t {
4040

4141
// Don't accept 'None' Python objects in the base class caster
4242
none_disallowed = (1 << 2),
43+
44+
// Indicates that this cast is performed by nb::cast or nb::try_cast
45+
manual = (1 << 3)
4346
};
4447

4548
/**
@@ -411,53 +414,86 @@ template <typename Type_> struct type_caster_base : type_caster_base_tag {
411414
template <typename Type, typename SFINAE>
412415
struct type_caster : type_caster_base<Type> { };
413416

414-
NAMESPACE_END(detail)
417+
template <bool Convert, typename T>
418+
T cast_impl(handle h) {
419+
using Caster = detail::make_caster<T>;
415420

416-
template <typename T, typename Derived>
417-
bool try_cast(const detail::api<Derived> &value, T &out, bool convert = true) noexcept {
421+
static_assert(
422+
!(std::is_reference_v<T> || std::is_pointer_v<T>) ||
423+
detail::is_base_caster_v<Caster> ||
424+
std::is_same_v<const char *, T>,
425+
"nanobind::cast(): cannot return a reference to a temporary.");
426+
427+
Caster caster;
428+
bool rv;
429+
if constexpr (Convert) {
430+
cleanup_list cleanup(nullptr);
431+
rv = caster.from_python(h.ptr(),
432+
((uint8_t) cast_flags::convert) |
433+
((uint8_t) cast_flags::manual),
434+
&cleanup);
435+
cleanup.release(); // 'from_python' is 'noexcept', so this always runs
436+
} else {
437+
rv = caster.from_python(h.ptr(), (uint8_t) cast_flags::manual, nullptr);
438+
}
439+
440+
if (!rv)
441+
detail::raise_cast_error();
442+
return caster.operator detail::cast_t<T>();
443+
}
444+
445+
template <bool Convert, typename T>
446+
bool try_cast_impl(handle h, T &out) noexcept {
418447
using Caster = detail::make_caster<T>;
419448

420449
static_assert(!std::is_same_v<const char *, T>,
421450
"nanobind::try_cast(): cannot return a reference to a temporary.");
422451

423452
Caster caster;
424-
if (caster.from_python(value.derived().ptr(),
425-
convert ? (uint8_t) detail::cast_flags::convert
426-
: (uint8_t) 0, nullptr)) {
453+
bool rv;
454+
if constexpr (Convert) {
455+
cleanup_list cleanup(nullptr);
456+
rv = caster.from_python(h.ptr(),
457+
((uint8_t) cast_flags::convert) |
458+
((uint8_t) cast_flags::manual),
459+
&cleanup);
460+
cleanup.release(); // 'from_python' is 'noexcept', so this always runs
461+
} else {
462+
rv = caster.from_python(h.ptr(), (uint8_t) cast_flags::manual, nullptr);
463+
}
464+
465+
if (rv) {
427466
try {
428467
out = caster.operator detail::cast_t<T>();
429468
return true;
430-
} catch (const builtin_exception&) {
431-
return false;
432-
}
469+
} catch (const builtin_exception&) { }
433470
}
434471

435472
return false;
436473
}
437474

475+
NAMESPACE_END(detail)
476+
438477
template <typename T, typename Derived>
439-
T cast(const detail::api<Derived> &value, bool convert = true) {
478+
NB_INLINE T cast(const detail::api<Derived> &value, bool convert = true) {
440479
if constexpr (std::is_same_v<T, void>) {
441480
return;
442481
} else {
443-
using Caster = detail::make_caster<T>;
444-
445-
static_assert(
446-
!(std::is_reference_v<T> || std::is_pointer_v<T>) ||
447-
detail::is_base_caster_v<Caster> ||
448-
std::is_same_v<const char *, T>,
449-
"nanobind::cast(): cannot return a reference to a temporary.");
450-
451-
Caster caster;
452-
if (!caster.from_python(value.derived().ptr(),
453-
convert ? (uint8_t) detail::cast_flags::convert
454-
: (uint8_t) 0, nullptr))
455-
detail::raise_cast_error();
456-
457-
return caster.operator detail::cast_t<T>();
482+
if (convert)
483+
return detail::cast_impl<true, T>(value);
484+
else
485+
return detail::cast_impl<false, T>(value);
458486
}
459487
}
460488

489+
template <typename T, typename Derived>
490+
NB_INLINE bool try_cast(const detail::api<Derived> &value, T &out, bool convert = true) noexcept {
491+
if (convert)
492+
return detail::try_cast_impl<true, T>(value, out);
493+
else
494+
return detail::try_cast_impl<false, T>(value, out);
495+
}
496+
461497
template <typename T>
462498
object cast(T &&value, rv_policy policy = rv_policy::automatic_reference) {
463499
handle h = detail::make_caster<T>::from_cpp((detail::forward_t<T>) value,

0 commit comments

Comments
 (0)