Lua (LuaBridge3)¶
welder binds Lua through two rods. The sol2 page covers the first;
this is the second — the LuaBridge3 rod. It binds the same annotated C++ (you
still just add welder::lang::lua to a type's weld), runs the same tests, and
produces the same kind of loadable Lua C module entered through
luaopen_<name>. Only the framework laying the bindings down differs.
Why a second Lua rod? LuaBridge3 is a lightweight, dependency-free, single-header binding library that supports a wider range of Lua runtimes than sol2 — PUC-Lua 5.1–5.5, LuaJIT, Luau, Ravi — so it is the rod to reach for when you need a newer Lua (sol2/3.5.0 caps at 5.4), a different license, or a smaller dependency.
#include <welder/vocabulary.hpp> // vocabulary (header-only)
#include <welder/rods/lua/luabridge/rod.hpp> // pulls in <lua.hpp> + LuaBridge3
struct
[[=welder::weld(welder::lang::lua)]]
Rect {
double w{0.0};
double h{0.0};
Rect() = default;
Rect(double width, double height) : w{width}, h{height} {}
double area() const { return w * h; }
Rect operator+(const Rect& o) const { return {w + o.w, h + o.h}; } // __add
};
// Per type, write the entry yourself. LuaBridge3 registers into a named namespace
// under _G, so welder builds the module there, returns the table, and clears _G.
extern "C" int luaopen_shapes(lua_State* L) {
using rod = welder::rods::luabridge::rod;
rod::module_type m{L, {"shapes"}}; // {lua_State*, module path}
welder::welder<rod>::weld_type<Rect>(m); // one type
// welder::welder<rod>::weld_namespace<^^ns>(m); // or a whole namespace
lua_getglobal(L, "shapes"); // the populated table
lua_pushnil(L); lua_setglobal(L, "shapes"); // keep _G clean
return 1;
}
local s = require("shapes")
local r = s.Rect(3.0, 4.0) -- or s.Rect.new(3.0, 4.0)
print(r:area()) -- 12.0 (methods use `:`)
print((r + s.Rect(1, 1)).w) -- 4.0 (operator+ -> __add)
Or skip the boilerplate with the rod-agnostic
entry macro (from
welder/rods/lua/luabridge/module.hpp), which binds a whole namespace and emits the
luaopen_ symbol — building under _G[<name>], returning the table, and clearing
the global for you. The selector is the rod name luabridge:
WELDER_MODULE(shapes_luabridge, luabridge) {
// optional hand-written glue; `module` is the {lua_State*, path} handle
::luabridge::getGlobalNamespace(module.L)
.beginNamespace(module.path.front().c_str())
.addVariable("BUILT_BY", "welder")
.endNamespace();
}
Only the module header include differs from sol2 (luabridge/… instead of
sol2/…) — the rod header includes <lua.hpp> and <LuaBridge/LuaBridge.h> for
you, so a binding TU needs just that one include.
How it differs from sol2¶
Both Lua rods bind the same core surface — metamethods, enums-as-tables, constructors
exposed as both T(…) and T.new(…), const/live namespace variables — asserted by
the same busted specs. Where they genuinely differ:
| sol2 | LuaBridge3 | |
|---|---|---|
| Module handle | a sol::table you create |
a {lua_State*, path} under _G (returned, _G cleared) |
| Multiple inheritance | yes, incl. virtual diamonds | non-virtual only — no virtual bases |
| Namespace variables (mutable) | metatable proxy | native addProperty |
| Lua versions | ≤ 5.4 | 5.1–5.5, LuaJIT, Luau, Ravi |
The virtual-base limitation is the one behavioral gap: LuaBridge3 computes a base's
cast offset as plain pointer arithmetic, which a virtual base breaks, so a virtual
diamond that binds under sol2 does not under LuaBridge3 (welder still supports its
non-virtual multiple inheritance). Everything else — enums
as name→value tables, operators as metamethods
(operator[] rides LuaBridge3's index fallback so it coexists with member access),
docstrings dropped at runtime and recovered in the
LuaCATS stub — matches sol2.
Building¶
LuaBridge3 has no Conan Center package and ships no CMake config, so welder sources it two ways:
- As a consumer (you
#includethe rod in your own project): bring your own LuaBridge3 headers — welder finds them withfind_package(LuaBridge3)or-DWELDER_LUABRIDGE_DIR=<dir containing LuaBridge/LuaBridge.h>. - Building welder's own tests/examples: welder FetchContents a pinned LuaBridge3
commit automatically (
-DWELDER_LUABRIDGE_FETCH=ON, on by default when tests are enabled; pin withWELDER_LUABRIDGE_GIT_TAG).
Lua itself comes from your system/install, with the rod's own version knobs so it can target a newer Lua than sol2 without disturbing it:
# Build the LuaBridge3 rod/tests against Lua 5.5 (sol2 stays on 5.4)
cmake --preset welder-gcc16 \
-DWELDER_LUABRIDGE_LUA_DIR="$(brew --prefix lua)" \
-DWELDER_LUABRIDGE_LUA_VERSION=5.5
WELDER_LUABRIDGE_LUA_VERSION / WELDER_LUABRIDGE_LUA_DIR default to the sol2 knobs
(WELDER_LUA_VERSION / WELDER_LUA_DIR), so a single Lua install serves both rods
unless you point them apart. Create an extension with
welder_luabridge_add_module(<name> <sources>) (the LuaBridge3 counterpart of
welder_sol2_add_module); the <name> must match the luaopen_<name> module name.
ABI must match the interpreter
Like any loadable Lua module, a LuaBridge3 extension resolves lua_* from the
host interpreter and has no cross-minor ABI — a 5.4 module in a 5.5 interpreter
segfaults. welder hard-errors at configure time if the found Lua minor does not
match WELDER_LUABRIDGE_LUA_VERSION.