Files
valkey/src/modules/lua/engine_lua.c
T
eifrah-aws 0327c27131 Add Static Module Support (#3392)
Add a build option to compile the Lua scripting engine as a static
module and wire the server to load it directly at startup when enabled.
The module load path now resolves on-load and on-unload entry points
from the main binary, and the module lifecycle keeps those callbacks so
unload works without a shared library handle.

The Lua module build was updated to support both static and shared
variants, with the static path exporting visible wrapper symbols and
linking the server with the module archive. While touching the Lua code,
a few internal symbols were renamed for consistency and the monotonic
time helper was clarified.

Note that this PR addresses the LUA module, but it can be applied to
other "core" modules (like: Bloom, Json, Search and others). With this
change, it will be easier to ship Valkey bundle with modules.

Areas touched:

* CMake
* Makefile
* Lua scripting module
* Core module loading

**Generated by CodeLite**

---------

Signed-off-by: Eran Ifrah <eifrah@amazon.com>
2026-04-20 14:45:57 +03:00

572 lines
22 KiB
C

/*
* Copyright (c) Valkey Contributors
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "../../valkeymodule.h"
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <string.h>
#if defined(__GLIBC__) && !defined(USE_LIBC)
#include <malloc.h>
#endif
#include <errno.h>
#include "engine_structs.h"
#include "function_lua.h"
#include "script_lua.h"
#include "debug_lua.h"
#define LUA_ENGINE_NAME "LUA"
#define REGISTRY_ERROR_HANDLER_NAME "__ERROR_HANDLER__"
/* Adds server.debug() function used by lua debugger
*
* Log a string message into the output console.
* Can take multiple arguments that will be separated by commas.
* Nothing is returned to the caller. */
static int luaServerDebugCommand(lua_State *lua) {
if (!ldbIsActive()) return 0;
int argc = lua_gettop(lua);
ValkeyModuleString *log = ValkeyModule_CreateStringPrintf(NULL, "<debug> line %d: ", ldbGetCurrentLine());
while (argc--) {
log = ldbCatStackValue(log, lua, -1 - argc);
if (argc != 0) {
ValkeyModule_StringAppendBuffer(NULL, log, ", ", 2);
}
}
ldbLog(log);
return 0;
}
/* Adds server.breakpoint() function used by lua debugger.
*
* Allows to stop execution during a debugging session from within
* the Lua code implementation, like if a breakpoint was set in the code
* immediately after the function. */
static int luaServerBreakpointCommand(lua_State *lua) {
if (ldbIsActive()) {
ldbSetBreakpointOnNextLine(1);
lua_pushboolean(lua, 1);
} else {
lua_pushboolean(lua, 0);
}
return 1;
}
/* Adds server.replicate_commands()
*
* DEPRECATED: Now do nothing and always return true.
* Turn on single commands replication if the script never called
* a write command so far, and returns true. Otherwise if the script
* already started to write, returns false and stick to whole scripts
* replication, which is our default. */
int luaServerReplicateCommandsCommand(lua_State *lua) {
lua_pushboolean(lua, 1);
return 1;
}
static void luaStateInstallErrorHandler(lua_State *lua) {
/* Add a helper function we use for pcall error reporting.
* Note that when the error is in the C function we want to report the
* information about the caller, that's what makes sense from the point
* of view of the user debugging a script. */
lua_pushstring(lua, REGISTRY_ERROR_HANDLER_NAME);
char *errh_func = "local dbg = debug\n"
"debug = nil\n"
"local error_handler = function (err)\n"
" local i = dbg.getinfo(2,'nSl')\n"
" if i and i.what == 'C' then\n"
" i = dbg.getinfo(3,'nSl')\n"
" end\n"
" if type(err) ~= 'table' then\n"
" err = {err='ERR ' .. tostring(err)}"
" end"
" if i then\n"
" err['source'] = i.source\n"
" err['line'] = i.currentline\n"
" end"
" return err\n"
"end\n"
"return error_handler";
luaL_loadbuffer(lua, errh_func, strlen(errh_func), "@err_handler_def");
lua_pcall(lua, 0, 1, 0);
lua_settable(lua, LUA_REGISTRYINDEX);
}
static void luaStateLockGlobalTable(lua_State *lua) {
/* Lock the global table from any changes */
lua_pushvalue(lua, LUA_GLOBALSINDEX);
luaSetErrorMetatable(lua);
/* Recursively lock all tables that can be reached from the global table */
luaSetTableProtectionRecursively(lua);
lua_pop(lua, 1);
/* Set metatables of basic types (string, number, nil etc.) readonly. */
luaSetTableProtectionForBasicTypes(lua);
}
static void initializeEvalLuaState(lua_State *lua) {
/* register debug commands. we only need to add it under 'server' as 'redis'
* is effectively aliased to 'server' table at this point. */
lua_getglobal(lua, "server");
/* server.breakpoint */
lua_pushstring(lua, "breakpoint");
lua_pushcfunction(lua, luaServerBreakpointCommand);
lua_settable(lua, -3);
/* server.debug */
lua_pushstring(lua, "debug");
lua_pushcfunction(lua, luaServerDebugCommand);
lua_settable(lua, -3);
/* server.replicate_commands */
lua_pushstring(lua, "replicate_commands");
lua_pushcfunction(lua, luaServerReplicateCommandsCommand);
lua_settable(lua, -3);
lua_setglobal(lua, "server");
/* Duplicate the function with __server__err__handler and
* __redis__err_handler name for backwards compatibility. */
lua_pushstring(lua, REGISTRY_ERROR_HANDLER_NAME);
lua_gettable(lua, LUA_REGISTRYINDEX);
lua_setglobal(lua, "__server__err__handler");
lua_getglobal(lua, "__server__err__handler");
lua_setglobal(lua, "__redis__err__handler");
}
static uint32_t parse_semver(const char *version) {
unsigned int major = 0, minor = 0, patch = 0;
sscanf(version, "%u.%u.%u", &major, &minor, &patch);
return ((major & 0xFF) << 16) | ((minor & 0xFF) << 8) | (patch & 0xFF);
}
static void get_version_info(ValkeyModuleCtx *ctx,
char **redis_version,
uint32_t *redis_version_num,
char **server_name,
char **valkey_version,
uint32_t *valkey_version_num) {
ValkeyModuleServerInfoData *info = ValkeyModule_GetServerInfo(ctx, "server");
ValkeyModule_Assert(info != NULL);
const char *rv = ValkeyModule_ServerInfoGetFieldC(info, "redis_version");
*redis_version = lm_strcpy(rv);
*redis_version_num = parse_semver(*redis_version);
const char *sn = ValkeyModule_ServerInfoGetFieldC(info, "server_name");
*server_name = lm_strcpy(sn);
const char *vv = ValkeyModule_ServerInfoGetFieldC(info, "valkey_version");
*valkey_version = lm_strcpy(vv);
*valkey_version_num = parse_semver(*valkey_version);
ValkeyModule_FreeServerInfo(ctx, info);
}
static void initializeLuaState(luaEngineCtx *lua_engine_ctx,
ValkeyModuleScriptingEngineSubsystemType type) {
lua_State *lua = lua_open();
if (type == VMSE_EVAL) {
lua_engine_ctx->eval_lua = lua;
} else {
ValkeyModule_Assert(type == VMSE_FUNCTION);
lua_engine_ctx->function_lua = lua;
}
luaRegisterServerAPI(lua_engine_ctx, lua);
luaStateInstallErrorHandler(lua);
if (type == VMSE_EVAL) {
initializeEvalLuaState(lua);
luaStateLockGlobalTable(lua);
} else {
luaStateLockGlobalTable(lua);
luaFunctionInitializeLuaState(lua_engine_ctx, lua);
}
}
static struct luaEngineCtx *createEngineContext(ValkeyModuleCtx *ctx) {
luaEngineCtx *lua_engine_ctx = ValkeyModule_Alloc(sizeof(*lua_engine_ctx));
get_version_info(ctx,
&lua_engine_ctx->redis_version,
&lua_engine_ctx->redis_version_num,
&lua_engine_ctx->server_name,
&lua_engine_ctx->valkey_version,
&lua_engine_ctx->valkey_version_num);
lua_engine_ctx->lua_enable_insecure_api = 0;
initializeLuaState(lua_engine_ctx, VMSE_EVAL);
initializeLuaState(lua_engine_ctx, VMSE_FUNCTION);
return lua_engine_ctx;
}
static void destroyEngineContext(luaEngineCtx *lua_engine_ctx) {
lua_close(lua_engine_ctx->eval_lua);
lua_close(lua_engine_ctx->function_lua);
ValkeyModule_Free(lua_engine_ctx->redis_version);
ValkeyModule_Free(lua_engine_ctx->server_name);
ValkeyModule_Free(lua_engine_ctx->valkey_version);
ValkeyModule_Free(lua_engine_ctx);
}
static ValkeyModuleScriptingEngineMemoryInfo luaEngineGetMemoryInfo(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCtx *engine_ctx,
ValkeyModuleScriptingEngineSubsystemType type) {
VALKEYMODULE_NOT_USED(module_ctx);
luaEngineCtx *lua_engine_ctx = engine_ctx;
ValkeyModuleScriptingEngineMemoryInfo mem_info = {0};
if (type == VMSE_EVAL || type == VMSE_ALL) {
mem_info.used_memory += luaMemory(lua_engine_ctx->eval_lua);
}
if (type == VMSE_FUNCTION || type == VMSE_ALL) {
mem_info.used_memory += luaMemory(lua_engine_ctx->function_lua);
}
mem_info.engine_memory_overhead = ValkeyModule_MallocSize(engine_ctx);
return mem_info;
}
static ValkeyModuleScriptingEngineCompiledFunction **luaEngineCompileCode(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCtx *engine_ctx,
ValkeyModuleScriptingEngineSubsystemType type,
const char *code,
size_t code_len,
size_t timeout,
size_t *out_num_compiled_functions,
ValkeyModuleString **err) {
luaEngineCtx *lua_engine_ctx = (luaEngineCtx *)engine_ctx;
ValkeyModuleScriptingEngineCompiledFunction **functions = NULL;
if (type == VMSE_EVAL) {
lua_State *lua = lua_engine_ctx->eval_lua;
if (luaL_loadbuffer(
lua, code, code_len, "@user_script")) {
*err = ValkeyModule_CreateStringPrintf(module_ctx, "Error compiling script (new function): %s", lua_tostring(lua, -1));
lua_pop(lua, 1);
return functions;
}
ValkeyModule_Assert(lua_isfunction(lua, -1));
int function_ref = luaL_ref(lua, LUA_REGISTRYINDEX);
luaFunction *script = ValkeyModule_Calloc(1, sizeof(luaFunction));
*script = (luaFunction){
.lua = lua,
.function_ref = function_ref,
};
ValkeyModuleScriptingEngineCompiledFunction *func = ValkeyModule_Alloc(sizeof(*func));
*func = (ValkeyModuleScriptingEngineCompiledFunction){
.name = NULL,
.function = script,
.desc = NULL,
.f_flags = 0};
*out_num_compiled_functions = 1;
functions = ValkeyModule_Calloc(1, sizeof(ValkeyModuleScriptingEngineCompiledFunction *));
*functions = func;
} else {
functions = luaFunctionLibraryCreate(lua_engine_ctx->function_lua,
code,
timeout,
out_num_compiled_functions,
err);
}
return functions;
}
static void luaEngineFunctionCall(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCtx *engine_ctx,
ValkeyModuleScriptingEngineServerRuntimeCtx *server_ctx,
ValkeyModuleScriptingEngineCompiledFunction *compiled_function,
ValkeyModuleScriptingEngineSubsystemType type,
ValkeyModuleString **keys,
size_t nkeys,
ValkeyModuleString **args,
size_t nargs) {
luaEngineCtx *lua_engine_ctx = (luaEngineCtx *)engine_ctx;
lua_State *lua = type == VMSE_EVAL ? lua_engine_ctx->eval_lua : lua_engine_ctx->function_lua;
luaFunction *script = compiled_function->function;
int lua_function_ref = script->function_ref;
/* Push the pcall error handler function on the stack. */
lua_pushstring(lua, REGISTRY_ERROR_HANDLER_NAME);
lua_gettable(lua, LUA_REGISTRYINDEX);
lua_rawgeti(lua, LUA_REGISTRYINDEX, lua_function_ref);
ValkeyModule_Assert(!lua_isnil(lua, -1));
luaCallFunction(module_ctx,
server_ctx,
type,
lua,
keys,
nkeys,
args,
nargs,
type == VMSE_EVAL ? ldbIsActive() : 0,
lua_engine_ctx->lua_enable_insecure_api);
lua_pop(lua, 1); /* Remove the error handler. */
}
static void resetLuaContext(void *context) {
lua_State *lua = context;
lua_gc(lua, LUA_GCCOLLECT, 0);
lua_close(lua);
#if defined(__GLIBC__) && !defined(USE_LIBC)
/* The lua interpreter may hold a lot of memory internally, and lua is
* using libc. libc may take a bit longer to return the memory to the OS,
* so after lua_close, we call malloc_trim try to purge it earlier.
*
* We do that only when the server itself does not use libc. When Lua and the server
* use different allocators, one won't use the fragmentation holes of the
* other, and released memory can take a long time until it is returned to
* the OS. */
malloc_trim(0);
#endif
}
static int isLuaInsecureAPIEnabled(ValkeyModuleCtx *module_ctx) {
int result = 0;
ValkeyModuleCallReply *reply = ValkeyModule_Call(module_ctx, "CONFIG", "ccE", "GET", "lua-enable-insecure-api");
if (ValkeyModule_CallReplyType(reply) == VALKEYMODULE_REPLY_ERROR) {
ValkeyModule_Log(module_ctx,
"warning",
"Unable to determine 'lua-enable-insecure-api' configuration value: %s",
ValkeyModule_CallReplyStringPtr(reply, NULL));
ValkeyModule_FreeCallReply(reply);
return 0;
}
ValkeyModule_Assert(ValkeyModule_CallReplyType(reply) == VALKEYMODULE_REPLY_ARRAY &&
ValkeyModule_CallReplyLength(reply) == 2);
ValkeyModuleCallReply *val = ValkeyModule_CallReplyArrayElement(reply, 1);
ValkeyModule_Assert(ValkeyModule_CallReplyType(val) == VALKEYMODULE_REPLY_STRING);
const char *val_str = ValkeyModule_CallReplyStringPtr(val, NULL);
result = strncmp(val_str, "yes", 3) == 0;
ValkeyModule_FreeCallReply(reply);
return result;
}
static ValkeyModuleScriptingEngineCallableLazyEnvReset *luaEngineResetEnv(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCtx *engine_ctx,
ValkeyModuleScriptingEngineSubsystemType type,
int async) {
VALKEYMODULE_NOT_USED(module_ctx);
luaEngineCtx *lua_engine_ctx = (luaEngineCtx *)engine_ctx;
ValkeyModule_Assert(type == VMSE_EVAL || type == VMSE_FUNCTION);
lua_State *lua = type == VMSE_EVAL ? lua_engine_ctx->eval_lua : lua_engine_ctx->function_lua;
ValkeyModule_Assert(lua);
ValkeyModuleScriptingEngineCallableLazyEnvReset *callback = NULL;
if (async) {
callback = ValkeyModule_Calloc(1, sizeof(*callback));
*callback = (ValkeyModuleScriptingEngineCallableLazyEnvReset){
.context = lua,
.engineLazyEnvResetCallback = resetLuaContext,
};
} else {
resetLuaContext(lua);
}
lua_engine_ctx->lua_enable_insecure_api = isLuaInsecureAPIEnabled(module_ctx);
initializeLuaState(lua_engine_ctx, type);
return callback;
}
static size_t luaEngineFunctionMemoryOverhead(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCompiledFunction *compiled_function) {
VALKEYMODULE_NOT_USED(module_ctx);
return ValkeyModule_MallocSize(compiled_function->function) +
(compiled_function->name ? ValkeyModule_MallocSize(compiled_function->name) : 0) +
(compiled_function->desc ? ValkeyModule_MallocSize(compiled_function->desc) : 0) +
ValkeyModule_MallocSize(compiled_function);
}
static void luaEngineFreeFunction(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCtx *engine_ctx,
ValkeyModuleScriptingEngineSubsystemType type,
ValkeyModuleScriptingEngineCompiledFunction *compiled_function) {
VALKEYMODULE_NOT_USED(module_ctx);
ValkeyModule_Assert(type == VMSE_EVAL || type == VMSE_FUNCTION);
luaEngineCtx *lua_engine_ctx = engine_ctx;
lua_State *lua = type == VMSE_EVAL ? lua_engine_ctx->eval_lua : lua_engine_ctx->function_lua;
ValkeyModule_Assert(lua);
luaFunction *script = (luaFunction *)compiled_function->function;
if (lua == script->lua) {
/* The lua context is still the same, which means that we're not
* resetting the whole eval context, and therefore, we need to
* delete the function from the lua context.
*/
lua_unref(lua, script->function_ref);
}
ValkeyModule_Free(script);
if (compiled_function->name) {
ValkeyModule_Free(compiled_function->name);
}
if (compiled_function->desc) {
ValkeyModule_Free(compiled_function->desc);
}
ValkeyModule_Free(compiled_function);
}
static ValkeyModuleScriptingEngineDebuggerEnableRet luaEngineDebuggerEnable(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCtx *engine_ctx,
ValkeyModuleScriptingEngineSubsystemType type,
const ValkeyModuleScriptingEngineDebuggerCommand **commands,
size_t *commands_len) {
VALKEYMODULE_NOT_USED(module_ctx);
if (type != VMSE_EVAL) {
return VMSE_DEBUG_NOT_SUPPORTED;
}
ldbEnable();
luaEngineCtx *lua_engine_ctx = engine_ctx;
ldbGenerateDebuggerCommandsArray(lua_engine_ctx->eval_lua,
commands,
commands_len);
return VMSE_DEBUG_ENABLED;
}
static void luaEngineDebuggerDisable(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCtx *engine_ctx,
ValkeyModuleScriptingEngineSubsystemType type) {
VALKEYMODULE_NOT_USED(module_ctx);
VALKEYMODULE_NOT_USED(engine_ctx);
VALKEYMODULE_NOT_USED(type);
ldbDisable();
}
static void luaEngineDebuggerStart(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCtx *engine_ctx,
ValkeyModuleScriptingEngineSubsystemType type,
ValkeyModuleString *source) {
VALKEYMODULE_NOT_USED(module_ctx);
VALKEYMODULE_NOT_USED(engine_ctx);
VALKEYMODULE_NOT_USED(type);
ldbStart(source);
}
static void luaEngineDebuggerEnd(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCtx *engine_ctx,
ValkeyModuleScriptingEngineSubsystemType type) {
VALKEYMODULE_NOT_USED(module_ctx);
VALKEYMODULE_NOT_USED(engine_ctx);
VALKEYMODULE_NOT_USED(type);
ldbEnd();
}
static struct luaEngineCtx *engine_ctx = NULL;
#if STATIC_LUA
/*
* When building Lua as a static library, hide the generic module entry
* points, ValkeyModule_OnLoad and ValkeyModule_OnLoad, to avoid multiple
* symbol definitions. This is done by declaring these functions as static.
*/
#define LUA_MODULE_VISIBILITY static
#else
#define LUA_MODULE_VISIBILITY
#endif
LUA_MODULE_VISIBILITY int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx,
ValkeyModuleString **argv,
int argc) {
VALKEYMODULE_NOT_USED(argv);
VALKEYMODULE_NOT_USED(argc);
if (ValkeyModule_Init(ctx, "lua", 1, VALKEYMODULE_APIVER_1) == VALKEYMODULE_ERR) {
return VALKEYMODULE_ERR;
}
ValkeyModule_SetModuleOptions(ctx, VALKEYMODULE_OPTIONS_HANDLE_REPL_ASYNC_LOAD |
VALKEYMODULE_OPTIONS_HANDLE_ATOMIC_SLOT_MIGRATION);
engine_ctx = createEngineContext(ctx);
if (ValkeyModule_LoadConfigs(ctx) == VALKEYMODULE_ERR) {
ValkeyModule_Log(ctx, "warning", "Failed to load LUA module configs");
destroyEngineContext(engine_ctx);
engine_ctx = NULL;
return VALKEYMODULE_ERR;
}
ValkeyModuleScriptingEngineMethods methods = {
.version = VALKEYMODULE_SCRIPTING_ENGINE_ABI_VERSION,
.compile_code = luaEngineCompileCode,
.free_function = luaEngineFreeFunction,
.call_function = luaEngineFunctionCall,
.get_function_memory_overhead = luaEngineFunctionMemoryOverhead,
.reset_env = luaEngineResetEnv,
.get_memory_info = luaEngineGetMemoryInfo,
.debugger_enable = luaEngineDebuggerEnable,
.debugger_disable = luaEngineDebuggerDisable,
.debugger_start = luaEngineDebuggerStart,
.debugger_end = luaEngineDebuggerEnd,
};
int result = ValkeyModule_RegisterScriptingEngine(ctx,
LUA_ENGINE_NAME,
engine_ctx,
&methods);
if (result == VALKEYMODULE_ERR) {
ValkeyModule_Log(ctx, "warning", "Failed to register LUA scripting engine");
destroyEngineContext(engine_ctx);
engine_ctx = NULL;
return VALKEYMODULE_ERR;
}
engine_ctx->lua_enable_insecure_api = isLuaInsecureAPIEnabled(ctx);
return VALKEYMODULE_OK;
}
LUA_MODULE_VISIBILITY int ValkeyModule_OnUnload(ValkeyModuleCtx *ctx) {
if (ValkeyModule_UnregisterScriptingEngine(ctx, LUA_ENGINE_NAME) != VALKEYMODULE_OK) {
ValkeyModule_Log(ctx, "error", "Failed to unregister engine");
return VALKEYMODULE_ERR;
}
destroyEngineContext(engine_ctx);
engine_ctx = NULL;
return VALKEYMODULE_OK;
}
#if STATIC_LUA
/* Unique entry points (Load and Unload) used by the Lua module when linked statically */
int ValkeyModule_OnLoad_lua(ValkeyModuleCtx *ctx,
ValkeyModuleString **argv,
int argc) {
return ValkeyModule_OnLoad(ctx, argv, argc);
}
int ValkeyModule_OnUnload_lua(ValkeyModuleCtx *ctx) {
return ValkeyModule_OnUnload(ctx);
}
#endif