asobi_lua_loader (asobi v0.75.1)

View Source

Loads Lua scripts into a hardened Luerl state.

The state is built on top of luerl:init/0 and then has every dangerous standard-library entry point cleared:

  • os.execute, os.exit, os.getenv, os.remove, os.rename, os.tmpname
  • io (the whole library)
  • dofile, loadfile, load, loadstring
  • package (the whole library) — replaced by an asobi_lua-controlled require/1 so scripts can still split logic across files

require/1 resolves names relative to the directory of the script that was loaded. Names must match [a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*, so dotted module paths work (require("bots.chaser")<base>/bots/chaser.lua) but parent traversal (..), absolute paths, and arbitrary characters are rejected. Module results are cached in a private _ASOBI_LOADED table so repeat require calls return the same value.

math.random and math.sqrt are overridden to call into the Erlang rand and math modules respectively — Luerl's defaults are slower and less deterministic than the BEAM equivalents.

Use init_sandboxed/0 when you need a hardened state with no script attached (e.g. for evaluating a config.lua manifest); use new/1 to load a specific script and pin its base directory for require.

Summary

Types

pre_install()

-type pre_install() :: fun((dynamic()) -> dynamic()).

Functions

call/3

-spec call(atom() | [atom() | binary()], [term()], dynamic()) ->
              {ok, [term()], dynamic()} | {error, term()}.

call(FuncPath, Args, St, TimeoutMs)

-spec call(atom() | [atom() | binary()], [term()], dynamic(), non_neg_integer()) ->
              {ok, [term()], dynamic()} | {error, timeout | heap_exhausted | term()}.

collect_state/1

-spec collect_state(map()) -> map().

Collect the Luerl state carried in a bridge state map, every so often.

Call this once per tick from a bridge that holds a long-lived lua_state. Luerl never collects such a state on its own, so without this the per-tick luerl:encode/2 of entities or inputs accumulates in it forever - and call/4 copies the whole state into its eval worker and back on every callback, so the cost of a tick grows with everything the state has ever allocated. Collect on the calling process: it costs no extra copy there, and luerl:gc/1 runs no Lua code so it cannot hang on a script.

game_state is the one Luerl value asobi holds between callbacks. It is unreachable from the Lua root set while Erlang is between them, so the collector would free the tables underneath it and the next luerl:decode/2 on it would crash. It is rooted in a global for the duration of the collection and unrooted again straight after, so a script never observes the anchor.

Bookkeeping is kept under lua_gc in the same map. Set {asobi, [{lua_gc, false}]} to turn the collector off entirely.

do_with_timeout(Code, St, TimeoutMs)

-spec do_with_timeout(string() | binary(), dynamic(), non_neg_integer()) ->
                         {ok, dynamic()} | {error, term()}.

init_sandboxed()

-spec init_sandboxed() -> dynamic().

is_defined(FuncName, St)

-spec is_defined(atom(), dynamic()) -> boolean().

new(ScriptPath)

-spec new(binary() | string()) -> {ok, dynamic()} | {error, term()}.

new(ScriptPath, TimeoutMs)

-spec new(binary() | string(), non_neg_integer()) -> {ok, dynamic()} | {error, term()}.

new(ScriptPath, TimeoutMs, PreInstall)

-spec new(binary() | string(), non_neg_integer(), pre_install()) -> {ok, dynamic()} | {error, term()}.