futriix/tests/modules/helloscripting.c
Ricardo Dias af71619c45
Extract the scripting engine code from the functions unit (#1312)
This commit creates a new compilation unit for the scripting engine code
by extracting the existing code from the functions unit.
We're doing this refactor to prepare the code for running the `EVAL`
command using different scripting engines.

This PR has a module API change: we changed the type of error messages
returned by the callback
`ValkeyModuleScriptingEngineCreateFunctionsLibraryFunc` to be a
`ValkeyModuleString` (aka `robj`);

This PR also fixes #1470.

---------

Signed-off-by: Ricardo Dias <ricardo.dias@percona.com>
2025-01-16 10:08:16 +01:00

407 lines
12 KiB
C

#include "valkeymodule.h"
#include <ctype.h>
#include <errno.h>
#include <string.h>
/*
* 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 only supports 32-bit integer, and it only allows to return an
* integer constant, or return the value passed as the first argument to the
* function.
*
* 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
*
* FUNCTION bar # declaration of 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.
* ```
*/
/*
* List of instructions of the HELLO language.
*/
typedef enum HelloInstKind {
FUNCTION = 0,
CONSTI,
ARGS,
RETURN,
_NUM_INSTRUCTIONS, // Not a real instruction.
} HelloInstKind;
/*
* String representations of the instructions above.
*/
const char *HelloInstKindStr[] = {
"FUNCTION",
"CONSTI",
"ARGS",
"RETURN",
};
/*
* Struct that represents an instance of an instruction.
* Instructions may have at most one parameter.
*/
typedef struct HelloInst {
HelloInstKind kind;
union {
uint32_t integer;
const 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;
} 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;
/*
* Struct that represents the runtime context of an HELLO program.
*/
typedef struct HelloLangCtx {
HelloProgram *program;
} HelloLangCtx;
static HelloLangCtx *hello_ctx = NULL;
static uint32_t str2int(const char *str) {
char *end;
errno = 0;
uint32_t val = (uint32_t)strtoul(str, &end, 10);
ValkeyModule_Assert(errno == 0);
return val;
}
/*
* 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) {
char *token = strtok(NULL, " \n");
ValkeyModule_Assert(token != NULL);
func->name = ValkeyModule_Alloc(sizeof(char) * strlen(token) + 1);
strcpy(func->name, token);
}
/*
* Parses an integer parameter.
*/
static void helloLangParseIntegerParam(HelloFunc *func) {
char *token = strtok(NULL, " \n");
func->instructions[func->num_instructions].param.integer = str2int(token);
}
/*
* Parses the CONSTI instruction parameter.
*/
static void helloLangParseConstI(HelloFunc *func) {
helloLangParseIntegerParam(func);
func->num_instructions++;
}
/*
* Parses the ARGS instruction parameter.
*/
static void helloLangParseArgs(HelloFunc *func) {
helloLangParseIntegerParam(func);
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:
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);
break;
case CONSTI:
ValkeyModule_Assert(currentFunc != NULL);
helloLangParseConstI(currentFunc);
break;
case ARGS:
ValkeyModule_Assert(currentFunc != NULL);
helloLangParseArgs(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;
}
/*
* Executes an HELLO function.
*/
static uint32_t executeHelloLangFunction(HelloFunc *func,
ValkeyModuleString **args, int nargs) {
uint32_t stack[64];
int sp = 0;
for (uint32_t pc = 0; pc < func->num_instructions; pc++) {
HelloInst instr = func->instructions[pc];
switch (instr.kind) {
case CONSTI:
stack[sp++] = instr.param.integer;
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);
uint32_t arg = str2int(argStr);
stack[sp++] = arg;
break;
}
case RETURN: {
ValkeyModule_Assert(sp > 0);
uint32_t val = stack[--sp];
ValkeyModule_Assert(sp == 0);
return val;
}
case FUNCTION:
default:
ValkeyModule_Assert(0);
}
}
ValkeyModule_Assert(0);
return 0;
}
static ValkeyModuleScriptingEngineMemoryInfo engineGetMemoryInfo(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCtx *engine_ctx) {
VALKEYMODULE_NOT_USED(module_ctx);
HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx;
ValkeyModuleScriptingEngineMemoryInfo mem_info = {0};
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,
void *compiled_function) {
VALKEYMODULE_NOT_USED(module_ctx);
HelloFunc *func = (HelloFunc *)compiled_function;
return ValkeyModule_MallocSize(func->name);
}
static void engineFreeFunction(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCtx *engine_ctx,
void *compiled_function) {
VALKEYMODULE_NOT_USED(module_ctx);
VALKEYMODULE_NOT_USED(engine_ctx);
HelloLangCtx *ctx = (HelloLangCtx *)engine_ctx;
HelloFunc *func = (HelloFunc *)compiled_function;
ctx->program->functions[func->index] = NULL;
ValkeyModule_Free(func->name);
func->name = NULL;
ValkeyModule_Free(func);
}
static ValkeyModuleScriptingEngineCompiledFunction **createHelloLangEngine(ValkeyModuleCtx *module_ctx,
ValkeyModuleScriptingEngineCtx *engine_ctx,
const char *code,
size_t timeout,
size_t *out_num_compiled_functions,
ValkeyModuleString **err) {
VALKEYMODULE_NOT_USED(module_ctx);
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) {
.name = ValkeyModule_CreateString(NULL, func->name, strlen(func->name)),
.function = func,
.desc = NULL,
.f_flags = 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,
ValkeyModuleScriptingEngineFunctionCtx *func_ctx,
void *compiled_function,
ValkeyModuleString **keys, size_t nkeys,
ValkeyModuleString **args, size_t nargs) {
VALKEYMODULE_NOT_USED(engine_ctx);
VALKEYMODULE_NOT_USED(func_ctx);
VALKEYMODULE_NOT_USED(keys);
VALKEYMODULE_NOT_USED(nkeys);
HelloFunc *func = (HelloFunc *)compiled_function;
uint32_t result = executeHelloLangFunction(func, args, nargs);
ValkeyModule_ReplyWithLongLong(module_ctx, result);
}
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;
hello_ctx = ValkeyModule_Alloc(sizeof(HelloLangCtx));
hello_ctx->program = NULL;
ValkeyModuleScriptingEngineMethods methods = {
.version = VALKEYMODULE_SCRIPTING_ENGINE_ABI_VERSION,
.create_functions_library = createHelloLangEngine,
.call_function = callHelloLangFunction,
.get_function_memory_overhead = engineFunctionMemoryOverhead,
.free_function = engineFreeFunction,
.get_memory_info = engineGetMemoryInfo,
};
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;
}