# benilla's `mlua-sys` fork: what differs from upstream, and how to check Upstream: [`mlua-rs/mlua`](https://github.com/mlua-rs/mlua), MIT, crate `mlua-sys` version `0.10.0`, the version mlua 0.11.6 resolves to. Wired in through `[patch.crates-io]` in the workspace root. ## Why a fork exists at all `src/lua51/compat.rs` is mlua-sys' back-port of the Lua 5.2/5.3 auxiliary library onto Lua 5.1, derived from `lua-compat-5.3`. One of the functions it back-ports is `luaL_traceback`, and 5.3's `luaL_traceback` names each stack frame by **searching for the frame's function by value**: the globals table, then one level into every table in it, comparing every value with `lua_rawequal` until it finds a match (`compat53_findfield` / `compat53_pushglobalfuncname`). That is run once per stack level, per traceback. mlua builds exactly that traceback for **every `Err` a Rust binding returns** — `callback_error_ext` in mlua's `src/state/util.rs` wraps the error as `Error::CallbackError { traceback, cause }` before re-raising it — and benilla's Lua surface is ~2,400 Rust bindings that raise by returning `Err` (`Usage: UnitName("unit")`, `Unknown unit name: …`, the addon instruction budget). The search's cost is the size of `_G`, and a loaded 1.12 interface's `_G` is not small. Measured by `benilla-app`'s `addon_harness::tick_bench` on the shipped FrameXML plus the 23 addons under `benilla-config/AddOns`: ```text [raise] what mlua's handler would walk: 27114 globals, 83372 keys one level in, biggest _G (27114) [raise] a binding's own `Usage:` raise, caught by Lua: 6949.2 us ← before this fork [raise] the same raise in a bare VM (1169 globals): 83.2 us ``` **6.9 ms for one raise** (7.0–7.5 ms across runs), and a raise an addon wraps in `pcall` pays all of it and reaches no log: `pcall` catches the error before the host ever sees it, so the only symptom is a frame that took too long. Five such raises a frame is an entire frame budget, with a clean error log — 44.7 ms of VM tick, measured below. benilla already had to route around the same search once: handler and `C_Timer` dispatch go through Lua's own `xpcall` rather than mlua's protected call, for this reason (`benilla_ui::script::event::PROTECTED_CALL`, whose doc comment carries the measurement). That route cannot reach `callback_error_ext`, which is inside the `lua_CFunction` mlua wraps every binding in. A fork is the only place the cost can be removed from. ## What is not being done benilla is not forking mlua, and not changing the error contract. The raise's **message** is untouched: it is built by the binding, carried as the `cause`, and printed first. The traceback still has its `stack traceback:` header, its `\n\t`-indented frame list, its `::` prefixes and its `(...tail calls...)` and `(skipping %d levels)` lines. Only how a frame's *function* is named changes, and only for frames whose call site gives no name. The other five Lua dialects and the whole of Luau are upstream's, untouched, including their own copies of `compat53_findfield` (`src/lua52/compat.rs`, `src/lua53/compat.rs`, `src/luau/compat.rs`). benilla builds `lua51` and only `lua51`; a hunk in the others would be a hunk nobody compiles. ## The delta, and the command that proves it One file, `src/lua51/compat.rs`. Two deletions and one changed line, all under a `BENILLA DELTA` banner that carries the reasoning at the site: | hunk | why | |---|---| | `compat53_findfield` and `compat53_pushglobalfuncname` deleted, and the `std::ffi::CStr` import with them (its only use was the `_G.` prefix strip in `compat53_pushglobalfuncname`) | the globals-table search, 6.9 ms a raise on this interface | | `compat53_pushfuncname`'s first arm is now `*ar.namewhat != '\0'` → `function ''`, i.e. the name from the call site | this *is* Lua 5.1's own naming: `db_errorfb` in `third_party/lua-src/lua-5.1.5/ldblib.c:357` writes `" in function " LUA_QS` off `ar.name` under exactly that test, and searches nothing | Upstream's remaining arms (`main chunk`, `function <%s:%d>`, `?`) are unchanged and in the same order, so a frame the search would not have named is named exactly as before. ### Why the logged text does not move `lua_getinfo`'s `name`/`namewhat` come from the **call site**: a binding called as a global carries `namewhat = "global"`, `name = "UnitName"`, which is the same name the `_G` search would have found. Upstream printed `function 'UnitName'` from the search and `global 'UnitName'` from the call site; this fork prints `function 'UnitName'` from the call site, which is byte-identical to what the log carried before. That matters: `benilla-app`'s `addon_harness` reads `in function 'loadaddon'` out of a traceback to decide whose raise a demand-load's error is (`raised_inside_a_demand_load`), and the `Usage:`/`Unknown unit name:`/instruction-budget messages have tests pinning them. What does change is a frame whose function is a global *and* whose call site gives no name — a tail call, or a function handed to `pcall` by value. Those drop from `function 'Name'` to `function ` (Lua frame) or `?` (C function), which is what Lua 5.1 and the 1.12 client's own `debugstack` (`0x703760`, reimplemented in `benilla_ui::script::stdlib::debugstack`) both print there, and what benilla's handler-dispatch traceback already printed. ### Verifying against upstream ```sh # 0.10.0 is the pinned version; adjust the path if cargo's registry hash differs. diff -r --exclude=BENILLA.md --exclude=LICENSE --exclude=Cargo.toml \ --exclude=Cargo.lock --exclude=Cargo.toml.orig --exclude='.cargo*' \ third_party/mlua-sys ~/.cargo/registry/src/*/mlua-sys-0.10.0 ``` That must name exactly `src/lua51/compat.rs` and nothing else. The excludes are all bookkeeping: `Cargo.toml` because the fork prepends a header comment to cargo's generated manifest; `LICENSE` because the registry copy ships none and the fork carries mlua's MIT text beside the source; and `Cargo.lock`, `Cargo.toml.orig` and `.cargo-ok`/`.cargo_vcs_info.json` because they are the registry's own packaging artefacts, which a path dependency ignores. ## What this bought, measured Same bench, same command (`cargo test --release -p benilla-app --lib -- --ignored --nocapture tick_bench`), same interface: | row | before | after | |---|---|---| | a binding's own `Usage:` raise, caught by Lua | 6949.2 us | **2.8 us** | | the same raise in a bare VM (1169 globals) | 83.2 us | 2.6 us | | `UiScript::run` of a raising chunk, through mlua | 6434.3 us | 2.3 us | | five `pcall`'d binding raises a frame, over the whole VM tick | 45.104 ms/frame | 0.507 ms/frame | | the same frame with the raises disarmed (the control) | 0.412 ms/frame | 0.446 ms/frame | Two rows carry the argument. The bare-VM row is the control that names the cause: 1,169 globals cost 83 us where 27,114 cost 6,949, a ratio of 84 against a `_G` ratio of 23 — the search is quadratic-ish because it also walks one level in. And the five-raises row is the shape a player actually hits: an interface that swallows five binding raises a frame spent **44.7 ms of every frame** inside the VM tick and wrote **nothing** to any error log, which is a frame budget gone with no symptom an instrument was looking for. The test that fails if the search returns is `benilla_ui::script::tests::caught_raise_cost::a_caught_binding_raise_does_not_cost_the_size_of_the_global_table`: it measures the identical caught raise in a bare VM and in a 21,171-global one and refuses a ratio over 4×. Reverting this fork takes it to 37.3× (3607.2 us against 96.8 us).