Binding a type¶
Every rod goes through the same entry point — welder::welder<Rod>::weld_type<T>(m),
where Rod is any of the shipped rods
(welder::rods::pybind11::rod<>, welder::rods::nanobind::rod<>,
welder::rods::sol2::rod, welder::rods::luabridge::rod) — which reflects T and
emits its whole surface. This
page covers what "whole surface" means for a class: data members, constructors,
methods, and operators. The annotations and the resolution are identical across
rods; only the emitted target-language surface differs. Each member obeys the
resolution rule — excludes, includes, and the
type's policy decide what participates.
The examples below weld one struct and show how it looks from each language. The C++ is the same; pick your tab.
In the cookbook
Recipe 01 — One of everything welds a type (fields, methods, operators, the synthesized aggregate constructor) alongside an enum, a free function and a namespace variable; Recipe 06 does the same for template instantiations.
Data members¶
Public data members bind as read/write attributes. (Protected members can join
them — see
policy::weld_protected;
private members never bind.)
struct [[=welder::weld(welder::lang::py, welder::lang::lua)]]
Point {
double x{0.0};
double y{0.0};
};
Every bound member's type must pass the bindability gate — if the rod can't convert it to a meaningful value in the target language, you get a compile error naming the type, never a silent skip.
Constructors¶
welder binds:
- the default constructor, if present;
- each public, non-copy/non-move constructor →
pybind11::init<…>; - for a baseless aggregate, a synthesized field constructor that brace-inits
it — giving Python
T(f0, f1, …).
Why aggregates are special
Aggregate initialization is positional and all-or-nothing, so the synthesized constructor is only offered when every field binds.
struct [[=welder::weld(welder::lang::py, welder::lang::lua)]]
Rect { // an aggregate: no user ctors, no bases
double w{0.0};
double h{0.0};
};
Compare with a type that declares its own constructors — each public one binds:
struct [[=welder::weld(welder::lang::py, welder::lang::lua)]]
Rect {
double w{0.0};
double h{0.0};
Rect() = default;
Rect(double width, double height) : w{width}, h{height} {}
};
Parameter names → keyword arguments (Python)¶
When every parameter of a signature is named, welder passes the names through
as py::arg, so they work as Python keyword arguments:
Lua has no keyword arguments, so this is a Python-only convenience; the same constructor is still callable positionally there.
Methods and static methods¶
Member functions bind as methods; static member functions as static/free
functions on the type. Overloads are all registered on every rod — the Python
rods (pybind11/nanobind) chain them, and the sol2 rod groups a name's
overloads into one sol::overload(…) — so each overload dispatches on its arguments
at call time.
struct [[=welder::weld(welder::lang::py, welder::lang::lua)]]
Rect {
double w{0.0}, h{0.0};
[[=welder::doc("The area of the rectangle.")]]
double area() const { return w * h; }
static Rect square(double s) { return Rect{s, s}; }
};
Overloaded operators¶
A member operator binds under the target language's special method / metamethod, told apart unary vs. binary by arity. The mapping differs per language:
| C++ | Python | C++ | Python |
|---|---|---|---|
operator+ |
__add__ |
operator== |
__eq__ |
operator- (binary) |
__sub__ |
operator- (unary) |
__neg__ |
operator* |
__mul__ |
operator[] |
__getitem__ |
operator() |
__call__ |
operator< |
__lt__ |
Arithmetic, bitwise, comparison, call and subscript operators are covered. See the Python rods page for the full table.
| C++ | Lua | C++ | Lua |
|---|---|---|---|
operator+ |
__add |
operator== |
__eq |
operator- (binary) |
__sub |
operator- (unary) |
__unm |
operator* |
__mul |
operator[] |
__index |
operator() |
__call |
operator< |
__lt |
Lua's metamethod set is smaller and asymmetric — !=, >, >= are derived
from __eq, __lt, __le, so you don't bind them. See the
Lua rod page for the full
table.
struct [[=welder::weld(welder::lang::py, welder::lang::lua)]]
Vec2 {
double x{0.0}, y{0.0};
Vec2 operator+(const Vec2& o) const { return {x + o.x, y + o.y}; }
Vec2 operator-() const { return {-x, -y}; } // unary → __neg__ / __unm
bool operator==(const Vec2& o) const { return x == o.x && y == o.y; }
};
Deliberately not mapped
In-place compound assignment (operator+=) is not mapped — Python falls
back to a = a + b via __add__. Nor are <=>, &&, ||, ++, --, or
operator= (a special member). Free (non-member) operators aren't bound
yet.
Nested types¶
A class or enum declared inside a welded type resolves like any other class
member: the outer's policy plus the nested type's own
exclude / include / only marks
decide participation. A nested type never carries (or needs) a weld of its own
— nested types are interface helpers of their enclosing type, and the enclosing
weld is the discovery marker. Nesting recurses (Outer::Inner::Innermost),
private nested types never bind, and protected ones follow
policy::weld_protected.
struct [[=welder::weld(welder::lang::py, welder::lang::lua)]]
Robot {
struct Sensor { double range{1.5}; }; // binds as Robot.Sensor
enum class Mode { idle, active }; // binds as Robot.Mode
struct [[=welder::mark::exclude]] Impl { }; // bound nowhere
Sensor sensor{}; // fine: Sensor is registered
void set_mode(Mode m); // fine: Mode is registered
};
Because the nested types register with the outer, members whose signatures use them pass the bindability gate with no extra annotation — and a signature naming a nested type that does not participate (excluded, private, or forward-declared) is a hard compile error, not a runtime surprise.
The nested type is registered with the enclosing class as its scope, exactly
like a hand-written py::class_<Robot::Sensor>(robot_cls, "Sensor"):
s = mymod.Robot.Sensor() # scoped: module.Outer.Inner
Robot.Sensor.__qualname__ # "Robot.Sensor" — stubs nest too
mymod.Robot.Mode.active # a nested IntEnum
An unscoped nested enum exports its values onto the class
(Robot.quiet), mirroring C++'s Robot::quiet.
Both Lua rods expose the same access chain — mod.Robot.Sensor (sol2 places
the usertype on the outer's table; LuaBridge3 moves the class table onto the
outer as a static entry). The generated
LuaCATS stub declares it under the dotted name
(---@class mod.Robot.Sensor):
Excluding + welding manually
To keep a nested type out of the outer's surface but still bind it (flat,
under a name of your choosing), combine mark::exclude with an explicit
weld and weld it manually — the exclude removes it from the sweep, the
weld keeps the gate satisfied for manual registration:
Flattened bases keep their nested types to themselves
A nested type registers exactly once, with its declaring class. A non-welded base's members are flattened into the derived binding, but its nested types are not — two derived types flattening one mixin would register the same type twice. A flattened signature naming one therefore fails the gate until you weld the base (or trust/exclude the member).
Chaining on the returned handle¶
weld_type returns the rod's own class handle — pybind11's py::class_<T>,
nanobind's nb::class_<T>, sol2's sol::usertype<T> — so hand-written
framework registrations chain right on: welder lays the reflected boilerplate,
you add what it shouldn't guess (a lambda-backed helper, a member you
excluded to bind manually, a custom
return-value policy):
auto cls = weld::weld_type<Rectangle>(m); // welder binds the reflected surface
cls.def("scaled", [](const Rectangle& r, double k) // …and you weld on by hand
{ return Rectangle{r.width * k, r.height * k}; });
weld_function likewise returns the bound function object where the framework
has one (the Python rods, sol2), and
weld_namespace_as_submodule returns the new
submodule handle — every entry point hands back its framework object so
welder-generated and hand-written bindings mix freely.
Next: Enums.