Automatically generate Lua type stubs from your Rust mlua UserData implementations. Get full autocomplete and type checking in LuaLS or EmmyLua with zero manual annotation.
mlua-typegen is a rustc driver (like clippy or miri) that hooks into compilation after type checking, when all types are fully resolved. It walks the HIR to find impl UserData blocks, extracts method signatures and field types using rustc's type inference, and generates LuaCATS stub files.
No attributes, no macros, no code changes required.
Given this Rust code:
impl UserData for Vec2 {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("x", |_, this| Ok(this.x));
fields.add_field_method_get("y", |_, this| Ok(this.y));
}
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("dot", |_, this, (other_x, other_y): (f64, f64)| {
Ok(this.x * other_x + this.y * other_y)
});
methods.add_method("components", |_, this, ()| {
Ok((this.x, this.y))
});
}
}It generates:
---@meta
---@class (exact) Vec2
---@field x number (readonly)
---@field y number (readonly)
local Vec2 = {}
---@param other_x number
---@param other_y number
---@return number
Vec2["dot"] = function(self, other_x, other_y) end
---@return number x
---@return number y
Vec2["components"] = function(self) endIf Rust came from rustup, the installer handles everything else. It installs the pinned private nightly and compiler components without changing your default toolchain.
Linux and macOS:
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/raw/themixednuts/mlua-typegen/main/scripts/install.sh | shWindows PowerShell:
irm https://raw.githubusercontent.com/themixednuts/mlua-typegen/main/scripts/install.ps1 | iexThe installer builds cargo-mlua-typegen and its rustc driver once. Later runs check the private toolchain and restore a missing component automatically. Your project can keep using stable Rust.
# Generate ./lua-types/mlua-typegen.lua
cargo mlua-typegen
# Specify output directory
cargo mlua-typegen --output types/
# Target a specific crate in a workspace
cargo mlua-typegen -p my-plugin-crate
# Generate EmmyLua-flavored annotations
cargo mlua-typegen --emmylua
# Use a non-default workspace mapping file
cargo mlua-typegen --config config/mlua-typegen.tomlRun cargo mlua-typegen --help for generator options. Cargo package and workspace flags pass through to the internal cargo check.
Each successful run publishes one complete mlua-typegen.lua snapshot. Compiler
processes write only to private per-run report directories; a failed build leaves
the previous public snapshot unchanged. If upgrading from the former multi-file
layout, remove the old generated output directory once before regenerating.
Then point your language server at the output:
// .luarc.json (LuaLS)
{
"workspace.library": ["lua-types"]
}// .emmyrc.json (EmmyLua)
{
"workspace": {
"library": ["lua-types"]
}
}- Classes from
impl UserDatablocks - Fields from
add_field_method_get/add_field_method_set(tracks readonly vs writable) - Methods from
add_method/add_function(instance vs static) - Async methods from
add_async_method/add_async_function - Metamethods from
add_meta_method(__tostring,__add,__len, etc.) - Enums from Rust enums with string-based
IntoLua/FromLuaimpls - Modules from
create_table/ scope-based function registration - Doc comments carried through to the generated stubs
- Overloaded methods emitted with
@overloadannotations - Named return values extracted from tuple multi-returns (e.g.
---@return number x) - Union types from match/if branches returning different types (e.g.
string | integer) AnyUserDataresolution — traces throughcreate_any_userdata(expr)to resolve the concrete classinto_lua_multidecomposition — expands tuple returns into multiple@returnannotations
Place mlua-typegen.toml at the Cargo workspace root, or select exactly one
file with --config PATH. Start the file with Tombi's schema directive so the
editor validates the generated contract directly:
#:schema https://github.com/raw/themixednuts/mlua-typegen/main/schemas/mlua-typegen.schema.json
[[mappings]]
id = "uuid"
rust = { package = "uuid", version = "1", path = "Uuid" }
lua = "string"
[[mappings]]
id = "wire-id"
rust = { package = "domain", path = "WireId" }
from_lua = "string"
into_lua = "integer"
[[external_types]]
name = "Promise"
params = ["T"]lua asserts the same FromLua and IntoLua type. Directional fields may be
declared separately. Expressions support unions, optionals, arrays, maps,
functions, named types, and declared Rust parameters such as $T. A genuinely
runtime-erased value must use dynamic("reason"); bare any is invalid.
The schema is generated from the Rust configuration types with
cargo run --example generate-config-schema. Its URL remains stable; the
project does not use or pre-bump numeric schema versions. See the
explicit type-mapping decision.
| Rust | Lua |
|---|---|
String, &str, Cow<str> |
string |
i8..i128, u8..u128, usize, isize |
integer |
f32, f64 |
number |
bool |
boolean |
Vec<T>, VecDeque<T> |
T[] |
Option<T> |
T? |
HashMap<K, V>, BTreeMap<K, V> |
table<K, V> |
Result<T, _> |
T (unwrapped) |
Box<T>, Arc<T>, Rc<T>, Mutex<T>, RwLock<T> |
T (unwrapped) |
mlua::Table |
table |
mlua::Function |
function |
mlua::Thread |
thread |
mlua::Value |
any with native dynamic evidence |
mlua::Error |
string |
mlua::AnyUserData |
resolved registered userdata identity |
External crate semantics such as uuid::Uuid are not guessed from names or
suffixes; declare them explicitly when native or extracted evidence cannot
prove the Lua type.
The examples/ directory contains generated stubs for:
- Lune — Roblox datatypes (CFrame, Vector3, Color3, etc.) and standard library modules
- Yazi — file manager plugin API with 200+ class methods
MIT