/* Type caster for opencascade::handle — the ownership core of this binding. OCCT transients carry their own intrusive, atomic reference count (Standard_Transient), so a handle keeps an object alive entirely on the C++ side. That is what lets us release the GIL around kernel calls: OCCT may copy handles on its own worker threads without ever touching Python. The design mirrors nanobind's own stl/shared_ptr.h caster: C++ -> Python the wrapper is a *non-owning* nanobind instance pointing at the C++ object, plus one handle stored in the object's keep-alive list. Python holds exactly one OCCT reference per wrapper, released at deallocation. An existing wrapper is reused (is_new == false), so `a is b` holds for as long as a wrapper stays alive. Python -> C++ a plain handle copy (one incref), balanced when the caster dies after the call. Unlike shared_ptr we do *not* need to keep the PyObject alive: the OCCT refcount, not the Python instance, owns the object's memory. That last sentence is only true if every wrapped transient was heap allocated and is handle-owned. See occt_transient.h — transient constructors are bound through ocp_new(), never nb::init<>, so Python never owns transient storage. from_python re-checks the invariant rather than trusting it, because the failure mode is a double free. */ #pragma once #include #include #include #include NAMESPACE_BEGIN(NB_NAMESPACE) NAMESPACE_BEGIN(detail) // Marked NB_NOINLINE so the (identical) body is not duplicated into every // instantiation of the caster below — there is one per bound transient class. inline NB_NOINLINE void occt_handle_keep_alive(opencascade::handle &&h, PyObject *o) noexcept { keep_alive(o, new opencascade::handle(std::move(h)), [](void *p) noexcept { delete (opencascade::handle *) p; }); } template struct type_caster< opencascade::handle, enable_if_t>>> { static constexpr bool IsClass = true; using Caster = make_caster; using Td = std::decay_t; NB_TYPE_CASTER(opencascade::handle, Caster::Name) static_assert(is_base_caster_v, "Conversion of opencascade::handle requires that T is " "bound through nanobind's regular class mechanism."); bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept { // A null handle is OCCT's absent value and maps to None in both // directions; several APIs (BRep_Tool::Surface on a degenerate edge, // for one) legitimately return one. if (src.is_none()) { value = Value(); return true; } flags &= ~((uint8_t) cast_flags::convert); Caster caster; if (!caster.from_python(src, flags, cleanup)) return false; Td *ptr = caster.operator Td *(); // Invariant check, not defensive programming: a refcount of zero means // this wrapper owns its storage (nb::init<>, or a by-value return), // and taking a handle to it would hand OCCT the right to `delete` a // nanobind instance's memory. Refuse instead — a TypeError beats a // heap corruption, and it can only be reached by a binding bug. if (ptr && ptr->GetRefCount() == 0) { assert(!"OCCT transient is not handle-owned (see occt_transient.h)"); return false; } value = Value(ptr); return true; } static handle from_cpp(const Value &v, rv_policy, cleanup_list *cleanup) noexcept { Td *ptr = v.get(); if (!ptr) return none().release(); // Transients are polymorphic, so nb_type_put_p downcasts on the // dynamic type: BRep_Tool::Surface returning a Geom_Surface handle to // a plane arrives in Python as Geom_Plane when that class is bound. bool is_new = false; handle result = nb_type_put_p(&typeid(Td), &typeid(*ptr), (void *) ptr, rv_policy::reference, cleanup, &is_new); // Only a freshly created wrapper takes a reference. Attaching one per // conversion would pile up redundant handles on a long-lived object // that crosses the boundary many times. if (result.is_valid() && is_new) occt_handle_keep_alive( opencascade::handle(ptr), result.ptr()); return result; } }; NAMESPACE_END(detail) NAMESPACE_END(NB_NAMESPACE)