Stubs¶
A compiled extension is a black box to every tool that reads source: an editor
can't complete Rect(, a type checker can't tell area() returns a float,
and your carefully written docstrings never show up in a tooltip. A stub is
the answer — a source-form description of the binary module's surface that the
tooling reads instead. Each target language has its own dialect, and welder
generates both:
| Language | Stub dialect | Read by |
|---|---|---|
| Python | .pyi stub file |
IDEs, mypy, pyright, … |
| Lua | LuaCATS ---@meta file |
the Lua language server |
For Python the stub is a nicety (the runtime module still carries __doc__
and signatures); for Lua it is the only home your docs have — Lua has no
runtime docstring slot at all, so without the stub the doc text you wrote
reaches no Lua user.
One source, both stubs¶
The same annotated type feeds both generators — nothing stub-specific is ever written by hand:
namespace shapes {
struct
[[=welder::weld(welder::lang::py, welder::lang::lua),
=welder::doc("An axis-aligned rectangle.")]]
Rect {
[[=welder::doc("The width.")]] double w{0.0};
[[=welder::doc("The height.")]] double h{0.0};
Rect() = default;
Rect(double width, double height);
[[=welder::doc("The area of the rectangle."),
=welder::returns("width times height")]]
double area() const;
};
} // namespace shapes
class Rect:
"""
An axis-aligned rectangle.
"""
@typing.overload
def __init__(self) -> None: ...
@typing.overload
def __init__(self, width: float, height: float) -> None: ...
@property
def w(self) -> float:
"""
The width.
"""
@w.setter
def w(self, arg0: float) -> None: ...
@property
def h(self) -> float:
"""
The height.
"""
@h.setter
def h(self, arg0: float) -> None: ...
def area(self) -> float:
"""
The area of the rectangle.
Returns:
width times height
"""
---@meta
shapes = {}
--- An axis-aligned rectangle.
---@class shapes.Rect
---@field w number The width.
---@field h number The height.
shapes.Rect = {}
---@return shapes.Rect
---@overload fun(width: number, height: number): shapes.Rect
function shapes.Rect.new() end
--- The area of the rectangle.
---@return number width times height
function shapes.Rect:area() end
Every piece of the doc vocabulary — summaries, parameter docs,
returns — lands in the stub, and a name style or weld_as
rename is honoured in it exactly as in the runtime binding.
Python: .pyi¶
The two Python rods produce the same kind of stub through different tools — both scrape the built extension (import it, walk its surface), so the stub is generated as a post-build step of the module target:
| Rod | Tool | Wiring |
|---|---|---|
| pybind11 | pybind11-stubgen (a pip package) | welder's CMake helper welder_pybind11_generate_stubs() |
| nanobind | nanobind's bundled stub generator (no pip dependency; stdlib-only on Python ≥ 3.11) | upstream nanobind_add_stub() |
Because the tool imports the module, the interpreter that runs it must be able
to load the extension (ABI match) and — for pybind11 — have pybind11-stubgen
installed. The rod-specific notes live on the
Python rods page.
Lua: LuaCATS (---@meta)¶
The Lua stub cannot be scraped: a loaded sol2/LuaBridge3 module exposes nothing
introspectable. So welder generates it the other way around — the build-time
welder::rods::luacats::rod walks the same welded types through the same
core driver as the runtime rods and emits the LuaCATS text directly from
reflection, before any module is ever loaded. It needs no Lua and no binding
framework at all, just the reflecting compiler; one generator TU plus the
welder_luacats_generate_stub() CMake helper produce the .lua file.
The generator TU, the CMake wiring, the C++→LuaCATS type map and the few metamethods LuaCATS cannot express are covered on the Lua rod page.
In the cookbook
Recipe 07 — One library, two languages builds
one header into a Python module with a .pyi stub and a Lua module with
a LuaCATS stub, CI-asserted.
Next: Naming conventions.