Skip to main content

Module api_table

Module api_table 

Source
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 name as a read-only API table.