Lua (sol2)¶
welder binds the same annotated C++ to Lua as to Python — you add
welder::lang::lua to a type's weld and register with the sol2 rod. The
core (which members bind, inheritance, the bindability
gate, namespaces) is shared verbatim; only the emission
differs. The result is a loadable Lua C module: a shared object Lua's require
finds on package.cpath and enters through luaopen_<name>.
Everything in the guide applies to Lua; this page is the sol2-specific detail — how the entry point works, where Lua's surface differs from Python's, and the LuaCATS stub that carries the docstrings Lua has no runtime slot for.
There is a second Lua rod
welder also ships a LuaBridge3 rod for Lua — same welded C++, same tests, but a dependency-free header and support for newer Lua (5.5, LuaJIT, Luau). See Lua (LuaBridge3) for how it differs.
#include <welder/vocabulary.hpp> // vocabulary (header-only)
#include <sol/sol.hpp>
#include <welder/rods/lua/sol2/rod.hpp>
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:
extern "C" int luaopen_shapes(lua_State* L) {
sol::state_view lua(L);
sol::table m = lua.create_table();
using weld = welder::welder<welder::rods::sol2::rod>;
weld::weld_type<Rect>(m); // one type
// weld::weld_namespace<^^ns>(m); // or a whole namespace
return sol::stack::push(L, m); // return the module table
}
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/sol2/module.hpp), which binds a whole namespace and emits the
luaopen_ symbol for you. The selector is the rod name sol2, not lua:
Building a loadable module¶
Use welder_sol2_add_module() — the Lua analogue of pybind11_add_module — which
produces a require-able <name>.so with the right link model (the module resolves
lua_* from the host interpreter; it never bundles its own Lua):
find_package(sol2 REQUIRED)
welder_sol2_add_module(shapes example.cpp)
target_compile_features(shapes PRIVATE cxx_std_26)
target_link_libraries(shapes PRIVATE welder::headers)
The target name must match the namespace token in WELDER_MODULE(shapes, sol2) so
luaopen_shapes is the loaded entry point.
Header-only consumption
welder is header-only today, so a Lua binding TU
brings the vocabulary in with #include <welder/vocabulary.hpp>. Even if the
planned import welder; module wrapper lands, a Lua TU could not use it: sol2's
<luaconf.h> does not survive C++20 module dependency scanning.
welder_sol2_add_module disables the scan for you (CXX_SCAN_FOR_MODULES OFF).
Match the Lua minor version
A loadable module has no cross-minor ABI compatibility — a Lua 5.4 module loaded
by a 5.5 interpreter segfaults. sol2 supplies only the C++ headers; Lua itself
comes from your system/install, steered by -DWELDER_LUA_DIR (an install
prefix, e.g. $(brew --prefix lua@5.4)) and pinned with -DWELDER_LUA_VERSION
(default 5.4). A configure-time guard hard-errors on a minor mismatch. (sol2
3.5.0 does not support Lua 5.5.)
How Lua differs from Python¶
Everything welder resolves (policy, mark::exclude/include/only, inheritance,
the bindability gate) works identically; the target-language surface is what
changes.
Operators become metamethods¶
Lua's metamethod set is smaller and asymmetric. Every welded member operator binds to a metamethod (told apart unary vs. binary by arity); this is the complete set welder maps:
| C++ | Lua | C++ | Lua | |
|---|---|---|---|---|
a + b |
__add |
a == b |
__eq |
|
a - b |
__sub |
a < b |
__lt |
|
-a (unary) |
__unm |
a <= b |
__le |
|
a * b |
__mul |
a(...) |
__call |
|
a / b |
__div |
a[i] |
__index |
|
a % b |
__mod |
Bitwise operators map too, but only on Lua ≥ 5.3 (not LuaJIT's 5.1 ABI), so
they are #if-gated:
| C++ | Lua | C++ | Lua | |
|---|---|---|---|---|
a ^ b |
__bxor |
a << b |
__shl |
|
a & b |
__band |
a >> b |
__shr |
|
a \| b |
__bor |
~a (unary) |
__bnot |
operator!=, operator> and operator>= map to nothing — Lua derives ~=,
> and >= from __eq, __lt and __le, so they just work once those are bound.
Note C++ operator^ is bitwise-xor → __bxor (not __pow/power).
Everything else¶
- Enums are tables. Lua has no enum type, so a welded enum binds as a name→value
table (
Color.Redis the value); an unscoped enum's names are also mirrored onto the enclosing module, mirroring C++. - No runtime docstrings. Lua has no
__doc__, sodoc/returnsannotations are ignored at runtime. Their home is the generated LuaCATS stub. - Namespace variables: snapshot or live. A
const/constexprvariable binds as a value snapshot at load time; a mutable one binds as a live get/set over the C++ global (a metatable proxy on the module table), so a Lua read sees the current value and a Lua write flows back to C++ — matching the Python rods. - Overloaded methods, functions and operators are grouped into a single
sol::overload(…)(sol2 stores one value per name / metamethod slot), so every overload of a name dispatches at call time —c:sum(a)andc:sum(a, b)both work. A same-named method in a derived class still hides the base's, matching C++. - Multiple and virtual base classes are supported, so a multi-base diamond binds here — sol2 is the most permissive rod on inheritance; see the per-rod support table.
Stubs (LuaCATS)¶
Because Lua drops docstrings at runtime, welder can emit a LuaCATS
(---@meta) definition file — the Lua analogue of the Python
.pyi stubs — so the Lua language
server gives you completion, type hints and the
docstrings in your editor. Unlike the Python stubs (scraped from the loaded
module), a Lua stub is reflection-emitted at build time by the
welder::rods::luacats::rod, which walks the same welded types through the same
core driver as sol2 and writes LuaCATS text — so it needs no sol2 or Lua at all,
just the reflecting compiler.
Write a tiny generator TU and let the entry macro provide main():
#include <welder/vocabulary.hpp>
#include <welder/rods/lua/luacats/module.hpp> // rod + WELDER_LUACATS_MAIN
WELDER_LUACATS_MAIN(shapes) // emit the ---@meta stub for namespace ^^shapes
Wire it in CMake:
welder_luacats_generate_stub(shapes_stub
SOURCES shapes_stub.cpp
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/shapes.lua)
The Rect above yields:
---@meta
shapes = {}
---@class shapes.Rect
---@field w number
---@field h number
---@operator add(shapes.Rect): shapes.Rect
shapes.Rect = {}
---@return shapes.Rect
function shapes.Rect.new() end
---@param width number
---@param height number
---@return shapes.Rect
function shapes.Rect.new(width, height) end
---@return number
function shapes.Rect:area() end
Any doc/returns/parameter docs you wrote land in the LuaCATS ---
comments and @field/@param/@return tags. The C++→LuaCATS type map covers
scalars (integer/number/boolean/string), the STL wrappers welder recurses
(std::vector<T> → T[], std::map<K,V> → table<K,V>, std::optional<T> →
T?, smart pointers → the pointee) and welded classes/enums (their dotted name);
anything else degrades to any. Enums become ---@enum tables, welded bases become
---@class X : Base. Overloaded methods, constructors and free functions render as a
single documented function plus idiomatic ---@overload fun(…) lines (the primary
signature keeps its full @param/summary docs; the extra signatures carry types
only, which is all LuaCATS ---@overload records). A const data member is noted
(read-only) in its ---@field description — LuaCATS has no read-only field tag
(an open lua-language-server request),
so the immutability the sol2 runtime enforces is documented here, not machine-checked.
Not every runtime metamethod has a stub form. LuaCATS ---@operator only names the
operators the language server models — the arithmetic and bitwise ones plus
call/len/concat/unm — so the comparison (==, <, <=) and
subscript ([]) metamethods the sol2 runtime binds are omitted from the stub:
lua-language-server has no ---@operator spelling for them (== is always allowed
and yields a boolean; indexing is expressed with ---@field [key] value). They work
at runtime regardless; the stub simply can't type them.
welder's own test suite runs the emitted stub through lua-language-server --check
when the server is installed (the Lua analogue of type-checking the .pyi stubs with
mypy), so a malformed annotation or dangling type reference fails the build.