tinymux/mux/modules/engine/lua_mod.cpp
Stephen Dennis eb12a97f68 fix(lua/jit): pre-entry fallback, protect GETGLOBAL/EQ, CMP_RR types (#1835 #1836 #1837)
Phase 4 over-committed pre-entry setup failures (carg/depth/get_dbt) as
#-1 LUA JIT RUN FAIL; fall back to the interpreter when nothing ran.
Protect lua_getglobal and lua_compare via pcall so raising metamethods
cannot abort the server. Apply EQK-class type gates and NIL/BOOL kinds
to register-register CMP_RR equality, and decline mixed-type order.
2026-07-31 07:12:16 -06:00

1781 lines
52 KiB
C++

/*! \file lua_mod.cpp
* \brief Lua 5.4 scripting — embedded in engine.so.
*
* Embeds a sandboxed Lua 5.4 interpreter. Scripts live as LUA_* attributes
* on objects. The lua() softcode function dispatches through this module.
*
* Bridge functions use engine-internal APIs (externs.h) for permission
* checks and COM interfaces for cross-layer operations.
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include "engine_api.h"
#include "libmux.h"
#include "modules.h"
#include "lua_mod.h"
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <vector>
// Global pointer for bridge functions to reach the module instance.
// Safe because there is exactly one CLuaMod instance per process.
//
static CLuaMod *g_pLuaMod = nullptr;
// =========================================================================
// Per-execution context stored in Lua registry.
// =========================================================================
struct lua_exec_ctx
{
dbref executor;
dbref caller;
dbref enactor;
const UTF8 *pArgs[10];
int nArgs;
// compiled_route distinguishes the JIT leg for diagnostics and any
// future route-specific policy. It no longer forbids world effects:
// #1751 Phase 4 deleted post-entry interpreter re-run, so the #1750
// "compiled path is effect-free" medicine is obsolete. Effects on the
// compiled path are delivered exactly once under the same permission
// checks as the interpreter (softcode contract).
bool compiled_route;
};
// effect_refused / lua_refuse_compiled_effect removed with the effect-free
// corridor. Bridge functions below run on both routes.
#define LUA_EXEC_CTX_KEY "mux_exec_ctx"
#define LUA_MOD_KEY "mux_lua_mod"
static lua_exec_ctx *get_exec_ctx(lua_State *L)
{
lua_getfield(L, LUA_REGISTRYINDEX, LUA_EXEC_CTX_KEY);
lua_exec_ctx *ctx = static_cast<lua_exec_ctx *>(lua_touserdata(L, -1));
lua_pop(L, 1);
return ctx;
}
static CLuaMod *get_lua_mod(lua_State *L)
{
(void)L;
return g_pLuaMod;
}
// =========================================================================
// mux.* bridge functions (Lua C functions)
// =========================================================================
// mux.notify(dbref, message) — send text to a player.
//
// Permission model matches @pemit: executor must be nearby, have
// Long_Fingers, or control the target. If pemit_players is on and
// the target is a connected player, page-lock is checked. If
// pemit_any is on, any @pemit to a player is allowed.
//
static int bridge_notify(lua_State *L)
{
CLuaMod *mod = get_lua_mod(L);
if (nullptr == mod) return 0;
lua_exec_ctx *ctx = get_exec_ctx(L);
if (nullptr == ctx) return 0;
dbref target = static_cast<dbref>(luaL_checkinteger(L, 1));
const char *msg = luaL_checkstring(L, 2);
dbref executor = ctx->executor;
if (!Good_obj(target))
{
lua_pushboolean(L, 0);
return 1;
}
// Enforce locality constraints (matches do_pemit_single).
//
bool ok = nearby(executor, target)
|| Long_Fingers(executor)
|| Controls(executor, target);
if ( !ok
&& isPlayer(target)
&& mudconf.pemit_players)
{
// Check page-lock without side effects.
//
ok = Connected(target)
&& could_doit(executor, target, A_LPAGE)
&& could_doit(target, executor, A_LPAGE);
}
if (!ok && mudconf.pemit_any)
{
ok = true;
}
if (!ok)
{
lua_pushboolean(L, 0);
return 1;
}
mux_INotify *pNotify = nullptr;
MUX_RESULT mr = mux_CreateInstance(CID_Notify, nullptr, UseSameProcess,
IID_INotify, reinterpret_cast<void **>(&pNotify));
if (MUX_SUCCEEDED(mr) && nullptr != pNotify)
{
pNotify->Notify(target,
reinterpret_cast<const UTF8 *>(msg));
pNotify->Release();
}
lua_pushboolean(L, 1);
return 1;
}
// mux.name(dbref) — return object name.
//
// Permission model matches name(): if read_rem_name is off, requires
// nearby_or_control, isPlayer, or Long_Fingers.
//
static int bridge_name(lua_State *L)
{
lua_exec_ctx *ctx = get_exec_ctx(L);
if (nullptr == ctx) { lua_pushnil(L); return 1; }
dbref obj = static_cast<dbref>(luaL_checkinteger(L, 1));
if (!Good_obj(obj))
{
lua_pushnil(L);
return 1;
}
if (!mudconf.read_rem_name)
{
if ( !Controls(ctx->executor, obj)
&& !nearby(ctx->executor, obj)
&& !isPlayer(obj)
&& !Long_Fingers(ctx->executor))
{
lua_pushnil(L);
return 1;
}
}
const UTF8 *pName = Name(obj);
if (nullptr != pName)
{
// For exits, return only the name before the first semicolon.
//
if (isExit(obj))
{
const char *semi = strchr(reinterpret_cast<const char *>(pName), ';');
if (semi)
{
lua_pushlstring(L, reinterpret_cast<const char *>(pName),
semi - reinterpret_cast<const char *>(pName));
return 1;
}
}
lua_pushstring(L, reinterpret_cast<const char *>(pName));
return 1;
}
lua_pushnil(L);
return 1;
}
// mux.owner(dbref) — return owner dbref.
// Softcode owner() uses match_thing + Owner(); absolute #dbref is allowed
// there too. Require Good_obj only (same as softcode after a successful match).
//
static int bridge_owner(lua_State *L)
{
dbref obj = static_cast<dbref>(luaL_checkinteger(L, 1));
if (!Good_obj(obj))
{
lua_pushnil(L);
return 1;
}
mux_IObjectInfo *pOI = nullptr;
MUX_RESULT mr = mux_CreateInstance(CID_ObjectInfo, nullptr,
UseSameProcess, IID_IObjectInfo,
reinterpret_cast<void **>(&pOI));
if (MUX_SUCCEEDED(mr) && nullptr != pOI)
{
dbref owner;
mr = pOI->GetOwner(obj, &owner);
pOI->Release();
if (MUX_SUCCEEDED(mr))
{
lua_pushinteger(L, owner);
return 1;
}
}
lua_pushnil(L);
return 1;
}
// mux.location(dbref) — return location dbref.
//
// Permission model matches loc(): requires locatable(executor, obj,
// enactor), which respects UNFINDABLE, nearby, see_all.
//
static int bridge_location(lua_State *L)
{
lua_exec_ctx *ctx = get_exec_ctx(L);
if (nullptr == ctx) { lua_pushnil(L); return 1; }
dbref obj = static_cast<dbref>(luaL_checkinteger(L, 1));
if (!Good_obj(obj))
{
lua_pushnil(L);
return 1;
}
if (!locatable(ctx->executor, obj, ctx->enactor))
{
lua_pushnil(L);
return 1;
}
lua_pushinteger(L, Location(obj));
return 1;
}
// mux.get(dbref, attrname) — read an attribute value.
//
static int bridge_get(lua_State *L)
{
lua_exec_ctx *ctx = get_exec_ctx(L);
if (nullptr == ctx) { lua_pushnil(L); return 1; }
int obj = static_cast<int>(luaL_checkinteger(L, 1));
const char *attrname = luaL_checkstring(L, 2);
mux_IAttributeAccess *pAA = nullptr;
MUX_RESULT mr = mux_CreateInstance(CID_AttributeAccess, nullptr,
UseSameProcess, IID_IAttributeAccess,
reinterpret_cast<void **>(&pAA));
if (MUX_SUCCEEDED(mr) && nullptr != pAA)
{
UTF8 value[8000];
size_t nValue = 0;
mr = pAA->GetAttribute(ctx->executor, static_cast<dbref>(obj),
reinterpret_cast<const UTF8 *>(attrname),
value, sizeof(value), &nValue);
pAA->Release();
if (MUX_SUCCEEDED(mr))
{
lua_pushlstring(L, reinterpret_cast<const char *>(value), nValue);
return 1;
}
}
lua_pushnil(L);
return 1;
}
// mux.set(dbref, attrname, value) — write an attribute value.
//
static int bridge_set(lua_State *L)
{
lua_exec_ctx *ctx = get_exec_ctx(L);
if (nullptr == ctx) return luaL_error(L, "no execution context");
int obj = static_cast<int>(luaL_checkinteger(L, 1));
const char *attrname = luaL_checkstring(L, 2);
const char *value = luaL_checkstring(L, 3);
mux_IAttributeAccess *pAA = nullptr;
MUX_RESULT mr = mux_CreateInstance(CID_AttributeAccess, nullptr,
UseSameProcess, IID_IAttributeAccess,
reinterpret_cast<void **>(&pAA));
if (MUX_SUCCEEDED(mr) && nullptr != pAA)
{
mr = pAA->SetAttribute(ctx->executor, static_cast<dbref>(obj),
reinterpret_cast<const UTF8 *>(attrname),
reinterpret_cast<const UTF8 *>(value));
pAA->Release();
if (MUX_FAILED(mr))
{
return luaL_error(L, "permission denied");
}
}
return 0;
}
// mux.eval(expression) — evaluate softcode expression.
//
static int bridge_eval(lua_State *L)
{
lua_exec_ctx *ctx = get_exec_ctx(L);
if (nullptr == ctx) { lua_pushnil(L); return 1; }
const char *expr = luaL_checkstring(L, 1);
mux_IEvaluator *pEval = nullptr;
MUX_RESULT mr = mux_CreateInstance(CID_Evaluator, nullptr,
UseSameProcess, IID_IEvaluator,
reinterpret_cast<void **>(&pEval));
if (MUX_SUCCEEDED(mr) && nullptr != pEval)
{
UTF8 result[8000];
size_t nResult = 0;
mr = pEval->Eval(ctx->executor, ctx->caller, ctx->enactor,
reinterpret_cast<const UTF8 *>(expr),
result, sizeof(result), &nResult);
pEval->Release();
if (MUX_SUCCEEDED(mr))
{
lua_pushlstring(L, reinterpret_cast<const char *>(result),
nResult);
return 1;
}
}
lua_pushnil(L);
return 1;
}
// mux.type(dbref) — return object type string.
//
static int bridge_type(lua_State *L)
{
int obj = static_cast<int>(luaL_checkinteger(L, 1));
mux_IObjectInfo *pOI = nullptr;
MUX_RESULT mr = mux_CreateInstance(CID_ObjectInfo, nullptr,
UseSameProcess, IID_IObjectInfo,
reinterpret_cast<void **>(&pOI));
if (MUX_SUCCEEDED(mr) && nullptr != pOI)
{
int type = -1;
mr = pOI->GetType(static_cast<dbref>(obj), &type);
pOI->Release();
if (MUX_SUCCEEDED(mr))
{
// Type constants: 0=ROOM, 1=THING, 2=EXIT, 3=PLAYER
static const char *types[] = {"ROOM", "THING", "EXIT", "PLAYER"};
if (type >= 0 && type <= 3)
{
lua_pushstring(L, types[type]);
}
else
{
lua_pushstring(L, "UNKNOWN");
}
return 1;
}
}
lua_pushnil(L);
return 1;
}
// mux.flags(dbref) — return flag string.
//
static int bridge_flags(lua_State *L)
{
lua_exec_ctx *ctx = get_exec_ctx(L);
int obj = static_cast<int>(luaL_checkinteger(L, 1));
mux_IObjectInfo *pOI = nullptr;
MUX_RESULT mr = mux_CreateInstance(CID_ObjectInfo, nullptr,
UseSameProcess, IID_IObjectInfo,
reinterpret_cast<void **>(&pOI));
if (MUX_SUCCEEDED(mr) && nullptr != pOI)
{
UTF8 *pFlags = nullptr;
dbref looker = (ctx != nullptr) ? ctx->executor : static_cast<dbref>(1);
mr = pOI->DecodeFlags(looker, static_cast<dbref>(obj), &pFlags);
pOI->Release();
if (MUX_SUCCEEDED(mr) && nullptr != pFlags)
{
lua_pushstring(L, reinterpret_cast<const char *>(pFlags));
return 1;
}
}
lua_pushnil(L);
return 1;
}
// mux.isplayer(dbref) — check if object is a player.
//
static int bridge_isplayer(lua_State *L)
{
int obj = static_cast<int>(luaL_checkinteger(L, 1));
mux_IObjectInfo *pOI = nullptr;
MUX_RESULT mr = mux_CreateInstance(CID_ObjectInfo, nullptr,
UseSameProcess, IID_IObjectInfo,
reinterpret_cast<void **>(&pOI));
if (MUX_SUCCEEDED(mr) && nullptr != pOI)
{
bool bPlayer = false;
mr = pOI->IsPlayer(static_cast<dbref>(obj), &bPlayer);
pOI->Release();
if (MUX_SUCCEEDED(mr))
{
lua_pushboolean(L, bPlayer ? 1 : 0);
return 1;
}
}
lua_pushboolean(L, 0);
return 1;
}
// mux.isconnected(dbref) — check if player is connected.
// Match softcode hasflag(obj,CONNECTED): pub_flags || Examinable || self (#1287).
//
static int bridge_isconnected(lua_State *L)
{
lua_exec_ctx *ctx = get_exec_ctx(L);
if (nullptr == ctx) { lua_pushboolean(L, 0); return 1; }
dbref obj = static_cast<dbref>(luaL_checkinteger(L, 1));
if ( !Good_obj(obj)
|| ( !mudconf.pub_flags
&& !Examinable(ctx->executor, obj)
&& obj != ctx->executor
&& obj != ctx->enactor))
{
lua_pushboolean(L, 0);
return 1;
}
mux_IObjectInfo *pOI = nullptr;
MUX_RESULT mr = mux_CreateInstance(CID_ObjectInfo, nullptr,
UseSameProcess, IID_IObjectInfo,
reinterpret_cast<void **>(&pOI));
if (MUX_SUCCEEDED(mr) && nullptr != pOI)
{
bool bConn = false;
mr = pOI->IsConnected(obj, &bConn);
pOI->Release();
if (MUX_SUCCEEDED(mr))
{
lua_pushboolean(L, bConn ? 1 : 0);
return 1;
}
}
lua_pushboolean(L, 0);
return 1;
}
// mux.pennies(dbref) — return object pennies.
// Match softcode money(): Examinable required (#1287). Absolute dbref
// without Examinable used to return balance for any object.
//
static int bridge_pennies(lua_State *L)
{
lua_exec_ctx *ctx = get_exec_ctx(L);
if (nullptr == ctx) { lua_pushnil(L); return 1; }
dbref obj = static_cast<dbref>(luaL_checkinteger(L, 1));
if (!Good_obj(obj) || !Examinable(ctx->executor, obj))
{
lua_pushnil(L);
return 1;
}
mux_IObjectInfo *pOI = nullptr;
MUX_RESULT mr = mux_CreateInstance(CID_ObjectInfo, nullptr,
UseSameProcess, IID_IObjectInfo,
reinterpret_cast<void **>(&pOI));
if (MUX_SUCCEEDED(mr) && nullptr != pOI)
{
int pennies = 0;
mr = pOI->GetPennies(obj, &pennies);
pOI->Release();
if (MUX_SUCCEEDED(mr))
{
lua_pushinteger(L, pennies);
return 1;
}
}
lua_pushnil(L);
return 1;
}
// mux.iswizard(dbref) — check if object is a wizard.
// Match softcode hasflag() object form: pub_flags || Examinable || self (#1287).
//
static int bridge_iswizard(lua_State *L)
{
lua_exec_ctx *ctx = get_exec_ctx(L);
if (nullptr == ctx) { lua_pushboolean(L, 0); return 1; }
dbref obj = static_cast<dbref>(luaL_checkinteger(L, 1));
if ( !Good_obj(obj)
|| ( !mudconf.pub_flags
&& !Examinable(ctx->executor, obj)
&& obj != ctx->executor
&& obj != ctx->enactor))
{
lua_pushboolean(L, 0);
return 1;
}
mux_IPermissions *pPerms = nullptr;
MUX_RESULT mr = mux_CreateInstance(CID_Permissions, nullptr,
UseSameProcess, IID_IPermissions,
reinterpret_cast<void **>(&pPerms));
if (MUX_SUCCEEDED(mr) && nullptr != pPerms)
{
bool bWizard = false;
mr = pPerms->IsWizard(obj, &bWizard);
pPerms->Release();
if (MUX_SUCCEEDED(mr))
{
lua_pushboolean(L, bWizard ? 1 : 0);
return 1;
}
}
lua_pushboolean(L, 0);
return 1;
}
// mux.controls(who, what) — check if who controls what.
//
// Permission model: executor can only query control relationships
// where 'who' is themselves or an object they control. This
// prevents scripts from probing wizard control relationships.
//
static int bridge_controls(lua_State *L)
{
lua_exec_ctx *ctx = get_exec_ctx(L);
if (nullptr == ctx) { lua_pushboolean(L, 0); return 1; }
dbref who = static_cast<dbref>(luaL_checkinteger(L, 1));
dbref what = static_cast<dbref>(luaL_checkinteger(L, 2));
if ( !Good_obj(who)
|| !Good_obj(what))
{
lua_pushboolean(L, 0);
return 1;
}
// Restrict: executor must be 'who' or must control 'who'.
//
if ( who != ctx->executor
&& !Controls(ctx->executor, who))
{
lua_pushboolean(L, 0);
return 1;
}
lua_pushboolean(L, Controls(who, what) ? 1 : 0);
return 1;
}
// Bridge function table.
//
static const luaL_Reg bridge_funcs[] = {
{"notify", bridge_notify},
{"pemit", bridge_notify},
{"name", bridge_name},
{"owner", bridge_owner},
{"location", bridge_location},
{"type", bridge_type},
{"flags", bridge_flags},
{"isplayer", bridge_isplayer},
{"isconnected", bridge_isconnected},
{"pennies", bridge_pennies},
{"get", bridge_get},
{"set", bridge_set},
{"eval", bridge_eval},
{"iswizard", bridge_iswizard},
{"controls", bridge_controls},
{nullptr, nullptr}
};
// =========================================================================
// Custom memory allocator with limit enforcement.
// =========================================================================
void *CLuaMod::LuaAlloc(void *ud, void *ptr, size_t osize, size_t nsize)
{
CLuaMod *self = static_cast<CLuaMod *>(ud);
if (nsize == 0)
{
// Free.
if (ptr != nullptr)
{
self->m_nMemUsed -= osize;
free(ptr);
}
return nullptr;
}
// Check memory limit.
//
size_t delta = nsize - (ptr ? osize : 0);
if (self->m_nMemUsed + delta > static_cast<size_t>(self->m_nMemLimit))
{
self->m_bMemExceeded = true;
return nullptr; // Allocation denied — triggers Lua OOM error.
}
void *newptr = realloc(ptr, nsize);
if (newptr != nullptr)
{
self->m_nMemUsed += delta;
if (self->m_nMemUsed > self->m_nMemPeak)
{
self->m_nMemPeak = self->m_nMemUsed;
}
}
return newptr;
}
// =========================================================================
// Instruction count hook — enforces execution limits.
// =========================================================================
// Fires every m_nInsnPoll VM instructions (#1591).
//
// The Lua module used to bound a chunk only by its own instruction and memory
// limits, neither of which is what the rest of the server bounds on: softcode
// bounds on wall time. alarm_clock is armed per command
// (alarm_clock.set(mudconf.max_cmdsecs) in cque.cpp), the AST evaluator polls
// it and answers "#-1 CPU LIMITED", and the JIT is handed the same flag as
// dbt->alarm_flag. Lua referenced it nowhere, so lua() ignored max_cmdsecs
// entirely.
//
// Shape borrowed from the JIT's guest-loop budget (#1571): count cheaply, do
// the real check periodically. The hook fires on a poll interval rather than
// once at the limit, checks the alarm, and only then accounts instructions --
// so the instruction limit keeps its previous meaning while wall time becomes
// the bound that agrees with everything else.
//
// This does NOT bound time spent inside a C function; the hook cannot fire
// there. The pattern-matcher half of #1591 is MatchInterrupt below, installed
// into lstrlib.c for the same pcall window.
//
// Wall-clock escape for the Lua pattern matcher (#1591).
//
// InsnCountHook below cannot cover this: lua_sethook(LUA_MASKCOUNT) counts VM
// instructions and does not fire inside a C function, and a pathological
// pattern spends all its time inside one call to string.find. lstrlib.c's
// own MAXCCALLS bounds recursion depth, which stops a stack overflow but not
// exponential backtracking -- that grows in breadth, so depth stays under 200
// while the matcher runs unbounded.
//
// lstrlib.c calls this every MATCH_INTERRUPT_MASK+1 match() steps when the
// pointer is installed. Returning non-zero raises "cpu limited" there, which
// lands in the same m_bCpuLimited path as the instruction hook, so the caller
// still sees "#-1 CPU LIMITED" -- one budget, one message, all four routes.
//
extern "C" int (*lua_match_interrupt)(lua_State *L);
int CLuaMod::MatchInterrupt(lua_State *L)
{
if (!alarm_clock.alarmed)
{
return 0;
}
// Same instance recovery as InsnCountHook. Flagging the module is what
// turns the Lua error into the softcode answer; without it the player
// would get a raw "#-1 LUA ERROR: cpu limited" instead.
void *ud = nullptr;
lua_getallocf(L, &ud);
CLuaMod *self = static_cast<CLuaMod *>(ud);
if (nullptr != self)
{
self->m_bCpuLimited = true;
}
return 1;
}
void CLuaMod::InsnCountHook(lua_State *L, lua_Debug *ar)
{
(void)ar;
// The allocator ud is the module instance (lua_newstate(LuaAlloc, this)).
void *ud = nullptr;
lua_getallocf(L, &ud);
CLuaMod *self = static_cast<CLuaMod *>(ud);
if (nullptr != self)
{
if (alarm_clock.alarmed)
{
self->m_bCpuLimited = true;
luaL_error(L, "cpu limited");
}
self->m_nInsnUsed += self->m_nInsnPoll;
if (self->m_nInsnUsed < self->m_nInsnLimit)
{
return;
}
}
luaL_error(L, "instruction limit exceeded");
}
// =========================================================================
// Lua state creation and sandbox setup.
// =========================================================================
bool CLuaMod::CreateLuaState(void)
{
m_nMemUsed = 0;
m_nMemPeak = 0;
m_bMemExceeded = false;
m_L = lua_newstate(LuaAlloc, this);
if (nullptr == m_L)
{
return false;
}
// Open whitelisted libraries.
//
luaL_requiref(m_L, "_G", luaopen_base, 1);
lua_pop(m_L, 1);
luaL_requiref(m_L, "string", luaopen_string, 1);
// string.dump reifies bytecode and is an escape hatch for reconstructing
// blocked loaders; softcode has no equivalent. (#1287)
lua_getglobal(m_L, "string");
if (lua_istable(m_L, -1))
{
lua_pushnil(m_L);
lua_setfield(m_L, -2, "dump");
}
lua_pop(m_L, 1);
luaL_requiref(m_L, "table", luaopen_table, 1);
lua_pop(m_L, 1);
luaL_requiref(m_L, "math", luaopen_math, 1);
lua_pop(m_L, 1);
luaL_requiref(m_L, "utf8", luaopen_utf8, 1);
lua_pop(m_L, 1);
luaL_requiref(m_L, "coroutine", luaopen_coroutine, 1);
lua_pop(m_L, 1);
// Remove dangerous functions from the global table.
//
// string.dump is removed via the string library patch below if present;
// base-library escapes that rebuild loaders are nilled here.
static const char *blocked[] = {
"load", "loadfile", "dofile", "require",
"rawget", "rawset", "rawequal", "rawlen",
"collectgarbage", nullptr
};
for (int i = 0; blocked[i] != nullptr; i++)
{
lua_pushnil(m_L);
lua_setglobal(m_L, blocked[i]);
}
// Remap print to do nothing (scripts should use mux.notify).
//
lua_pushcfunction(m_L, [](lua_State *) -> int { return 0; });
lua_setglobal(m_L, "print");
// Register mux.* bridge table.
//
luaL_newlib(m_L, bridge_funcs);
lua_setglobal(m_L, "mux");
// Store module pointer in registry for bridge function access.
//
lua_pushlightuserdata(m_L, this);
lua_setfield(m_L, LUA_REGISTRYINDEX, LUA_MOD_KEY);
return true;
}
void CLuaMod::DestroyLuaState(void)
{
if (nullptr != m_L)
{
lua_close(m_L);
m_L = nullptr;
}
}
// =========================================================================
// Chunk execution with sandbox.
// =========================================================================
// Execution-context setup shared by the interpreter and compiled routes.
//
// The bridge C functions (mux.eval, mux.name, ...) read the executor and
// friends from the registry, and per-run values (mux.executor, mux.args)
// are injected into the global mux table. This used to live inline in
// ExecuteChunk only -- the interpreter leg -- so a bridge function invoked
// from a COMPILED run found a cleared registry and stale mux fields. Now
// that compiled chunks call the real bridge functions through the ordinary
// Lua call path (#1745 follow-up), both routes must stage the same context.
//
// ctx is caller-owned: the registry holds a lightuserdata pointing at it,
// so it must outlive the run. Returns the PREVIOUS registry value so the
// teardown can restore rather than clear: nested runs (softcode -> lua
// under brackets can re-enter) used to stomp the outer run's context to
// nil, which #1750's adversarial review measured as a route-dependent
// divergence. Restore with lua_restore_exec_context.
//
// Nesting also used to leave the OUTER mux.args / executor fields destroyed:
// setup always overwrote the global mux table, and restore only put back the
// registry pointer. An outer chunk that did mux.eval("lua(...)") then
// re-read mux.args[k] saw the inner args (or nil) — plan residual "anytime
// item 3", pinned by smoke TC071. When nesting, the previous mux table
// fields are stacked in the registry and restored with the ctx pointer.
//
#define LUA_MUX_FIELDS_STACK "mux_fields_stack"
// Save mux.executor/caller/enactor/args onto a registry stack (nesting).
//
static void lua_save_mux_fields(lua_State *L)
{
lua_getglobal(L, "mux");
if (!lua_istable(L, -1))
{
lua_pop(L, 1);
return;
}
lua_createtable(L, 4, 0);
lua_getfield(L, -2, "executor");
lua_rawseti(L, -2, 1);
lua_getfield(L, -2, "caller");
lua_rawseti(L, -2, 2);
lua_getfield(L, -2, "enactor");
lua_rawseti(L, -2, 3);
lua_getfield(L, -2, "args");
lua_rawseti(L, -2, 4);
// stack: mux, saved
lua_getfield(L, LUA_REGISTRYINDEX, LUA_MUX_FIELDS_STACK);
if (!lua_istable(L, -1))
{
lua_pop(L, 1);
lua_newtable(L);
lua_pushvalue(L, -1);
lua_setfield(L, LUA_REGISTRYINDEX, LUA_MUX_FIELDS_STACK);
}
// stack: mux, saved, stack
const int n = static_cast<int>(lua_rawlen(L, -1));
lua_pushvalue(L, -2);
lua_rawseti(L, -2, n + 1);
lua_pop(L, 3); // stack, saved, mux
}
// Pop the most recently saved mux fields back onto the global mux table.
//
static void lua_restore_mux_fields(lua_State *L)
{
lua_getfield(L, LUA_REGISTRYINDEX, LUA_MUX_FIELDS_STACK);
if (!lua_istable(L, -1))
{
lua_pop(L, 1);
return;
}
const int n = static_cast<int>(lua_rawlen(L, -1));
if (n < 1)
{
lua_pop(L, 1);
return;
}
lua_rawgeti(L, -1, n);
// stack: stack, saved
lua_pushnil(L);
lua_rawseti(L, -3, n);
lua_getglobal(L, "mux");
if (!lua_istable(L, -1))
{
lua_pop(L, 3);
return;
}
// stack: stack, saved, mux
lua_rawgeti(L, -2, 1);
lua_setfield(L, -2, "executor");
lua_rawgeti(L, -2, 2);
lua_setfield(L, -2, "caller");
lua_rawgeti(L, -2, 3);
lua_setfield(L, -2, "enactor");
lua_rawgeti(L, -2, 4);
lua_setfield(L, -2, "args");
lua_pop(L, 3); // mux, saved, stack
}
static lua_exec_ctx *lua_setup_exec_context(lua_State *L, lua_exec_ctx &ctx,
dbref executor, dbref caller, dbref enactor,
const UTF8 *pArgs[], int nArgs, bool compiled_route)
{
lua_getfield(L, LUA_REGISTRYINDEX, LUA_EXEC_CTX_KEY);
lua_exec_ctx *prev =
static_cast<lua_exec_ctx *>(lua_touserdata(L, -1));
lua_pop(L, 1);
// Nesting: keep the outer mux.* per-run fields so the outer chunk still
// sees its own args after the inner run returns (#1773 TC071).
//
if (nullptr != prev)
{
lua_save_mux_fields(L);
}
ctx.compiled_route = compiled_route;
ctx.executor = executor;
ctx.caller = caller;
ctx.enactor = enactor;
ctx.nArgs = (nArgs > 10) ? 10 : nArgs;
for (int i = 0; i < ctx.nArgs; i++)
{
ctx.pArgs[i] = (pArgs != nullptr) ? pArgs[i] : nullptr;
}
lua_pushlightuserdata(L, &ctx);
lua_setfield(L, LUA_REGISTRYINDEX, LUA_EXEC_CTX_KEY);
// Inject mux.executor, mux.caller, mux.enactor, mux.args into
// the global mux table.
//
lua_getglobal(L, "mux");
lua_pushinteger(L, executor);
lua_setfield(L, -2, "executor");
lua_pushinteger(L, caller);
lua_setfield(L, -2, "caller");
lua_pushinteger(L, enactor);
lua_setfield(L, -2, "enactor");
// Build mux.args table.
//
int nSafe = (nArgs > 0 && pArgs != nullptr) ? nArgs : 0;
lua_createtable(L, nSafe, 0);
for (int i = 0; i < nSafe; i++)
{
if (pArgs[i] != nullptr)
{
lua_pushstring(L, reinterpret_cast<const char *>(pArgs[i]));
}
else
{
lua_pushstring(L, "");
}
lua_rawseti(L, -2, i + 1);
}
lua_setfield(L, -2, "args");
lua_pop(L, 1); // pop mux table
return prev;
}
static void lua_restore_exec_context(lua_State *L, lua_exec_ctx *prev)
{
// Restore mux table fields before swapping the registry pointer back,
// so any bridge that peeks at mux.args during teardown (none today)
// still sees a coherent pair.
//
if (nullptr != prev)
{
lua_restore_mux_fields(L);
}
if (prev != nullptr)
{
lua_pushlightuserdata(L, prev);
}
else
{
lua_pushnil(L);
}
lua_setfield(L, LUA_REGISTRYINDEX, LUA_EXEC_CTX_KEY);
}
bool CLuaMod::ExecuteChunk(lua_State *L, dbref executor, dbref caller,
dbref enactor, const UTF8 *pArgs[], int nArgs,
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen)
{
// The compiled chunk is on top of the Lua stack. Set up the execution
// context and call it.
lua_exec_ctx ctx;
lua_exec_ctx *prev_ctx =
lua_setup_exec_context(L, ctx, executor, caller, enactor,
pArgs, nArgs, false);
// Set instruction count hook.
//
m_bMemExceeded = false;
m_bCpuLimited = false;
// Poll often enough to notice the alarm, but never less often than the
// instruction limit itself -- a small configured limit must still fire
// where it always did.
m_nInsnPoll = (m_nInsnLimit < LUA_ALARM_POLL_INSNS)
? m_nInsnLimit : LUA_ALARM_POLL_INSNS;
if (m_nInsnPoll < 1)
{
m_nInsnPoll = 1;
}
m_nInsnUsed = 0;
lua_sethook(L, InsnCountHook, LUA_MASKCOUNT, m_nInsnPoll);
// Cover the C-function gap the count hook cannot reach (#1591). Set on
// every call rather than once at startup: the pointer lives in lstrlib.c
// and costs nothing to reassign, and this way it cannot be left dangling
// by a module unload.
lua_match_interrupt = CLuaMod::MatchInterrupt;
// Call the chunk (it's below the mux table stuff we just popped).
//
int status = lua_pcall(L, 0, 1, 0);
// Remove the hook.
//
lua_sethook(L, nullptr, 0, 0);
lua_match_interrupt = nullptr;
lua_restore_exec_context(L, prev_ctx);
if (status != LUA_OK)
{
// Error.
const char *errmsg = lua_tostring(L, -1);
if (nullptr == errmsg) errmsg = "unknown error";
if (m_bCpuLimited)
{
// Answer exactly as the AST evaluator and the JIT do, rather than
// wrapping it as a Lua error: one budget, one message (#1591).
m_stats.cpu_limit_hits++;
m_stats.errors++;
const UTF8 *kMsg = S_("#-1 CPU LIMITED");
size_t n = strlen(reinterpret_cast<const char *>(kMsg));
if (n >= nResultMax)
{
n = nResultMax - 1;
}
memcpy(pResult, kMsg, n);
pResult[n] = '\0';
*pnResultLen = n;
lua_pop(L, 1);
return MUX_S_OK;
}
if (m_bMemExceeded)
{
m_stats.mem_limit_hits++;
errmsg = "memory limit exceeded";
}
else if (strstr(errmsg, "instruction limit") != nullptr)
{
m_stats.insn_limit_hits++;
}
m_stats.errors++;
size_t n = mux_snprintf(pResult, nResultMax,
T("#-1 LUA ERROR: %s"), errmsg);
*pnResultLen = n;
lua_pop(L, 1);
return false;
}
// Success — convert return value to string.
//
size_t len = 0;
const char *result = nullptr;
if (lua_isnil(L, -1) || lua_isnone(L, -1))
{
result = "";
len = 0;
}
else if (lua_isboolean(L, -1))
{
result = lua_toboolean(L, -1) ? "1" : "0";
len = 1;
}
else
{
result = lua_tolstring(L, -1, &len);
if (nullptr == result)
{
result = "";
len = 0;
}
}
if (len >= nResultMax)
{
len = nResultMax - 1;
}
memcpy(pResult, result, len);
pResult[len] = '\0';
*pnResultLen = len;
lua_pop(L, 1);
return true;
}
// =========================================================================
// CLuaMod — main module class.
// =========================================================================
CLuaMod::CLuaMod(void) : m_cRef(1),
m_pILog(nullptr),
m_pIServerEventsControl(nullptr),
m_pINotify(nullptr),
m_pIObjectInfo(nullptr),
m_pIAttributeAccess(nullptr),
m_pIEvaluator(nullptr),
m_pIPermissions(nullptr),
m_pIJITCompile(nullptr),
m_L(nullptr),
m_nInsnLimit(LUA_DEFAULT_INSN_LIMIT),
m_bCpuLimited(false),
m_nInsnPoll(LUA_ALARM_POLL_INSNS),
m_nInsnUsed(0),
m_nMemLimit(LUA_DEFAULT_MEM_LIMIT),
m_nMemUsed(0),
m_nMemPeak(0),
m_bMemExceeded(false),
m_nCacheMaxSize(LUA_DEFAULT_CACHE_SIZE)
{
memset(&m_stats, 0, sizeof(m_stats));
g_pLuaMod = this;
}
MUX_RESULT CLuaMod::FinalConstruct(void)
{
MUX_RESULT mr;
// Acquire logging interface.
//
mr = mux_CreateInstance(CID_Log, nullptr, UseSameProcess,
IID_ILog, reinterpret_cast<void **>(&m_pILog));
if (MUX_FAILED(mr))
{
return mr;
}
// Register for server events.
//
mux_IServerEventsSink *pSink = nullptr;
mr = QueryInterface(IID_IServerEventsSink,
reinterpret_cast<void **>(&pSink));
if (MUX_SUCCEEDED(mr))
{
mr = mux_CreateInstance(CID_ServerEventsSource, nullptr,
UseSameProcess, IID_IServerEventsControl,
reinterpret_cast<void **>(&m_pIServerEventsControl));
if (MUX_SUCCEEDED(mr))
{
m_pIServerEventsControl->Advise(pSink);
}
pSink->Release();
}
// Acquire core interfaces.
//
mux_CreateInstance(CID_Notify, nullptr, UseSameProcess,
IID_INotify, reinterpret_cast<void **>(&m_pINotify));
mux_CreateInstance(CID_ObjectInfo, nullptr, UseSameProcess,
IID_IObjectInfo, reinterpret_cast<void **>(&m_pIObjectInfo));
mux_CreateInstance(CID_AttributeAccess, nullptr, UseSameProcess,
IID_IAttributeAccess,
reinterpret_cast<void **>(&m_pIAttributeAccess));
mux_CreateInstance(CID_Evaluator, nullptr, UseSameProcess,
IID_IEvaluator, reinterpret_cast<void **>(&m_pIEvaluator));
mux_CreateInstance(CID_Permissions, nullptr, UseSameProcess,
IID_IPermissions, reinterpret_cast<void **>(&m_pIPermissions));
// Acquire JIT compile interface (optional — graceful degradation).
mux_CreateInstance(CID_JITCompile, nullptr, UseSameProcess,
IID_IJITCompile, reinterpret_cast<void **>(&m_pIJITCompile));
// Create the Lua state.
//
if (!CreateLuaState())
{
if (nullptr != m_pILog)
{
bool fStarted;
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("INI"), T("ERR"));
if (fStarted)
{
m_pILog->log_text(T("Lua module: failed to create Lua state."));
m_pILog->end_log();
}
}
return MUX_E_FAIL;
}
// Log that we are alive.
//
if (nullptr != m_pILog)
{
bool fStarted;
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("INI"), T("INFO"));
if (fStarted)
{
m_pILog->log_text(T("Lua module loaded (Lua 5.4)."));
m_pILog->end_log();
}
}
return MUX_S_OK;
}
CLuaMod::~CLuaMod()
{
CacheClear();
DestroyLuaState();
if (nullptr != m_pILog)
{
bool fStarted;
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("INI"), T("INFO"));
if (fStarted)
{
m_pILog->log_text(T("Lua module unloading."));
m_pILog->end_log();
}
m_pILog->Release();
m_pILog = nullptr;
}
if (nullptr != m_pIServerEventsControl)
{
m_pIServerEventsControl->Release();
m_pIServerEventsControl = nullptr;
}
if (nullptr != m_pINotify)
{
m_pINotify->Release();
m_pINotify = nullptr;
}
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->Release();
m_pIObjectInfo = nullptr;
}
if (nullptr != m_pIAttributeAccess)
{
m_pIAttributeAccess->Release();
m_pIAttributeAccess = nullptr;
}
if (nullptr != m_pIEvaluator)
{
m_pIEvaluator->Release();
m_pIEvaluator = nullptr;
}
if (nullptr != m_pIPermissions)
{
m_pIPermissions->Release();
m_pIPermissions = nullptr;
}
if (nullptr != m_pIJITCompile)
{
m_pIJITCompile->Release();
m_pIJITCompile = nullptr;
}
if (g_pLuaMod == this)
{
g_pLuaMod = nullptr;
}
}
MUX_RESULT CLuaMod::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_ILuaControl *>(this);
}
else if (IID_ILuaControl == iid)
{
*ppv = static_cast<mux_ILuaControl *>(this);
}
else if (IID_IServerEventsSink == iid)
{
*ppv = static_cast<mux_IServerEventsSink *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CLuaMod::AddRef(void)
{
return m_cRef.fetch_add(1, std::memory_order_relaxed) + 1;
}
uint32_t CLuaMod::Release(void)
{
uint32_t prev = m_cRef.fetch_sub(1, std::memory_order_acq_rel);
if (1 == prev)
{
delete this;
return 0;
}
return prev - 1;
}
// =========================================================================
// Bytecode cache — LRU keyed by source text.
// =========================================================================
// LoadCached: try the cache first, compile on miss.
// On success, the compiled chunk is on top of the Lua stack.
// Returns true on success, false on compile error (error string on stack).
//
bool CLuaMod::LoadCached(const char *source, size_t nSource,
const char *chunkname)
{
std::string key(source, nSource);
auto it = m_cache.find(key);
if (it != m_cache.end())
{
// Cache hit — push the cached chunk.
//
m_stats.cache_hits++;
lua_rawgeti(m_L, LUA_REGISTRYINDEX, it->second.lua_ref);
// Move to front of LRU.
//
m_cache_lru.erase(it->second.lru_it);
m_cache_lru.push_front(key);
it->second.lru_it = m_cache_lru.begin();
return true;
}
// Cache miss — compile.
//
m_stats.cache_misses++;
int status = luaL_loadbufferx(m_L, source, nSource, chunkname, "t");
if (status != LUA_OK)
{
return false; // Error string is on top of stack.
}
// Store in cache: push a copy, get a registry reference.
//
lua_pushvalue(m_L, -1); // duplicate the chunk
int ref = luaL_ref(m_L, LUA_REGISTRYINDEX);
// Evict if full.
//
if (static_cast<int>(m_cache.size()) >= m_nCacheMaxSize)
{
CacheEvict();
}
// Insert.
//
m_cache_lru.push_front(key);
cache_entry entry;
entry.lua_ref = ref;
entry.lru_it = m_cache_lru.begin();
entry.jit_key = 0;
entry.jit_eligible = false;
m_cache[key] = entry;
return true;
}
void CLuaMod::CacheEvict(void)
{
if (m_cache_lru.empty())
{
return;
}
// Remove the least recently used entry (back of list).
//
const std::string &oldest = m_cache_lru.back();
auto it = m_cache.find(oldest);
if (it != m_cache.end())
{
luaL_unref(m_L, LUA_REGISTRYINDEX, it->second.lua_ref);
if (it->second.jit_key != 0 && nullptr != m_pIJITCompile)
{
m_pIJITCompile->Invalidate(it->second.jit_key);
}
m_cache.erase(it);
}
m_cache_lru.pop_back();
}
void CLuaMod::CacheClear(void)
{
for (auto &pair : m_cache)
{
if (nullptr != m_L)
{
luaL_unref(m_L, LUA_REGISTRYINDEX, pair.second.lua_ref);
}
if (pair.second.jit_key != 0 && nullptr != m_pIJITCompile)
{
m_pIJITCompile->Invalidate(pair.second.jit_key);
}
}
m_cache.clear();
m_cache_lru.clear();
}
// =========================================================================
// lua_dump writer callback — accumulates bytecode into a vector.
// =========================================================================
struct dump_buffer {
std::vector<uint8_t> data;
};
static int dump_writer(lua_State *L, const void *p, size_t sz, void *ud) {
(void)L;
dump_buffer *buf = static_cast<dump_buffer *>(ud);
const uint8_t *bytes = static_cast<const uint8_t *>(p);
buf->data.insert(buf->data.end(), bytes, bytes + sz);
return 0;
}
// =========================================================================
// TryJIT: attempt to JIT-compile a cached chunk.
// The chunk must be on top of the Lua stack.
// Returns true if JIT succeeded and result is in pResult.
// Returns false on JIT failure (caller should fall through to lua_pcall).
// Does NOT pop the chunk from the stack.
// =========================================================================
bool CLuaMod::TryJIT(cache_entry &entry, dbref executor, dbref caller,
dbref enactor, const UTF8 *pArgs[], int nArgs,
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen)
{
if (nullptr == m_pIJITCompile) return false;
// Safety gate (#1309): Lua JIT is off by default until the never-run
// lowering/codegen path is green. When off, fall through to the Lua
// interpreter — same behavior as before the loader fix made the JIT
// reachable.
//
if (!mudconf.lua_jit) return false;
// Already tried and failed?
if (entry.jit_eligible) {
// Already have a compiled key? Run it.
if (entry.jit_key != 0) {
// Save Lua stack — ECALL handlers may push tables/functions.
int saved_top = lua_gettop(m_L);
// Stage the same execution context the interpreter leg gets:
// compiled chunks reach the bridge C functions and the per-run
// mux fields through ordinary Lua calls now, and without this
// they saw a cleared registry and the PREVIOUS run's mux table.
lua_exec_ctx jit_ctx;
lua_exec_ctx *prev_ctx =
lua_setup_exec_context(m_L, jit_ctx, executor, caller,
enactor, pArgs, nArgs, true);
MUX_RESULT mr = m_pIJITCompile->RunCompiled(entry.jit_key,
executor, caller, enactor, pArgs, nArgs,
pResult, nResultMax, pnResultLen, m_L);
lua_restore_exec_context(m_L, prev_ctx);
lua_settop(m_L, saved_top); // restore stack
if (MUX_E_NOTFOUND == mr) {
// Program gone from cache (e.g. jitstats flush). Clear the
// latch and compile again below — pre-entry: nothing ran.
//
entry.jit_key = 0;
entry.jit_eligible = false;
} else if (MUX_FAILED(mr)) {
// Pre-entry setup failure from RunCompiled (depth,
// watermarks, oversize carg, get_dbt). Nothing ran;
// interpreter fallback is correct (#1837). Post-entry
// failures return MUX_S_OK with pResult already committed.
//
return false;
} else {
// #1751 Phase 4: handled run — success or committed
// LUA ERROR / POST-ENTRY / CPU LIMITED. Do not re-run.
//
return true;
}
} else {
return false; // Previously failed to compile (pre-entry).
}
}
// First attempt: dump the chunk to bytecode and try JIT compilation.
entry.jit_eligible = true;
// lua_dump expects the function on top of stack. We have it there
// from LoadCached. Push a copy so we don't consume it.
lua_pushvalue(m_L, -1);
dump_buffer buf;
int dump_status = lua_dump(m_L, dump_writer, &buf, 0);
lua_pop(m_L, 1); // pop the copy
if (dump_status != 0 || buf.data.empty()) {
return false;
}
// Try to compile.
uint64_t key = 0;
MUX_RESULT mr = m_pIJITCompile->CompileLuaBytecode(
buf.data.data(), buf.data.size(), &key);
if (MUX_FAILED(mr) || key == 0) {
return false; // JIT doesn't support this bytecode; fall through.
}
entry.jit_key = key;
// Run the compiled program.
// Save/restore Lua stack — ECALL handlers for table ops, getglobal,
// and generic calls push values onto the Lua stack that must be
// cleaned up after JIT execution completes. Execution context staged
// exactly as on the cached-key path above.
int saved_top = lua_gettop(m_L);
lua_exec_ctx jit_ctx;
lua_exec_ctx *prev_ctx =
lua_setup_exec_context(m_L, jit_ctx, executor, caller, enactor,
pArgs, nArgs, true);
mr = m_pIJITCompile->RunCompiled(key, executor, caller, enactor,
pArgs, nArgs, pResult, nResultMax, pnResultLen, m_L);
lua_restore_exec_context(m_L, prev_ctx);
lua_settop(m_L, saved_top); // restore stack
// Post-entry failures return MUX_S_OK with a committed error string.
// Pre-entry setup failure (MUX_E_FAIL, empty result) falls through to
// the interpreter — nothing ran (#1837). Keep the compiled key so a
// later invocation with short cargs can still take the compiled path.
//
if (MUX_FAILED(mr)) {
return false;
}
return true;
}
// =========================================================================
// mux_ILuaControl implementation.
// =========================================================================
MUX_RESULT CLuaMod::CallAttr(dbref executor, dbref caller, dbref enactor,
dbref obj, const UTF8 *pAttrName,
const UTF8 *pArgs[], int nArgs,
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen)
{
if (nullptr == m_L)
{
return MUX_E_FAIL;
}
// Read the attribute via COM interface (permission-checked).
//
if (nullptr == m_pIAttributeAccess)
{
return MUX_E_FAIL;
}
UTF8 source[8000];
size_t nSource = 0;
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(executor, obj,
pAttrName, source, sizeof(source), &nSource);
if (MUX_FAILED(mr))
{
size_t n = mux_snprintf(pResult, nResultMax,
T("#-1 LUA ERROR: cannot read attribute"));
*pnResultLen = n;
return MUX_S_OK;
}
if (nSource == 0)
{
pResult[0] = '\0';
*pnResultLen = 0;
return MUX_S_OK;
}
m_stats.calls++;
// Compile (or load from cache).
//
char chunkname[128];
mux_snprintf(reinterpret_cast<UTF8 *>(chunkname), sizeof(chunkname),
T("@#%d/%s"), static_cast<int>(obj), pAttrName);
if (!LoadCached(reinterpret_cast<const char *>(source), nSource,
chunkname))
{
const char *errmsg = lua_tostring(m_L, -1);
if (nullptr == errmsg) errmsg = "compile error";
m_stats.errors++;
size_t n = mux_snprintf(pResult, nResultMax,
T("#-1 LUA ERROR: %s"), errmsg);
*pnResultLen = n;
lua_pop(m_L, 1);
return MUX_S_OK;
}
// Try JIT execution. The compiled chunk is on top of the Lua stack.
// If JIT succeeds, pop the chunk and return.
//
std::string cache_key(reinterpret_cast<const char *>(source), nSource);
auto cache_it = m_cache.find(cache_key);
if (cache_it != m_cache.end())
{
if (TryJIT(cache_it->second, executor, caller, enactor,
pArgs, nArgs, pResult, nResultMax, pnResultLen))
{
lua_pop(m_L, 1); // pop the chunk
return MUX_S_OK;
}
}
// Fall through to Lua VM execution.
//
ExecuteChunk(m_L, executor, caller, enactor, pArgs, nArgs,
pResult, nResultMax, pnResultLen);
return MUX_S_OK;
}
MUX_RESULT CLuaMod::Eval(dbref executor, dbref caller, dbref enactor,
const UTF8 *pSource, size_t nSource,
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen)
{
if (nullptr == m_L)
{
return MUX_E_FAIL;
}
// Wizard-only check.
//
if (nullptr != m_pIPermissions)
{
bool bWizard = false;
m_pIPermissions->IsWizard(executor, &bWizard);
if (!bWizard)
{
size_t n = mux_snprintf(pResult, nResultMax,
T("#-1 LUA ERROR: wizard-only"));
*pnResultLen = n;
return MUX_S_OK;
}
}
m_stats.calls++;
if (!LoadCached(reinterpret_cast<const char *>(pSource), nSource,
"@inline"))
{
const char *errmsg = lua_tostring(m_L, -1);
if (nullptr == errmsg) errmsg = "compile error";
m_stats.errors++;
size_t n = mux_snprintf(pResult, nResultMax,
T("#-1 LUA ERROR: %s"), errmsg);
*pnResultLen = n;
lua_pop(m_L, 1);
return MUX_S_OK;
}
// Try JIT execution.
//
std::string cache_key(reinterpret_cast<const char *>(pSource), nSource);
auto cache_it = m_cache.find(cache_key);
if (cache_it != m_cache.end())
{
if (TryJIT(cache_it->second, executor, caller, enactor,
nullptr, 0, pResult, nResultMax, pnResultLen))
{
lua_pop(m_L, 1); // pop the chunk
return MUX_S_OK;
}
}
// Fall through to Lua VM execution.
//
ExecuteChunk(m_L, executor, caller, enactor, nullptr, 0,
pResult, nResultMax, pnResultLen);
return MUX_S_OK;
}
MUX_RESULT CLuaMod::GetStats(size_t *pnCalls, size_t *pnErrors,
size_t *pnInsnLimitHits, size_t *pnMemLimitHits,
size_t *pnBytesUsed,
size_t *pnCacheHits, size_t *pnCacheMisses,
size_t *pnCacheEntries)
{
*pnCalls = m_stats.calls;
*pnErrors = m_stats.errors;
*pnInsnLimitHits = m_stats.insn_limit_hits;
*pnMemLimitHits = m_stats.mem_limit_hits;
*pnBytesUsed = m_nMemUsed;
*pnCacheHits = m_stats.cache_hits;
*pnCacheMisses = m_stats.cache_misses;
*pnCacheEntries = m_cache.size();
return MUX_S_OK;
}
MUX_RESULT CLuaMod::SetLimits(int nInsnLimit, int nMemLimit)
{
if (nInsnLimit > 0)
{
m_nInsnLimit = nInsnLimit;
}
if (nMemLimit > 0)
{
m_nMemLimit = nMemLimit;
}
return MUX_S_OK;
}
// =========================================================================
// mux_IServerEventsSink stubs.
// =========================================================================
void CLuaMod::startup(void) { }
void CLuaMod::presync_database(void) { }
void CLuaMod::presync_database_sigsegv(void) { }
void CLuaMod::dump_database(int dump_type) { (void)dump_type; }
void CLuaMod::dump_complete_signal(void) { }
void CLuaMod::shutdown(void) { CacheClear(); DestroyLuaState(); }
void CLuaMod::dbck(void) { }
void CLuaMod::connect(dbref player, int isnew, int num) { (void)player; (void)isnew; (void)num; }
void CLuaMod::disconnect(dbref player, int num) { (void)player; (void)num; }
void CLuaMod::data_create(dbref object) { (void)object; }
void CLuaMod::data_clone(dbref clone, dbref source) { (void)clone; (void)source; }
void CLuaMod::data_free(dbref object) { (void)object; }
// =========================================================================
// =========================================================================
// Factory function — called from engine_com.cpp's CLuaModFactory.
// =========================================================================
MUX_RESULT lua_mod_create_instance(MUX_IID iid, void **ppv) {
CLuaMod *pLuaMod = nullptr;
try { pLuaMod = new CLuaMod; } catch (...) { ; }
if (nullptr == pLuaMod) return MUX_E_OUTOFMEMORY;
MUX_RESULT mr = pLuaMod->FinalConstruct();
if (MUX_SUCCEEDED(mr)) {
mr = pLuaMod->QueryInterface(iid, ppv);
}
pLuaMod->Release();
return mr;
}