stanchion GitHub

Typed class bindings

How #[lua_class] turns a Rust trait into a Lua class binding: what it generates, the attributes it accepts, and what it validates.

What the macro generates

ItemRole
trait Greeterrewritten to hold only &self methods, so it stays dyn-compatible
GreeterClasshandle to the class table; receiverless trait fns become inherent methods on it
GreeterHandleinstance handle; impl Greeter for GreeterHandle delegates to the Lua object
FromLua for bothvalidates the contract at the load boundary
LuaClass / LuaObject implsCLASS_NAME, required_functions(), required_methods(), table access

Because the trait survives as a plain Rust trait, Lua-backed and native implementations mix freely:

let registry: Vec<Box<dyn Greeter>> = vec![Box::new(handle), Box::new(NativeGreeter)];

Method attributes

AttributeEffect
(none), with &selfobj:name(..) — colon call, object passed implicitly
(none), no receiverClass.name(..) — moved onto {Trait}Class
#[lua(function)]obj.name(..) — dot call on an instance
#[lua(field)]field read (no args) or write (exactly one arg)
#[lua(optional)]missing key yields Ok(None); return type must be Result<Option<T>>
#[lua(name = "..")]override the Lua key

#[lua_class(class = "..", handle = "..", name = "..")] renames the generated types and the class name used in error messages. A set_-prefixed field method defaults to the key without the prefix (set_greetinggreeting).

Lookups go through ObjectLike, which honours __index, so methods inherited from a base class resolve and validate correctly.

Error types

A method returns Result<T, E> for any E: From<mlua::Error>, so a host with its own error enum does not wrap every call site:

enum HostError {
    Lua(mlua::Error),
    Policy(String),
}

impl From<mlua::Error> for HostError {
    fn from(err: mlua::Error) -> Self {
        HostError::Lua(err)
    }
}

#[lua_class]
pub trait Greeter {
    fn new(greeting: String) -> Result<Self, HostError>;
    fn greet(&self, who: String) -> Result<String, HostError>;
}

mlua::Result still works unchanged: the conversion the macro emits is then the blanket impl<T> From<T> for T, which compiles away. The return type does have to be written as a Result-shaped path — the macro reads it syntactically rather than resolving it — so a type HostResult<T> = Result<T, HostError>; alias is fine, but a bare -> String is not.

Validation

FromLua checks that every required key resolves to a function before handing back a handle, so a malformed plugin fails at load with a typed error rather than attempt to call a nil value mid-request:

error converting Lua table to GreeterHandle (missing required function `greet`)

#[lua(optional)] methods and fields are exempt.

#[cfg] on a trait method is honoured end to end: the method is dropped from the trait, from the impl, and from the required-key set, so a class compiled without it still loads. That is why the required keys are required_methods() / required_functions() rather than consts — array elements cannot carry #[cfg].

Tables or userdata

A handle wraps either a Lua table or userdata. Nothing in the trait says which, and the same contract binds both:

#[lua_class]
pub trait Tally {
    fn new(start: i64) -> Result<Self>;
    fn bump(&self, amount: i64) -> Result<i64>;
}
local Tally = {}
Tally.new = make_counter   -- a Rust function returning userdata
return Tally

mlua’s ObjectLike covers both shapes but is sealed, so LuaHandle re-dispatches the operations generated code needs. Validation resolves required methods through __index, which reaches methods on a userdata metatable; userdata carrying no methods at all has no __index and raises when probed, so that is reported as the missing method rather than as a Lua error.

LuaObject::handle() returns the LuaHandle; table() returns Option<&Table> for the features that genuinely need a table, such as the registry’s exports proxies.


← Documentation index