Section: The host owns the interpreter.
mlua creates and controls a Lua virtual machine from the host. The project pins:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
lua54 chooses Lua 5.4. vendored builds the matching Lua implementation with the crate, so a learner does not need to find a separate Lua D L L, dynamic link library. error-send lets contextual host error-reporting carry a Lua failure through anyhow without discarding its details.
The interpreter is a subsystem with its own state, memory manager, call stack, and error reporting. Embedding does not merge Lua and the host into one language. mlua builds a checked bridge that converts values and controls when execution crosses between them.
Treat each crossing as an interface:
host-to-Lua inputs need a documented table shape and units;
Lua-to-host requests need type checks and validation against current game state;
errors need enough context to identify the script and boundary;
callbacks need time and resource limits appropriate to the host loop.
The safest design passes snapshots and requests, not borrowed pointers into live game objects.
The host remains in charge even while Lua decides what it would like to do:
Lua plans with owned data; the host decides whether each plan is still valid at commit time.
Section: Load only the libraries you need.
The Rust host creates the interpreter and selects three standard-library groups. new_with returns an error if setup fails; ? passes that error to the caller before any script runs.
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
This lab needs table helpers, string formatting, and math. It does not load the Lua io, os, debug, or package libraries.
Removing libraries is not a complete hostile-code sandbox. It is still good capability hygiene: a script cannot call a function that does not exist in its environment.
Section: Expose one host function.
The script needs a way to report what it observed. This closure accepts one Lua string, prints it with a prefix in the host process, and returns success; registering it under log makes the name callable from Lua.
Optional: practise the named operations and their inputs with a few whole-token blanks.
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
create_function converts Lua arguments into the requested host type. Passing a table where a String is required produces a Lua error instead of an unchecked cast.

The first closure argument is the active Lua handle. It is named _ here because logging does not need to create any Lua values.
Underneath, every call into a host function travels through Lua’s value stack: the caller supplies arguments, and the host function returns results through that stack. The lab’s game.distance(a, b) takes two position tables and returns one number. Take this call, and call its first table p and its second q:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
The stack passes through three states:
Section: Prefer a namespace table.
The single global log works for a first example, but a host with many functions needs an obvious boundary. Put those functions under one game table, so the next script call spells out who supplied the behavior:
Alternative: The game table is a labelled service counter: grouping callbacks makes it clear which operations come from the host.
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Lua then calls:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
The game table becomes the documented boundary between engine and script.
Section: Convert host records into Lua tables.
The complete host creates a table for each copied entity:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Nested tables make the script readable: entity.position.x is clearer than entity[4].
For the lab’s sample data, the conversion produces this shape. Every Rust index moves up by one key, and each entity becomes two tables, one nested in the other:
The entity with id equals 0 sits at Lua key 1: the ID is data copied from the game, and the key is only its place in the sequence.
Section: Load a named script with context.
script_path is the file the host chose to run. Rust reads its text, gives the Lua chunk that path as a name for errors, and executes it; failure at any step returns to the host through ?.
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Naming the chunk gives errors a useful file label. The complete lab adds anyhow::Context so the host error also says which script failed.
Section: Separate setup, execution, and results.
A clear host follows this order:
create the Lua state;
apply resource limits;
construct the host A P I, application programming interface;
load script text;
execute it once or register callbacks;
collect bounded requested actions;
validate those actions in the host;
report errors without crashing the engine.
Do not let a script run during partial A P I construction. Do not hold a mutable game-state lock across script execution. Give Lua a copied snapshot and collect requests for later processing.
Narration: A script can call back into the host while it runs. Keeping an engine lock across that call can make otherwise valid work wait on itself.
This is a two-phase boundary. During planning, Lua reads an immutable snapshot and returns proposed actions. During commit, the host checks that the target, snapshot version, and entity IDs still match, and that the action is permitted in the current state. A plan based on an old snapshot is rejected instead of being applied to a different moment.
Section: Run all three useful scripts.
The observer logs copied entity data. The nearest-entity script measures living candidates and proposes one selection; the state-machine script proposes a marker and then stops. Compare those proposals with the host’s accepted-action output after each run.
Terminal window
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
The host prints accepted actions after the script returns. That delay is deliberate: Lua proposes data, and the host remains the authority that decides what happens.
In brief: Lua proposes data; the host checks and applies the proposal after script execution returns successfully.
Section: Why the host still needs validation.
mlua safely converts values at the language boundary, but it cannot decide whether an entity exists or an action is currently allowed. The host must still check:
entity IDs exist in the current snapshot;
coordinates are finite and within the map;
an action is allowed in the current state;
the script has not exceeded a rate limit;
the target still matches the verified local build.
Converting a value safely does not prove that the resulting action is allowed. A good host checks both.