Skip to content

Namespaces & modules

Binding types one at a time is fine, but welder can also bind a whole namespace — or emit an entire importable module — from a single declaration.

In the cookbook

Recipe 02 — Discovery rules drives a whole module through WELDER_MODULE, submodules and pruning included; Recipe 01 — One of everything shows the semi-manual weld_function / weld_variable route; Recipe 08 — Tack welding binds an unmarked library greedily.

Binding a namespace

welder::welder<Rod>::weld_namespace<^^ns>(m) walks a namespace and binds its contents in declaration order:

  • classes (via weld_type<T>),
  • class-template instantiations declared through a namespace-scope alias (using IntBox = Box<int>; — the alias is the target-language name; see Binding templates),
  • free functions (overloads included),
  • namespace-scope variables.

weld gates leaf entities only (a class type, a free function, a namespace-scope variable — namespaces are never welded). The namespace's policy (default automatic) plus member marks then resolve what actually binds.

namespace geometry {

struct [[=welder::weld(welder::lang::py, welder::lang::lua)]] Point { double x, y; };

[[=welder::weld(welder::lang::py, welder::lang::lua)]]
double distance(const Point& a, const Point& b) { /* … */ }

}  // namespace geometry
PYBIND11_MODULE(geometry, m) {
    welder::welder<welder::rods::pybind11::rod<>>::weld_namespace<^^geometry>(m);
}
extern "C" int luaopen_geometry(lua_State* L) {
    sol::state_view lua(L);
    sol::table m = lua.create_table();
    welder::welder<welder::rods::sol2::rod>::weld_namespace<^^geometry>(m);
    return sol::stack::push(L, m);
}

Namespace variables

A namespace-scope variable binds as a module attribute:

  • const / constexpr → a value snapshot;
  • otherwise → a live get/set over the C++ global: a read returns the current value and a write flows back to the global. Every runtime rod gives you the same behavior, each through its framework's own mechanism:

    Rod Live variables via
    pybind11 / nanobind a ModuleType __class__ swap (module-level property)
    sol2 a metatable proxy (__index/__newindex) on the module table
    LuaBridge3 the registrar's addProperty (a getter/setter pair)

Nested namespaces

A nested namespace resolves under the parent's policy (it has no weld of its own): automatic recurses unless excluded; opt_in recurses only if included — which keeps detail / impl namespaces out by giving the parent opt_in. A nested namespace becomes a submodule when it holds bound content.

Binding a namespace as a submodule

weld_namespace<^^ns>(m) welds a namespace's contents into m, flat. weld_namespace_as_submodule<^^ns>(m) instead first creates a submodule of m and welds the namespace into that — the manual counterpart of what the namespace walk above does automatically for nested namespaces, for when the parent module is otherwise hand-written (or when you want a library's namespace mounted under a name of your choosing).

The submodule's name defaults to the namespace's styled / weld_as name; an optional trailing name is used verbatim and beats both. The call returns the new submodule handle, so hand-written bindings chain right on:

PYBIND11_MODULE(mymod, m) {
    using weld = welder::welder<welder::rods::pybind11::rod<>>;
    // mymod.geometry — or pass a name: …as_submodule<^^geometry>(m, "geo")
    auto sub = weld::weld_namespace_as_submodule<^^geometry>(m);
    sub.attr("VERSION") = "1.0";      // keep hand-binding into the handle
}
extern "C" int luaopen_mymod(lua_State* L) {
    sol::state_view lua(L);
    sol::table m = lua.create_table();
    using weld = welder::welder<welder::rods::sol2::rod>;
    sol::table sub = weld::weld_namespace_as_submodule<^^geometry>(m);
    sub["VERSION"] = "1.0";           // the submodule is a nested table
    return sol::stack::push(L, m);
}

Binding a single function or variable

Welding a whole namespace is convenient, but the semi-manual route lets you drop down and bind one hand-picked free function or global directly onto a module handle — the free-standing analogue of weld_type<T>, for when you want welder to lay down just that entity and keep the rest of the entry point hand-written:

The entity still needs its [[=welder::weld(...)]] (that is always the gate), and its signature/type still runs the bindability gate — but you do not have to weld, or even name, the enclosing namespace.

namespace geometry {
[[=welder::weld(welder::lang::py)]] double distance(const Point& a, const Point& b);
[[=welder::weld(welder::lang::py)]] inline constexpr double kUnit{1.0};
}  // namespace geometry

PYBIND11_MODULE(geometry, m) {
    using weld = welder::welder<welder::rods::pybind11::rod<>>;
    weld::weld_function<^^geometry::distance>(m);  // one free function
    weld::weld_variable<^^geometry::kUnit>(m);     // one constant
    // … the rest of the module stays ordinary hand-written pybind11 code …
}

weld_variable follows the same const-vs-mutable rule as namespace variables above (a value snapshot, or a live property on the Python rods). Like weld_type, both take an optional trailing name argument used verbatim — and it takes precedence over any weld_as on the entity:

weld::weld_function<^^geometry::distance>(m, "dist");  // exposed as m.dist
weld::weld_variable<^^geometry::kUnit>(m, "UNIT");     // exposed as m.UNIT

For an overloaded name, ^^fn on the overload set is ambiguous — reflect a specific overload. weld_function then welds that overload's participating same-name siblings along with it, as one group (the same set the namespace walk would bind), so the call means the same thing on every rod — including the Lua ones, whose tables hold a single value per name. Shape the group with per-overload marks; the reflected overload itself always welds (the explicit call outranks its own marks).

Where the framework has a per-function object, weld_function returns it — the bound function (m.attr(name)) on the Python rods, the table entry on sol2 — so it chains like weld_type's class handle; LuaBridge3's fluent registrar has none, so there it returns void. weld_namespace_as_submodule returns the new submodule handle for the same reason: keep hand-binding into it.

Tack welding: an unmarked library

Everything so far needs a weld marker. But sometimes you want to bind a third-party library that has no welder annotations and that you can't edit. For that, swap the carriage — the traversal engine welder::welder drives — from the default stitch-welding carriage (bind where the markers direct) to the tack-welding carriage, which binds greedily: every reflectable type, free function and global participates, namespaces are recursed, and every public base is flattened in — the missing weld markers are simply ignored.

The carriage is welder::welder's third template argument:

namespace thirdparty {            // no welder annotations anywhere
struct Vec2 { double x, y; double length() const; };
Vec2 midpoint(const Vec2&, const Vec2&);
inline constexpr int ABI_VERSION{3};
}  // namespace thirdparty

using tack = welder::welder<welder::rods::pybind11::rod<>,
                            welder::naming::none,
                            welder::tack_welding_carriage>;   // ← greedy carriage

PYBIND11_MODULE(thirdparty, m) {
    tack::weld_namespace<^^thirdparty>(m);   // binds Vec2, midpoint, ABI_VERSION …
}

Bindability is still enforced

Tack welding drops the marker requirement, not the bindability gate. The gate's registration oracle does adapt: under tack welding, a class/enum type the greedy pass itself registers (any complete, non-excluded type — like Vec2 in midpoint's signature above) counts as registered, so the library's own types may appear in its signatures with no hatch. Everything else still hard-errors at compile time: a genuinely unrepresentable type, or a forward-declared (incomplete) one the walk cannot register. Vouch for a type registered elsewhere with a type-level trust_bindable<T>, or point the tack at a narrower sub-namespace. Any mark::exclude that does happen to be present is still honored, so a partially-annotated header can still prune.

The flip side of the greedy oracle: it can't know which namespaces you tack. A signature naming a registrable type you never actually weld binds fine but raises the framework's unregistered-type error at call time. One pybind11-specific wrinkle on top (a framework property, not welder's): pybind11 renders docstrings at def time, so a signature referencing a type declared later in the namespace spells the raw C++ name in docstrings/.pyi stubs — declare types before the signatures that use them (C++ mostly forces this anyway).

Both carriages ship as welder::stitch_welding_carriage (the default) and welder::tack_welding_carriage; a custom traversal is a welder::carriages::basic_carriage<Resolution> with your own resolution policy.

Subclassing welder::welder

Each weld_* entry point is a one-line forward to the carriage (which owns the resolution and the gates). To go beyond the stock flow you can either inject a different carriage (above) or derive from welder::welder<Rod, Style, Carriage> — being all-static it isn't a runtime base, but a subclass reaches the bound rod_type / name_style / carriage_type and the entry points to assemble a bespoke routine (a curated subset of a namespace, welded and hand-written registrations interleaved) without re-implementing the traversal or the gates.

Binding a whole module

WELDER_MODULE(ns, rod) emits the language's C entry symbol (PyInit_<name> for Python, luaopen_<name> for Lua) and fills the module from the namespace — no PYBIND11_MODULE, no hand-written luaopen_, no per-type weld_type calls. The namespace token doubles as the module name, and the namespace doc becomes the module docstring (where the language has one). Include the rod's module.hpp (not just its rod.hpp) to pull the macro in.

The rod selector is the rod name (pybind11, nanobind, sol2, luabridge), not the language. Everything above WELDER_MODULE — the namespace and its annotations — is identical; only the includes and the selector change:

shapes.cpp
#include <welder/vocabulary.hpp>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <welder/rods/python/pybind11/module.hpp>  // rod + WELDER_MODULE

namespace
[[=welder::doc("A small shapes module built by welder.")]]
shapes {

struct
[[=welder::weld(welder::lang::py, welder::lang::lua),
  =welder::doc("An axis-aligned rectangle.")]]
Rect {
    double w{0.0}, h{0.0};
    Rect() = default;
    Rect(double width, double height) : w{width}, h{height} {}

    [[=welder::doc("The area of the rectangle.")]]
    double area() const { return w * h; }
};

[[=welder::weld(welder::lang::py, welder::lang::lua),
  =welder::doc("Scale a length by a factor.")]]
double scale(
    [[=welder::doc("the length to scale")]] double length,
    [[=welder::doc("the multiplier")]] double factor) {
    return length * factor;
}

}  // namespace shapes

// One line: emits PyInit_shapes and binds the whole namespace into `module`.
// The trailing block is optional post-glue (the module handle is in scope).
WELDER_MODULE(shapes, pybind11) {
    module.attr("VERSION") = "1.0";
}
>>> import shapes
>>> shapes.__doc__
'A small shapes module built by welder.'
>>> shapes.Rect(2.0, 3.0).area()
6.0
>>> shapes.scale(length=10.0, factor=2.5)
25.0
>>> shapes.VERSION
'1.0'
shapes_lua.cpp
#include <welder/vocabulary.hpp>
#include <sol/sol.hpp>
#include <welder/rods/lua/sol2/module.hpp>  // rod + WELDER_MODULE

// ... namespace shapes { Rect, scale } exactly as in the Python tab ...

// Emits luaopen_shapes and binds the whole namespace into `module`
// (a sol::table here). The doc annotations are ignored at runtime by sol2 —
// their Lua home is the LuaCATS stub.
WELDER_MODULE(shapes, sol2) {
    module["VERSION"] = "1.0";
}
local shapes = require("shapes")
print(shapes.Rect(2.0, 3.0):area())     --> 6.0
print(shapes.scale(10.0, 2.5))          --> 25.0  (no keyword args in Lua)
print(shapes.VERSION)                   --> 1.0

Under the hood, WELDER_MODULE wraps welder::welder<Rod>::weld_module<^^ns>(m, pre, post): a pre hook, then weld_namespace, then a post hook (your trailing block).

By default that is the plain welder::welder<rod>. An optional third argument names the exact welder::welder<…> type to drive the weld with instead — the way to thread a name style (or a custom carriage) through the one-line module form; commas inside the template-id are fine:

WELDER_MODULE(shapes, pybind11,
              welder::welder<welder::rods::pybind11::rod<>,
                             welder::rods::python::pep8>) {}

One WELDER_MODULE per rod per TU — but several rods can coexist

Two rods that emit the same entry symbol collide — pybind11 and nanobind both emit PyInit_<name>, so only one Python rod per TU. But a Python and a Lua WELDER_MODULE emit different symbols (PyInit_shapes vs luaopen_shapes), so one shared object can carry both. That's the basis for shipping the same module across rods.

Next: Return policies & lifetimes.