Trust & type casters¶
The bindability gate is conservative on purpose: it rejects any
program-defined type it can't see welded, because a registration made outside
welder (a hand-written pybind11 class_, a nanobind nb::class_, a sol2 usertype, a
third-party library's bindings) is invisible to a compile-time caster read. There are
three ways to satisfy it.
The first two — the trust_bindable marks — are backend-agnostic vocabulary;
they suppress the gate wherever the type appears and leave the registration to you.
The third is a rod-native caster, so its exact form depends on the rod.
In the cookbook
Recipe 07 uses the member mark for a framework type the LuaCATS stub rod can't see. (Tack welding — Recipe 08 — needs no hatch for the tacked library's own types: the greedy pass vouches for what it registers itself.)
1. mark::trust_bindable — trust one member¶
A member mark vouches for that member's type (or, on a method, the whole signature). welder skips the gate there; you then own the registration.
struct Handmade { int n{0}; }; // welder never sees this welded
struct [[=welder::weld(welder::lang::py)]]
TrustsMember {
[[=welder::mark::trust_bindable]] // trust this member's type
Handmade item;
int count{0};
[[=welder::mark::trust_bindable]] // also trusts a *method's* whole signature
Handmade make(int n) const { return Handmade{n}; }
};
You must register Handmade with pybind11 by hand before binding the struct
that uses it.
2. trust_bindable<T> = true — trust a type everywhere¶
The type-level form is a specializable bool variable template (not an attribute).
It trusts T everywhere it appears — member, parameter, return, and container
element, because it folds into bindable() itself:
struct Handmade2 { int n{0}; };
template <> constexpr bool welder::trust_bindable<Handmade2> = true;
struct [[=welder::weld(welder::lang::py)]]
TrustsType {
Handmade2 item; // trusted → bound
std::vector<Handmade2> many; // also cleared: recursion hits a trusted leaf
int count{0};
};
Which trust to reach for
Use the member mark for a one-off. Use the type-level point when a type
appears in many places and is always registered elsewhere — it also clears
T*, const T&, and std::vector<T> in one stroke.
3. A self-contained rod caster — no trust needed¶
If you give T a self-contained caster in the rod's framework — one that
does not derive from the rod's registration-needing base — it displaces the
fallback caster. Now has_native_caster<T> reports true, the gate passes
automatically, and the caster even names the type in generated stubs. No weld, no
trust. The mechanism is standard for each framework:
A type_caster<T> built with PYBIND11_TYPE_CASTER (does not derive from
type_caster_base), so _needs_registration<T> is false and const_name
stubs it cleanly (e.g. as float):
struct Celsius { double t; };
namespace pybind11::detail {
template <> struct type_caster<Celsius> {
PYBIND11_TYPE_CASTER(Celsius, const_name("float"));
bool load(handle src, bool) { value.t = src.cast<double>(); return true; }
static handle cast(const Celsius& c, return_value_policy, handle) {
return PyFloat_FromDouble(c.t);
}
};
} // namespace pybind11::detail
The same idea with NB_TYPE_CASTER in nanobind::detail — a caster that isn't
a base caster flips is_base_caster_v<make_caster<T>> to false, so the gate
clears T without a registered class.
Lua's leaf question is different: the gate clears T when
sol::lua_type_of<T> is a native Lua type rather than userdata. A type that
sol2 already knows how to push/get natively (or one you teach it via a
sol_lua_push/sol_lua_get customization) passes without a usertype.
The same question in LuaBridge3's spelling: the gate clears T when
LuaBridge3 does not classify it as userdata
(luabridge::detail::IsUserdata). A type you teach LuaBridge3 to convert
natively — a luabridge::Stack<T> specialization — passes without a
registered class.
The caster must be visible before the bind
Standard framework behavior, not welder-specific: the caster has to be in scope
before welder binds any type using T. (gcc-16 happens to defer
instantiation to end-of-TU so a later caster in the same TU also works — but
relying on that is ill-formed-NDR. Keep the caster ahead of the bind.)
A caster that does derive from the registration-needing base (pybind11's
type_caster_base, nanobind's base caster) still needs its class registered —
only a self-contained one flips the type to native.
Summary¶
| Mechanism | Backend-agnostic? | Scope | Who registers T |
|---|---|---|---|
mark::trust_bindable |
✅ vocabulary | one member / one signature | you (by hand, before the bind) |
trust_bindable<T> = true |
✅ vocabulary | T everywhere (incl. T*, T&, vector<T>) |
you (by hand, before the bind) |
| self-contained rod caster | ⚙️ per rod | T everywhere |
the caster is the conversion |
A future bindable_as<T> mapping T to a stub type name is still TODO.
Next: Generating C++ docs.