#include "valkeymodule.h" #include #include #include #include #include /* * This module implements a very simple stack based scripting language. * It's purpose is only to test the valkey module API to implement scripting * engines. * * The language is called HELLO, and a program in this language is formed by * a list of function definitions. * The language supports 32-bit integer and string values, and allows to return * a single value from a function. * The language also supports error values when returned from the CALL * instruction. * * Example of a program: * * ``` * FUNCTION foo # declaration of function 'foo' * ARGS 0 # pushes the value in the first argument to the top of the * # stack * RETURN # returns the current value on the top of the stack and marks * # the end of the function declaration * * RFUNCTION bar # declaration of read-only function 'bar' * CONSTI 432 # pushes the value 432 to the top of the stack * RETURN # returns the current value on the top of the stack and marks * # the end of the function declaration. * * FUNCTION baz # declaration of function 'baz' * ARGS 0 # pushes the value in the first argument to the top of the * # stack * SLEEP # Pops the current value in the stack and sleeps for `value` * # seconds * CONSTS x # pushes the string 'x' to the top of the stack * CONSTI 0 # pushes the value 0 to the top of the stack * CONSTI 2 # pushes the value 2 to the top of the stack * CALL SET # calls the command specified in the first argument with the * # with arguments pushed to the stack on previous instructions. * # The top of the stack must always have an integer value that * # specifies the number of arguments to pop from the stack. * # In this case, the command called will be `SET x 0` * RETURN # returns the current value on the top of the stack and marks * # the end of the function declaration. * ``` */ /* * List of instructions of the HELLO language. */ typedef enum HelloInstKind { FUNCTION = 0, RFUNCTION, CONSTI, CONSTS, ARGS, SLEEP, CALL, RETURN, _NUM_INSTRUCTIONS, // Not a real instruction. } HelloInstKind; /* * String representations of the instructions above. */ const char *HelloInstKindStr[] = { "FUNCTION", "RFUNCTION", "CONSTI", "CONSTS", "ARGS", "SLEEP", "CALL", "RETURN", }; /* * Value type used in the HELLO language. */ typedef enum { VALUE_INT, VALUE_STRING, VALUE_ERROR, } ValueType; /* * Value used in the HELLO language. */ typedef struct { ValueType type; union { uint32_t integer; const char *string; }; } Value; /* * Struct that represents an instance of an instruction. * Instructions may have at most one parameter. */ typedef struct HelloInst { HelloInstKind kind; union { uint32_t integer; char *string; } param; } HelloInst; /* * Struct that represents an instance of a function. * A function is just a list of instruction instances. */ typedef struct HelloFunc { char *name; HelloInst instructions[256]; uint32_t num_instructions; uint32_t index; int read_only; } HelloFunc; /* * Struct that represents an instance of an HELLO program. * A program is just a list of function instances. */ typedef struct HelloProgram { HelloFunc *functions[16]; uint32_t num_functions; } HelloProgram; typedef struct HelloDebugCtx { int enabled; int stop_on_next_instr; int abort; const Value *stack; uint32_t sp; } HelloDebugCtx; /* * Struct that represents the runtime context of an HELLO program. */ typedef struct HelloLangCtx { HelloProgram *program; HelloDebugCtx debug; } HelloLangCtx; static HelloLangCtx *hello_ctx = NULL; static Value parseValue(const char *str) { char *end; errno = 0; Value result; // Check for NULL or empty string if (!str || *str == '\0') { ValkeyModule_Log(NULL, "error", "Failed to parse argument: NULL or empty string"); ValkeyModule_Assert(0); return result; } unsigned long val = strtoul(str, &end, 10); // Check if no digits were found if (end == str) { result.type = VALUE_STRING; result.string = str; return result; } // Check for trailing characters (non-digits after the number) if (*end != '\0') { result.type = VALUE_STRING; result.string = str; return result; } // Check for overflow if (errno == ERANGE || val > UINT32_MAX) { result.type = VALUE_STRING; result.string = str; return result; } // Success case result.type = VALUE_INT; result.integer = (uint32_t)val; return result; } /* * Parses the kind of instruction that the current token points to. */ static HelloInstKind helloLangParseInstruction(const char *token) { for (HelloInstKind i = 0; i < _NUM_INSTRUCTIONS; i++) { if (strcmp(HelloInstKindStr[i], token) == 0) { return i; } } return _NUM_INSTRUCTIONS; } /* * Parses the function param. */ static void helloLangParseFunction(HelloFunc *func, int read_only) { char *token = strtok(NULL, " \n"); ValkeyModule_Assert(token != NULL); func->name = ValkeyModule_Alloc(sizeof(char) * strlen(token) + 1); strcpy(func->name, token); func->read_only = read_only; } /* * Parses an integer parameter. */ static void helloLangParseIntegerParam(HelloFunc *func) { char *token = strtok(NULL, " \n"); Value parsed = parseValue(token); if (parsed.type == VALUE_INT) { func->instructions[func->num_instructions].param.integer = parsed.integer; } else { ValkeyModule_Log(NULL, "error", "Failed to parse integer parameter: '%s'", token); ValkeyModule_Assert(0); // Parse error } } /* * Parses the CONSTI instruction parameter. */ static void helloLangParseConstI(HelloFunc *func) { helloLangParseIntegerParam(func); func->num_instructions++; } /* * Parses the CONSTS instruction parameter. */ static void helloLangParseConstS(HelloFunc *func) { char *token = strtok(NULL, " \n"); ValkeyModule_Assert(token != NULL); func->instructions[func->num_instructions].param.string = ValkeyModule_Alloc(sizeof(char) * strlen(token) + 1); strcpy(func->instructions[func->num_instructions].param.string, token); func->num_instructions++; } /* * Parses the ARGS instruction parameter. */ static void helloLangParseArgs(HelloFunc *func) { helloLangParseIntegerParam(func); func->num_instructions++; } static void helloLangParseCall(HelloFunc *func) { char *token = strtok(NULL, " \n"); ValkeyModule_Assert(token != NULL); func->instructions[func->num_instructions].param.string = ValkeyModule_Alloc(sizeof(char) * strlen(token) + 1); strcpy(func->instructions[func->num_instructions].param.string, token); func->num_instructions++; } /* * Parses an HELLO program source code. */ static int helloLangParseCode(const char *code, HelloProgram *program, ValkeyModuleString **err) { char *_code = ValkeyModule_Alloc(sizeof(char) * strlen(code) + 1); strcpy(_code, code); HelloFunc *currentFunc = NULL; char *token = strtok(_code, " \n"); while (token != NULL) { HelloInstKind kind = helloLangParseInstruction(token); if (currentFunc != NULL) { currentFunc->instructions[currentFunc->num_instructions].kind = kind; } switch (kind) { case FUNCTION: case RFUNCTION: ValkeyModule_Assert(currentFunc == NULL); currentFunc = ValkeyModule_Alloc(sizeof(HelloFunc)); memset(currentFunc, 0, sizeof(HelloFunc)); currentFunc->index = program->num_functions; program->functions[program->num_functions++] = currentFunc; helloLangParseFunction(currentFunc, kind == RFUNCTION); break; case CONSTI: ValkeyModule_Assert(currentFunc != NULL); helloLangParseConstI(currentFunc); break; case CONSTS: ValkeyModule_Assert(currentFunc != NULL); helloLangParseConstS(currentFunc); break; case ARGS: ValkeyModule_Assert(currentFunc != NULL); helloLangParseArgs(currentFunc); break; case SLEEP: ValkeyModule_Assert(currentFunc != NULL); currentFunc->num_instructions++; break; case CALL: ValkeyModule_Assert(currentFunc != NULL); helloLangParseCall(currentFunc); break; case RETURN: ValkeyModule_Assert(currentFunc != NULL); currentFunc->num_instructions++; currentFunc = NULL; break; default: *err = ValkeyModule_CreateStringPrintf(NULL, "Failed to parse instruction: '%s'", token); ValkeyModule_Free(_code); return -1; } token = strtok(NULL, " \n"); } ValkeyModule_Free(_code); return 0; } static ValkeyModuleScriptingEngineExecutionState executeSleepInst(ValkeyModuleScriptingEngineServerRuntimeCtx *server_ctx, uint32_t seconds) { uint32_t elapsed_milliseconds = 0; ValkeyModuleScriptingEngineExecutionState state = VMSE_STATE_EXECUTING; while(1) { state = ValkeyModule_GetFunctionExecutionState(server_ctx); if (state != VMSE_STATE_EXECUTING) { break; } if (elapsed_milliseconds >= (seconds * 1000)) { break; } usleep(1000); elapsed_milliseconds++; } return state; } static void executeCallInst(ValkeyModuleCtx *module_ctx, const char *cmd_name, Value *stack, int *sp) { ValkeyModule_Assert(stack != NULL); ValkeyModule_Assert(sp != NULL); ValkeyModuleCallReply *rep = NULL; errno = 0; ValkeyModule_Assert(*sp > 0); Value numargs = stack[--(*sp)]; ValkeyModule_Assert(numargs.type == VALUE_INT); ValkeyModule_Assert(numargs.integer <= (uint32_t)(*sp)); if (numargs.integer > 0) { ValkeyModuleString **cmd_args = ValkeyModule_Alloc(sizeof(ValkeyModuleString *) * numargs.integer); int bp = *sp - numargs.integer; for (uint32_t i = 0; i < numargs.integer; i++) { if (stack[bp + i].type == VALUE_INT) { cmd_args[i] = ValkeyModule_CreateStringPrintf(NULL, "%u", stack[bp + i].integer); } else { cmd_args[i] = ValkeyModule_CreateStringPrintf(NULL, "%s", stack[bp + i].string); } (*sp)--; } rep = ValkeyModule_Call(module_ctx, cmd_name, "ESCXv", cmd_args, numargs.integer); for (uint32_t i = 0; i < numargs.integer; i++) { ValkeyModule_FreeString(NULL, cmd_args[i]); } ValkeyModule_Free(cmd_args); } else { rep = ValkeyModule_Call(module_ctx, cmd_name, "ESCX"); } ValkeyModule_Assert(rep != NULL); int type = ValkeyModule_CallReplyType(rep); ValkeyModuleString *response_str = ValkeyModule_CreateStringFromCallReply(rep); const char *resp_cstr = ValkeyModule_StringPtrLen(response_str, NULL); if (type == VALKEYMODULE_REPLY_ERROR) { stack[*sp].type = VALUE_ERROR; stack[(*sp)++].string = resp_cstr; } else if (type == VALKEYMODULE_REPLY_ARRAY_NULL) { stack[(*sp)].type = VALUE_STRING; stack[(*sp)++].string = "(null array)"; } else if (type == VALKEYMODULE_REPLY_NULL) { stack[(*sp)].type = VALUE_STRING; stack[(*sp)++].string = "(null string)"; } else { if (response_str != NULL) { stack[(*sp)++] = parseValue(resp_cstr); } else { stack[(*sp)].type = VALUE_STRING; stack[(*sp)++].string = "OK"; } } ValkeyModule_FreeCallReply(rep); } static void helloDebuggerLogCurrentInstr(uint32_t pc, HelloInst *instr) { ValkeyModuleString *msg = NULL; switch (instr->kind) { case CONSTI: case ARGS: msg = ValkeyModule_CreateStringPrintf(NULL, ">>> %3u: %s %u", pc, HelloInstKindStr[instr->kind], instr->param.integer); break; case CONSTS: case CALL: msg = ValkeyModule_CreateStringPrintf(NULL, ">>> %3u: %s %s", pc, HelloInstKindStr[instr->kind], instr->param.string); break; case SLEEP: case RETURN: msg = ValkeyModule_CreateStringPrintf(NULL, ">>> %3u: %s", pc, HelloInstKindStr[instr->kind]); break; case FUNCTION: case RFUNCTION: case _NUM_INSTRUCTIONS: ValkeyModule_Assert(0); } ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); } static int helloDebuggerInstrHook(uint32_t pc, HelloInst *instr) { helloDebuggerLogCurrentInstr(pc, instr); ValkeyModule_ScriptingEngineDebuggerFlushLogs(); int client_disconnected = 0; ValkeyModuleString *err; ValkeyModule_ScriptingEngineDebuggerProcessCommands(&client_disconnected, &err); if (err) { ValkeyModule_ScriptingEngineDebuggerLog(err, 0); goto error; } else if (client_disconnected) { ValkeyModuleString *msg = ValkeyModule_CreateStringPrintf(NULL, "ERROR: Client socket disconnected"); ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); goto error; } return 1; error: ValkeyModule_ScriptingEngineDebuggerFlushLogs(); return 0; } typedef enum { FINISHED, KILLED, ABORTED, } HelloExecutionState; /* * Executes an HELLO function. */ static HelloExecutionState executeHelloLangFunction(ValkeyModuleCtx *module_ctx, ValkeyModuleScriptingEngineServerRuntimeCtx *server_ctx, HelloDebugCtx *debug_ctx, HelloFunc *func, ValkeyModuleString **args, int nargs, Value *result) { ValkeyModule_Assert(result != NULL); Value stack[64]; int sp = 0; for (uint32_t pc = 0; pc < func->num_instructions; pc++) { HelloInst instr = func->instructions[pc]; if (debug_ctx->enabled && debug_ctx->stop_on_next_instr) { debug_ctx->stack = stack; debug_ctx->sp = sp; if (!helloDebuggerInstrHook(pc, &instr)) { return ABORTED; } if (debug_ctx->abort) { return ABORTED; } } switch (instr.kind) { case CONSTI: { stack[sp].type = VALUE_INT; stack[sp++].integer = instr.param.integer; break; } case CONSTS: { stack[sp].type = VALUE_STRING; stack[sp++].string = instr.param.string; break; } case ARGS: { uint32_t idx = instr.param.integer; ValkeyModule_Assert(idx < (uint32_t)nargs); size_t len; const char *argStr = ValkeyModule_StringPtrLen(args[idx], &len); stack[sp++] = parseValue(argStr); break; } case SLEEP: { ValkeyModule_Assert(sp > 0); Value sleepVal = stack[--sp]; ValkeyModule_Assert(sleepVal.type == VALUE_INT); if (executeSleepInst(server_ctx, sleepVal.integer) == VMSE_STATE_KILLED) { return KILLED; } break; } case CALL: { const char *cmd_name = instr.param.string; executeCallInst(module_ctx, cmd_name, stack, &sp); break; } case RETURN: { ValkeyModule_Assert(sp > 0); Value returnVal = stack[--sp]; ValkeyModule_Assert(sp == 0); *result = returnVal; return FINISHED; } case FUNCTION: case RFUNCTION: case _NUM_INSTRUCTIONS: ValkeyModule_Assert(0); } } ValkeyModule_Assert(0); return ABORTED; } static ValkeyModuleScriptingEngineMemoryInfo engineGetMemoryInfo(ValkeyModuleCtx *module_ctx, ValkeyModuleScriptingEngineCtx *engine_ctx, ValkeyModuleScriptingEngineSubsystemType type) { VALKEYMODULE_NOT_USED(module_ctx); VALKEYMODULE_NOT_USED(type); HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; ValkeyModuleScriptingEngineMemoryInfo mem_info = { .version = VALKEYMODULE_SCRIPTING_ENGINE_ABI_MEMORY_INFO_VERSION }; if (ctx->program != NULL) { mem_info.used_memory += ValkeyModule_MallocSize(ctx->program); for (uint32_t i = 0; i < ctx->program->num_functions; i++) { HelloFunc *func = ctx->program->functions[i]; if (func != NULL) { mem_info.used_memory += ValkeyModule_MallocSize(func); mem_info.used_memory += ValkeyModule_MallocSize(func->name); } } } mem_info.engine_memory_overhead = ValkeyModule_MallocSize(ctx); if (ctx->program != NULL) { mem_info.engine_memory_overhead += ValkeyModule_MallocSize(ctx->program); } return mem_info; } static size_t engineFunctionMemoryOverhead(ValkeyModuleCtx *module_ctx, ValkeyModuleScriptingEngineCompiledFunction *compiled_function) { VALKEYMODULE_NOT_USED(module_ctx); HelloFunc *func = (HelloFunc *)compiled_function->function; return ValkeyModule_MallocSize(func->name); } static void engineFreeFunction(ValkeyModuleCtx *module_ctx, ValkeyModuleScriptingEngineCtx *engine_ctx, ValkeyModuleScriptingEngineSubsystemType type, ValkeyModuleScriptingEngineCompiledFunction *compiled_function) { VALKEYMODULE_NOT_USED(module_ctx); VALKEYMODULE_NOT_USED(type); HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; HelloFunc *func = (HelloFunc *)compiled_function->function; for (uint32_t i = 0; i < func->num_instructions; i++) { HelloInst instr = func->instructions[i]; if (instr.kind == CONSTS || instr.kind == CALL) { ValkeyModule_Free(instr.param.string); } } ctx->program->functions[func->index] = NULL; ValkeyModule_Free(func->name); func->name = NULL; ValkeyModule_Free(func); ValkeyModule_Free(compiled_function->name); ValkeyModule_Free(compiled_function); } static ValkeyModuleScriptingEngineCompiledFunction **createHelloLangEngine(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) { VALKEYMODULE_NOT_USED(module_ctx); VALKEYMODULE_NOT_USED(code_len); VALKEYMODULE_NOT_USED(type); VALKEYMODULE_NOT_USED(timeout); VALKEYMODULE_NOT_USED(err); HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; if (ctx->program == NULL) { ctx->program = ValkeyModule_Alloc(sizeof(HelloProgram)); memset(ctx->program, 0, sizeof(HelloProgram)); } else { ctx->program->num_functions = 0; } int ret = helloLangParseCode(code, ctx->program, err); if (ret < 0) { for (uint32_t i = 0; i < ctx->program->num_functions; i++) { HelloFunc *func = ctx->program->functions[i]; ValkeyModule_Free(func->name); ValkeyModule_Free(func); ctx->program->functions[i] = NULL; } ctx->program->num_functions = 0; return NULL; } ValkeyModuleScriptingEngineCompiledFunction **compiled_functions = ValkeyModule_Alloc(sizeof(ValkeyModuleScriptingEngineCompiledFunction *) * ctx->program->num_functions); for (uint32_t i = 0; i < ctx->program->num_functions; i++) { HelloFunc *func = ctx->program->functions[i]; ValkeyModuleScriptingEngineCompiledFunction *cfunc = ValkeyModule_Alloc(sizeof(ValkeyModuleScriptingEngineCompiledFunction)); *cfunc = (ValkeyModuleScriptingEngineCompiledFunction) { .version = VALKEYMODULE_SCRIPTING_ENGINE_ABI_COMPILED_FUNCTION_VERSION, .name = ValkeyModule_CreateString(NULL, func->name, strlen(func->name)), .function = func, .desc = NULL, .f_flags = func->read_only ? VMSE_SCRIPT_FLAG_NO_WRITES : 0, }; compiled_functions[i] = cfunc; } *out_num_compiled_functions = ctx->program->num_functions; return compiled_functions; } static void callHelloLangFunction(ValkeyModuleCtx *module_ctx, ValkeyModuleScriptingEngineCtx *engine_ctx, ValkeyModuleScriptingEngineServerRuntimeCtx *server_ctx, ValkeyModuleScriptingEngineCompiledFunction *compiled_function, ValkeyModuleScriptingEngineSubsystemType type, ValkeyModuleString **keys, size_t nkeys, ValkeyModuleString **args, size_t nargs) { VALKEYMODULE_NOT_USED(keys); VALKEYMODULE_NOT_USED(nkeys); ValkeyModule_Assert(type == VMSE_EVAL || type == VMSE_FUNCTION); ValkeyModule_AutoMemory(module_ctx); HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; HelloFunc *func = (HelloFunc *)compiled_function->function; Value result; HelloExecutionState state = executeHelloLangFunction( module_ctx, server_ctx, &ctx->debug, func, args, nargs, &result); ValkeyModule_Assert(state == KILLED || state == FINISHED || state == ABORTED); if (state == KILLED) { if (type == VMSE_EVAL) { ValkeyModule_ReplyWithCustomErrorFormat(module_ctx, 1, "ERR Script killed by user with SCRIPT KILL."); return; } if (type == VMSE_FUNCTION) { ValkeyModule_ReplyWithCustomErrorFormat(module_ctx, 1, "ERR Script killed by user with FUNCTION KILL"); return; } } else if (state == ABORTED) { ValkeyModule_ReplyWithCustomErrorFormat(module_ctx, 1, "ERR execution aborted during debugging session"); } else { if (result.type == VALUE_INT) { ValkeyModule_Log(module_ctx, "info", "Function '%s' executed, returning %u", func->name, result.integer); ValkeyModule_ReplyWithLongLong(module_ctx, result.integer); } else if (result.type == VALUE_STRING) { ValkeyModule_Log(module_ctx, "info", "Function '%s' executed, returning string '%s'", func->name, result.string); ValkeyModule_ReplyWithCString(module_ctx, result.string); } else { ValkeyModule_Assert(result.type == VALUE_ERROR); ValkeyModule_Log(module_ctx, "info", "Function '%s' executed, returning error '%s'", func->name, result.string); ValkeyModule_ReplyWithCustomErrorFormat(module_ctx, 0, "%s",result.string); } } } static ValkeyModuleScriptingEngineCallableLazyEnvReset *helloResetEvalEnv(ValkeyModuleCtx *module_ctx, ValkeyModuleScriptingEngineCtx *engine_ctx, int async) { VALKEYMODULE_NOT_USED(module_ctx); VALKEYMODULE_NOT_USED(engine_ctx); VALKEYMODULE_NOT_USED(async); return NULL; } static ValkeyModuleScriptingEngineCallableLazyEnvReset *helloResetEnv(ValkeyModuleCtx *module_ctx, ValkeyModuleScriptingEngineCtx *engine_ctx, ValkeyModuleScriptingEngineSubsystemType type, int async) { VALKEYMODULE_NOT_USED(module_ctx); VALKEYMODULE_NOT_USED(engine_ctx); VALKEYMODULE_NOT_USED(type); VALKEYMODULE_NOT_USED(async); return NULL; } static int helloDebuggerStepCommand(ValkeyModuleString **argv, size_t argc, void *context) { VALKEYMODULE_NOT_USED(argv); VALKEYMODULE_NOT_USED(argc); HelloDebugCtx *ctx = context; ctx->stop_on_next_instr = 1; return 0; } static int helloDebuggerContinueCommand(ValkeyModuleString **argv, size_t argc, void *context) { VALKEYMODULE_NOT_USED(argv); VALKEYMODULE_NOT_USED(argc); HelloDebugCtx *ctx = context; ctx->stop_on_next_instr = 0; return 0; } static int helloDebuggerStackCommand(ValkeyModuleString **argv, size_t argc, void *context) { HelloDebugCtx *ctx = context; ValkeyModuleString *msg = NULL; if (argc > 1) { long long n; ValkeyModule_StringToLongLong(argv[1], &n); uint32_t index = (uint32_t)n; if (index >= ctx->sp) { ValkeyModuleString *msg = ValkeyModule_CreateStringPrintf(NULL, "Index out of range. Current stack size: %u", ctx->sp); ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); } else { Value stackVal = ctx->stack[ctx->sp - index - 1]; if (stackVal.type == VALUE_INT) { msg = ValkeyModule_CreateStringPrintf(NULL, "[%u] %d", index, stackVal.integer); } else { msg = ValkeyModule_CreateStringPrintf(NULL, "[%u] \"%s\"", index, stackVal.string); } ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); } } else { msg = ValkeyModule_CreateStringPrintf(NULL, "Stack contents:"); if (ctx->sp == 0) { msg = ValkeyModule_CreateStringPrintf(NULL, "[empty]"); ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); } else { ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); for (uint32_t i=0; i < ctx->sp; i++) { Value stackVal = ctx->stack[ctx->sp - i - 1]; if (stackVal.type == VALUE_INT) { if (i == 0) { msg = ValkeyModule_CreateStringPrintf(NULL, "top -> [%u] %d", i, stackVal.integer); } else { msg = ValkeyModule_CreateStringPrintf(NULL, " [%u] %d", i, stackVal.integer); } } else { if (i == 0) { msg = ValkeyModule_CreateStringPrintf(NULL, "top -> [%u] \"%s\"", i, stackVal.string); } else { msg = ValkeyModule_CreateStringPrintf(NULL, " [%u] \"%s\"", i, stackVal.string); } } ValkeyModule_ScriptingEngineDebuggerLog(msg, 0); } } } ValkeyModule_ScriptingEngineDebuggerFlushLogs(); return 1; } static int helloDebuggerAbortCommand(ValkeyModuleString **argv, size_t argc, void *context) { VALKEYMODULE_NOT_USED(argv); VALKEYMODULE_NOT_USED(argc); HelloDebugCtx *ctx = context; ctx->abort = 1; return 0; } #define COMMAND_COUNT (4) static ValkeyModuleScriptingEngineDebuggerCommandParam stack_params[1] = { { .name = "index", .optional = 1 } }; static ValkeyModuleScriptingEngineDebuggerCommand helloDebuggerCommands[COMMAND_COUNT] = { VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("step", 1, NULL, 0, "Execute current instruction.", 0 ,helloDebuggerStepCommand), VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("continue", 1, NULL, 0, "Continue normal execution.", 0, helloDebuggerContinueCommand), VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("stack", 2, stack_params, 1, "Print stack contents. If index is specified, print only the value at index. Indexes start at 0 (top = 0).", 0, helloDebuggerStackCommand), VALKEYMODULE_SCRIPTING_ENGINE_DEBUGGER_COMMAND("abort", 1, NULL, 0, "Abort execution.", 0, helloDebuggerAbortCommand), }; static ValkeyModuleScriptingEngineDebuggerEnableRet helloDebuggerEnable(ValkeyModuleCtx *module_ctx, ValkeyModuleScriptingEngineCtx *engine_ctx, ValkeyModuleScriptingEngineSubsystemType type, const ValkeyModuleScriptingEngineDebuggerCommand **commands, size_t *commands_len) { VALKEYMODULE_NOT_USED(module_ctx); VALKEYMODULE_NOT_USED(type); HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; ctx->debug = (HelloDebugCtx) {.enabled = 1}; *commands = helloDebuggerCommands; *commands_len = COMMAND_COUNT; for (int i=0; i < COMMAND_COUNT; i++) { helloDebuggerCommands[i].context = &ctx->debug; } return VMSE_DEBUG_ENABLED; } static void helloDebuggerDisable(ValkeyModuleCtx *module_ctx, ValkeyModuleScriptingEngineCtx *engine_ctx, ValkeyModuleScriptingEngineSubsystemType type) { VALKEYMODULE_NOT_USED(module_ctx); VALKEYMODULE_NOT_USED(type); HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; ctx->debug = (HelloDebugCtx){0}; } static void helloDebuggerStart(ValkeyModuleCtx *module_ctx, ValkeyModuleScriptingEngineCtx *engine_ctx, ValkeyModuleScriptingEngineSubsystemType type, ValkeyModuleString *code) { VALKEYMODULE_NOT_USED(module_ctx); VALKEYMODULE_NOT_USED(type); VALKEYMODULE_NOT_USED(code); HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; ctx->debug.stop_on_next_instr = 1; } static void helloDebuggerEnd(ValkeyModuleCtx *module_ctx, ValkeyModuleScriptingEngineCtx *engine_ctx, ValkeyModuleScriptingEngineSubsystemType type) { VALKEYMODULE_NOT_USED(module_ctx); VALKEYMODULE_NOT_USED(type); HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx; ctx->debug.stop_on_next_instr = 0; ctx->debug.abort = 0; ctx->debug.stack = NULL; ctx->debug.sp = 0; } int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { VALKEYMODULE_NOT_USED(argv); VALKEYMODULE_NOT_USED(argc); if (ValkeyModule_Init(ctx, "helloengine", 1, VALKEYMODULE_APIVER_1) == VALKEYMODULE_ERR) { return VALKEYMODULE_ERR; } unsigned long long abi_version = VALKEYMODULE_SCRIPTING_ENGINE_ABI_VERSION; if (argc > 0) { if (ValkeyModule_StringToULongLong(argv[0], &abi_version) == VALKEYMODULE_ERR) { const char *arg_str = ValkeyModule_StringPtrLen(argv[0], NULL); ValkeyModule_Log(ctx, "error", "Invalid ABI version: %s", arg_str); return VALKEYMODULE_ERR; } else { const char *arg_str = ValkeyModule_StringPtrLen(argv[0], NULL); ValkeyModule_Log(ctx, "info", "initializing Hello scripting engine with ABI version: %s", arg_str); } } hello_ctx = ValkeyModule_Alloc(sizeof(HelloLangCtx)); hello_ctx->program = NULL; hello_ctx->debug.enabled = 0; ValkeyModuleScriptingEngineMethodsV3 methodsV3; ValkeyModuleScriptingEngineMethodsV4 methodsV4; if (abi_version <= 2) { methodsV3 = (ValkeyModuleScriptingEngineMethodsV3) { .version = abi_version, .compile_code = createHelloLangEngine, .free_function = engineFreeFunction, .call_function = callHelloLangFunction, .get_function_memory_overhead = engineFunctionMemoryOverhead, .reset_eval_env_v2 = helloResetEvalEnv, .get_memory_info = engineGetMemoryInfo, }; } else if (abi_version <= 3) { methodsV3 = (ValkeyModuleScriptingEngineMethodsV3) { .version = abi_version, .compile_code = createHelloLangEngine, .free_function = engineFreeFunction, .call_function = callHelloLangFunction, .get_function_memory_overhead = engineFunctionMemoryOverhead, .reset_env = helloResetEnv, .get_memory_info = engineGetMemoryInfo, }; } else { methodsV4 = (ValkeyModuleScriptingEngineMethodsV4) { .version = abi_version, .compile_code = createHelloLangEngine, .free_function = engineFreeFunction, .call_function = callHelloLangFunction, .get_function_memory_overhead = engineFunctionMemoryOverhead, .reset_env = helloResetEnv, .get_memory_info = engineGetMemoryInfo, .debugger_enable = helloDebuggerEnable, .debugger_disable = helloDebuggerDisable, .debugger_start = helloDebuggerStart, .debugger_end = helloDebuggerEnd, }; } ValkeyModuleScriptingEngineMethods *methods = abi_version <= 3 ? (ValkeyModuleScriptingEngineMethods *)&methodsV3 : (ValkeyModuleScriptingEngineMethods *)&methodsV4; ValkeyModule_RegisterScriptingEngine(ctx, "HELLO", hello_ctx, methods); return VALKEYMODULE_OK; } int ValkeyModule_OnUnload(ValkeyModuleCtx *ctx) { if (ValkeyModule_UnregisterScriptingEngine(ctx, "HELLO") != VALKEYMODULE_OK) { ValkeyModule_Log(ctx, "error", "Failed to unregister engine"); return VALKEYMODULE_ERR; } ValkeyModule_Free(hello_ctx->program); hello_ctx->program = NULL; ValkeyModule_Free(hello_ctx); hello_ctx = NULL; return VALKEYMODULE_OK; }