Expand description
API tables a script can read and cannot rewrite.
§Why a proxy
The engine hands Lua a table per subsystem — input, entity, physics — and every script
shares those objects. Sandboxing _G fixed one half of that: a script’s globals are its own
now. It did not fix this half, because input is not a global write, it is a field write on
a shared object. Measured: input.is_pressed = function() return true end in one script, and
every other script sees the replacement for the rest of the session.
A __newindex metamethod alone does not close it. __newindex fires only for keys the table
does not already have, and every key worth clobbering — is_pressed, spawn, apply_force
— is a key the table already has. Assigning to those writes straight through the metatable.
So the global a script sees is an empty proxy. Empty means every read misses and goes through
__index to the real table, and every write — new key or not — reaches __newindex, which
raises. The real table lives in the Lua registry, which is reachable from Rust and not from
Lua at all: no global points at it, so a script cannot walk to it, and __metatable blocks
getmetatable from lifting it out of the proxy.
Rust still writes the real table every frame with raw_set, which bypasses metamethods by
definition. That is the asymmetry the whole arrangement exists to create: the engine writes,
the scripts read.
§What it does not stop
A script can still shadow the name in its own environment (input = something_else) — that is
what _G isolation makes safe, since the shadow is private to that script. And a table the
engine hands out by value, rather than exposing as a global, is unaffected; this covers the
long-lived API surface, not every table that crosses the boundary.
Functions§
- raw
- The real table behind a protected API, for the engine’s own per-frame writes.
- register_
protected - Publish
nameas a read-only API table.