Getting started¶
Toolchain¶
C++26 reflection is bleeding-edge. welder is written against the standard, not gcc extensions, but today only one compiler implements the papers it needs:
| Requirement | Needed |
|---|---|
| Compiler (P2996 + P3394) | gcc-16 — the only one so far (g++-16, GCC ≥ 16.1); install it from whatever package manager or source build you prefer |
| Build system | CMake ≥ 3.28; the presets drive Ninja |
| Packages (examples/tests only) | Conan 2 (conanfile.py) provisions the backends → pybind11 / nanobind (Python), sol2 (Lua); LuaBridge3 is FetchContent-pinned instead. Not needed to consume welder — see Consuming welder. |
| Python | a python3 with development headers (for the Python modules) |
| Lua | headers via conan sol2 (pulls Lua); a Lua interpreter to load the module |
welder does not propagate these onto your targets — welder::headers is the include
path only, and you set -std=c++26 -freflection on your own target (welder checks
the compiler + standard, see Consuming welder). As Clang/MSVC
catch up, add a branch to WelderRequirements.cmake.
Reflection flags
Just -std=c++26 -freflection on gcc-16 — annotations are included, no
separate flag. Reflect with ^^Thing, splice with [: r :],
pointer-to-member with &[:member:].
Consuming welder¶
welder is header-only and needs no Conan — plain CMake wires it in. It exports the core only; you bring your own backend (pybind11/nanobind/sol2/LuaBridge3). Pulled in as a subproject, welder builds nothing of its own — no backends, no tests, no install rules — so all it asks of a consumer is a C++26 compiler.
welder::headers is just the include path. welder does not force the C++ standard
or gcc's -freflection onto your target — those are your project's to set. welder
checks them (at find_package time, and via the compile-time guard in
<welder/lang.hpp>) and fails with a clear message if they're missing, rather than
imposing them:
include(FetchContent)
FetchContent_Declare(welder
GIT_REPOSITORY https://github.com/skarndev/welder.git
GIT_TAG main)
FetchContent_MakeAvailable(welder)
target_link_libraries(my_bindings PRIVATE welder::headers)
target_compile_features(my_bindings PRIVATE cxx_std_26) # welder needs C++26 …
target_compile_options(my_bindings PRIVATE -freflection) # … + gcc-16's reflection flag
CPM.cmake wraps FetchContent, so this is the same subproject collapse with caching and version pinning on top:
include(cmake/CPM.cmake) # the get_cpm.cmake bootstrap, or a vendored copy
CPMAddPackage("gh:skarndev/welder#main")
target_link_libraries(my_bindings PRIVATE welder::headers)
target_compile_features(my_bindings PRIVATE cxx_std_26) # welder needs C++26 …
target_compile_options(my_bindings PRIVATE -freflection) # … + gcc-16's reflection flag
Once tagged releases exist, CPMAddPackage("gh:skarndev/welder@0.1.0") pins one
(CPM resolves the version to the v0.1.0 tag, and checks it against the version
welder's project() reports).
Configure with the dev-time build off (nothing of welder's own compiles), install the header tree, then depend on it:
Optional — only if your project already uses Conan.
conan create . -pr:a conan/profiles/gcc16 --build=missing publishes welder to your
local cache; a downstream requires("welder/0.1.0") then resolves the same
find_package(welder) / welder::headers.
In the cookbook
The Cookbook recipes are complete standalone projects consuming welder exactly this way (FetchContent, backends included, no Conan) — copy one as a starting point. CI builds them all on every commit.
find_package(welder) (and the FetchContent pull) also define the build helpers —
welder_pybind11_generate_stubs, welder_sol2_add_module,
welder_luabridge_add_module, welder_luacats_generate_stub — for producing the
loadable module or stub. Each references its backend's targets only inside the call,
so pulling them in is free; you set that backend up yourself.
A consuming TU brings the vocabulary in first, then the rod header:
The vocabulary also carries welder's version identity (<welder/version.hpp>):
WELDER_VERSION_MAJOR / MINOR / PATCH, a comparable WELDER_VERSION, and
WELDER_VERSION_STRING, for feature-gating against future releases. Every
welder:: name lives in a versioned inline namespace — invisible in source, but
it keeps binaries built against different welder versions from silently mixing;
see the ABI inline namespace.
A C++20 import welder; module wrapper is planned but currently deferred — see
Header-only for now for the toolchain reasons why.
Your first type¶
welder's promise is that one annotated type binds to every language you weld it
for. Here is a struct welded for both Python and Lua — the default policy
(automatic) reflects every member unless excluded. This C++ is identical no
matter which rod you register it with:
struct
[[=welder::weld(welder::lang::py, welder::lang::lua)]] // (1)
Point {
double x{0.0};
double y{0.0};
[[=welder::mark::exclude]] // (2)
std::uint64_t internal_id{0};
};
weldis required to bind, and lists the target languages. Without it,weld_type<Point>is a no-op-by-contract. Weld for only the languages you want.mark::exclude(no argument) hidesinternal_idfrom all welded languages.mark::exclude(welder::lang::py)would hide it from Python only.
Then a small translation unit per rod registers it. The #includes and the
weld_type call differ; the annotated type above does not.
#include <cstdint>
#include <string>
#include <welder/vocabulary.hpp> // annotation vocabulary
#include <pybind11/pybind11.h>
#include <pybind11/stl.h> // std::string conversion (1)
#include <welder/rods/python/pybind11/rod.hpp>
// ... Point as above ...
PYBIND11_MODULE(welder_poc, m) {
m.doc() = "welder pybind11 proof-of-concept";
welder::welder<welder::rods::pybind11::rod<>>::weld_type<Point>(m); // (2)
}
- pybind11 needs its STL converters included to move
std::string,std::vector, etc. across the boundary. welder's bindability gate reminds you at compile time if one is missing. welder::welder<Rod>is welder's one entry point, parameterized on a rod (the backend that lays the bindings down);weld_type<T>reflectsPoint, resolves which members bind, checks each is representable, and emits the pybind11 registration. The bound name defaults toidentifier_of(^^T); pass a second string to override it. (When you call several times, alias it:using weld = welder::welder<welder::rods::pybind11::rod<>>;.)
#include <cstdint>
#include <string>
#include <welder/vocabulary.hpp> // annotation vocabulary
#include <sol/sol.hpp>
#include <welder/rods/lua/sol2/rod.hpp>
// ... Point as above ...
extern "C" int luaopen_shapes_lua(lua_State* L) {
sol::state_view lua(L);
sol::table m = lua.create_table();
// same core, sol2 emission
welder::welder<welder::rods::sol2::rod>::weld_type<Point>(m);
return sol::stack::push(L, m);
}
welder synthesizes a field constructor for a baseless aggregate when every
field binds, so Point(1.0, 2.0) also works — see
Binding a type.
Building welder's own examples & tests¶
Consuming welder needs no Conan — but building welder's own examples and tests from a clone does: Conan provisions the backends, then the preset configures and builds:
conan install . -pr:a conan/profiles/gcc16 --build=missing
cmake --preset welder-gcc16
cmake --build --preset welder-gcc16
The example modules are then loadable — a Python extension and a Lua C module, both built from the same welder core:
Next: the annotation vocabulary. When you're ready to pick or combine rods, see the Languages section.