Note
Feel free to use this crate and start working with ideas and features that could be useful.
Pull requests and contribution are encouraged
If you want to discuss this project, you can do that here
The goal of this project is to add a light convenience layer wrapping the mlua crate. The goal isn't to change the way that mlua is used, but instead to make lua embedded development in Rust more enjoyable.
Tealr: A project to enhance and extendmluawith a focus in type information and documentation along with a type syntax in the lua code itself with thetealrsyntax.- This crate is a great choice if you need: type syntax, type information, documentation generation
-
Helper Traits
LuaExtras- Manipulate the lua
pathandcpathvariables withappend,prepend, andsetmethods for each variant. It also includes the ability to add multiple paths with each variant. - Set global variables and functions with
set_global("value", "value")andset_global_function("func", |lua, ()| Ok(()))which wold replacelua.globals().set("value", "value)andlua.globals().set("func", lua.create_function(|lua, ()| Ok(()))?)respectively
- Manipulate the lua
-
Typed Lua Traits
Typed- Generate a
TypeandParamfor a rust type so it can be used both as a type and as a parameter for a function
- Generate a
TypedUserData- Typed variant of
mlua::UserDatawith an additionaladd_documentationmethod to add doc comments to theUserDatatype - An extra
documentmethod is added to theTypedDataFieldsandTypedDataMethodsforadd_fieldsandadd_methods. This will queue doc comments to be added to the next field or method that is added. - All types from function parameters and and return types are stored for fields, functions, and methods.
- This trait is mainly used when generating type definitions. If it is called through the
UserDataderive macro it will ignore all types and documentation
- Typed variant of
TypedDataFields: Implemented on a generator forTypedUserData(add_fields)TypedDataMethods: Implemented on a generator forTypedUserData(add_methods)TypedDataDocumentation: Implemented on a generator forTypedUserData(add_documentation)
-
Derive Macros
Typed: Auto implement theTypedtrait to get type information for bothstructandenumUserData: Auto implement themlua::UserDatatrait for rust types that also implementTypedUserData. This will pass through theUserDataadd_methodsandadd_fieldsto theTypedUserData's version. This will ignore all documentation and types.
- Fully featured definition file generation
- Fully featured documentation generation
- Fully featured addon generator when creating a lua modules with
mlua'smodulefeature - Better and more informative type errors associated with lua type definitions and output generation
- More expressive way of defining exposed lua api types
- Generic types
- Doc comments for params and return types
Helpers
use mlua::{Lua, Table, Function, Variadic, Value};
fn main() -> mlua::Result<()> {
let lua = Lua::new();
// Prepend path to the lua `path`
let path = lua.globals().get::<Table>("package")?.get::<String>("path");
lua.globals().get::<Table>("package")?.set("path", format!("?.lua;{path}"))?;
let temp = lua.create_table()?;
temp.set("getName", lua.create_function(|lua, ()| Ok("name"))?;
// Get a nested function: `table.unpack`
let unpack = lua.globals().get::<Table>("table")?.get::<_, Function>("unpack")?;
// Call the `table.unpack` function
let _ = unpack.call::<Variadic<Value>>(temp)?;
Ok(())
}use mlua_extras::{
mlua::{self, Lua, Table, Variadic, Value}
extras::{Require, LuaExtras},
typed::TypedFunction,
function,
};
fn main() -> mlua::Result<()> {
let lua = Lua::new();
// Prepend path to the lua `path`
lua.prepend_path("?.lua")?;
let temp = lua.create_table()?;
temp.set("name", "MluaExtras")?;
// Get a nested function: `table.unpack`
let unpack = lua.require::<TypedFunction<Table, Variadic<Value>>>("table.unpack")?;
// Call the `table.unpack` function
let _ = unpack.call(temp)?;
Ok(())
}Types
use serde::Deserialize;
use mlua_extras::{
mlua::{self, Lua, Table, Variadic, Value},
extras::{ Require, LuaExtras },
typed::{
generator::{Definition, Definitions, DefinitionFileGenerator},
TypedFunction, TypedUserData
},
Typed, UserData, function,
};
#[derive(Default, Debug, Clone, Copy, Typed, Deserialize)]
enum SystemColor {
#[default]
Black,
Red,
Green,
Yellow,
Blue,
Cyan,
Magenta,
White,
}
#[derive(Debug, Clone, Copy, Typed, Deserialize)]
#[serde(untagged)]
enum Color {
System(SystemColor),
Xterm(u8),
Rgb(u8, u8, u8),
}
impl Default for Color {
fn default() -> Self {
Color::System(SystemColor::default())
}
}
impl<'lua> FromLua<'lua> for Color {
fn from_lua(value: Value<'lua>, lua: &'lua Lua) -> mlua::prelude::LuaResult<Self> {
match value {
Value::UserData(data) => data.borrow::<Self>().map(|v| *v),
// Use serde deserialize if not userdata
other => lua.from_value(other),
}
}
}
#[derive(Debug, Clone, Copy, Typed, UserData, Deserialize)]
struct Example {
color: Color
}
impl TypedUserData for Example {
fn add_documentation<F: mlua_extras::typed::TypedDataDocumentation<Self>>(docs: &mut F) {
docs.add("This is a doc comment section for the overall type");
}
fn add_fields<'lua, F: TypedDataFields<'lua, Self>>(fields: &mut F) {
fields
.document("Example complex type")
.add_field_method_get_set(
"color",
|_lua, this| Ok(this.color),
|_lua, this, clr: Color| {
this.color = clr;
Ok(())
},
);
}
}
fn main() -> mlua::Result<()> {
let definitions = Definitions::generate()
.define("init", Definition::generate()
.register::<SystemColor>("System")?
.register::<Color>("Color")?
.register::<Example>("Example")
.document("Example module")
.value::<Example>("example")
.function::<Color, ()>("printColor", ())
.document("Greet the name that was passed in")
.param("name", "Name of the person to greet")
.function::<String, ()>("greet", ())
)
.finish();
let gen = DefinitionFileGenerator::new(definitions);
for (name, writer) in gen.iter() {
// Writes to a new file `init.d.lua`
writer.write_file(name).unwrap();
}
println!();
Ok(())
}Produces the following definition file
--- init.d.lua
--- @meta
--- @alias System SystemBlack
--- | SystemRed
--- | SystemGreen
--- | SystemYellow
--- | SystemBlue
--- | SystemCyan
--- | SystemMagenta
--- | SystemWhite
--- @class _System
--- @class SystemBlack: _System
--- @class SystemRed: _System
--- @class SystemGreen: _System
--- @class SystemYellow: _System
--- @class SystemBlue: _System
--- @class SystemCyan: _System
--- @class SystemMagenta: _System
--- @class SystemWhite: _System
System(SystemColor),
Xterm(u8),
Rgb(u8, u8, u8),
--- @alias Color ColorSystem | ColorXterm | ColorRgb
--- @class _Color
--- @class ColorSystem: _Color
--- @field [1] SystemColor
--- @class ColorXterm: _Color
--- @field [1] integer
--- @class ColorRgb: _Color
--- @field [1] integer
--- @field [2] integer
--- @field [3] integer
--- This is a doc comment section for the overall type
--- @class Example
--- Example complex type
--- @field color Color
--- Example module
--- @type Example
example = nil
--- Greet the name that was passed in
--- @param name string Name of the person to greet
function greet(name) end
--- @param param0 Color
function printColor(param0) endThere are helper macros that make writing lua integrations simplier and less manual. There are variants that support recording type information, and variants that just focus on making the creation of custom userdata types simple.
use std::path::PathBuf;
use mlua_extras::{
TypedUserData,
typed::generator::{
Definition, DefinitionFileGenerator, Definitions, LuauDefinitionFileGenerator,
},
typed_user_data_impl,
};
/// Simple Counter
#[derive(Clone, TypedUserData)]
struct Counter { value: i64 }
#[typed_user_data_impl]
impl Counter {
/// The default count
const COUNT: usize = 10;
/// Max count value
#[field]
fn max() -> i64 {
i64::MAX
}
/// Min count value
#[field(rename = "MIN")]
fn min() -> i64 {
0
}
/// Direction of the counter
#[getter("direction")]
fn get_direction(&self) -> String {
"up".into()
}
#[setter("direction")]
fn set_direction(&mut self, dir: String) {
println!("Direction: {dir}");
}
/// Get the current counter value
#[method]
fn get(&self) -> i64 { self.value }
/// Increment the counter
#[method]
fn increment(&mut self) { self.value += 1 }
/// Create a new table
#[method]
fn create_table(&self, lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
lua.create_table()
}
/// String representation of the counter
#[metamethod(ToString)]
fn to_string(&self) -> String { format!("Counter({})", self.value) }
// Requires the `async` feature
// Must be accessed from lua code with an entry of `mlua::Chunk::eval_async` or `mlua::Chunk::exec_async`
/// Fetch the global counter online
#[method]
async fn fetch(&self, lua: mlua::Lua, url: String) -> mlua::Result<String> {
_ = lua;
Ok(format!("fetched: {url}"))
}
}
fn main() -> mlua::Result<()> {
let definitions: Definitions = Definitions::start()
.define("macros", Definition::start().register::<Counter>("Counter"))
.finish();
let types_path = PathBuf::from("examples/types");
if !types_path.exists() {
std::fs::create_dir_all(&types_path).unwrap();
}
let dfg = DefinitionFileGenerator::new(definitions.clone());
for (name, writer) in dfg.iter() {
println!("==== Generated \x1b[1;33mexample/types/{name}\x1b[0m ====");
writer.write_file(types_path.join(name)).unwrap();
}
Ok(())
}Results in the lua type definition
--- @meta
--- Simple Counter
--- @class Counter
--- Direction of the counter
--- @field direction string
--- @field value integer
local _CLASS_Counter_ = {
--- The default count
COUNT = 10,
--- Min count value
MIN = 0,
--- Max count value
max = 9223372036854775807,
--- Create a new table
--- @param self Counter
--- @return table
create_table = function(self) end,
--- Fetch the global counter online
--- @param self Counter
--- @param url string
--- @return string
fetch = function(self, url) end,
--- Get the current counter value
--- @param self Counter
--- @return integer
get = function(self) end,
--- Increment the counter
--- @param self Counter
increment = function(self) end,
__metatable = {
--- @param param1 userdata
--- @param param2 any
--- @return any
__index = function(param1, param2) end,
--- @param param1 userdata
--- @param param2 any
--- @param param3 any
--- @return any | nil
__newindex = function(param1, param2, param3) end,
--- String representation of the counter
--- @param self Counter
--- @return string
__tostring = function(self) end,
}
}To run all the tests in one shot, use cargo test --features luau,vendored,send,async,serialize,derive
Some features of this crate generate luau compatible definition files, or use luau specific features. To add an additional layer of validation to the tests you can install luau-lsp and the tests will run the type checker, and fail if the results are not as expected.
See our luau docs for more information on installing the lsp; there are pre-built binaries available which makes it quick and painless.
The TEST_LUAU environment variable controls luau-lsp validation:
| Value | Behavior |
|---|---|
| (unset) | Auto-detect luau-lsp on PATH. If found, run validation; otherwise skip. |
0 |
Skip validation entirely, even if luau-lsp is on PATH. |
| path | Use the given value as the luau-lsp binary path (e.g. TEST_LUAU=/tmp/luau-lsp). |
Some tests validate the LuaLS-format (.d.lua) definition files generated by
DefinitionFileGenerator. These tests use
lua-language-server, which
ships as a self-contained binary with no runtime dependencies. Pre-built
archives for Linux, macOS, and Windows are available on the
releases page.
To install on Linux x64:
VERSION=$(curl -sI https://github.com/LuaLS/lua-language-server/releases/latest \
| grep -i location | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
curl -fsSL "https://github.com/LuaLS/lua-language-server/releases/download/${VERSION}/lua-language-server-${VERSION}-linux-x64.tar.gz" \
| tar -xz -C ~/.local
export PATH="$HOME/.local/bin:$PATH"The TEST_LUALS environment variable controls validation:
| Value | Behavior |
|---|---|
| (unset) | Auto-detect lua-language-server on PATH. If found, run validation; otherwise skip. |
0 |
Skip validation entirely, even if lua-language-server is on PATH. |
| path | Use the given value as the binary path (e.g. TEST_LUALS=/opt/lua-ls/bin/lua-language-server). |