Added a Redis/Valkey extension named redis. (0.42.0)
This commit is contained in:
parent
06072576fc
commit
cbb81c0b93
28 changed files with 798 additions and 15 deletions
|
|
@ -186,6 +186,11 @@ typedef enum {
|
|||
OP_SQLITE_EXEC, // pops sql, handle; pushes sqlite rc (0=OK)
|
||||
OP_SQLITE_QUERY, // pops sql, handle; pushes array<map>
|
||||
|
||||
// Redis (optional, hiredis)
|
||||
OP_REDIS_CONNECT, // pops port:int, host:string; pushes handle (>0) or 0
|
||||
OP_REDIS_CMD, // pops cmd:string, handle:int; pushes reply (string/int/array/nil/map)
|
||||
OP_REDIS_CLOSE, // pops handle:int; pushes Nil
|
||||
|
||||
// PCSC (smart card) opcodes
|
||||
OP_PCSC_ESTABLISH, // returns context id (>0) or 0
|
||||
OP_PCSC_RELEASE, // pops ctx id; returns 1/0
|
||||
|
|
|
|||
221
src/extensions/redis.c
Normal file
221
src/extensions/redis.c
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* This file is part of the Fun programming language.
|
||||
* https://fun-lang.xyz/
|
||||
*
|
||||
* Copyright 2026 Johannes Findeisen <you@hanez.org>
|
||||
* Licensed under the terms of the Apache-2.0 license.
|
||||
* https://opensource.org/license/apache-2-0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file redis.c
|
||||
* @brief Hiredis handle registry and reply mapping helpers for the Fun VM.
|
||||
*
|
||||
* This translation unit provides two small building blocks used by the Redis
|
||||
* opcodes (implemented under src/vm/redis/*.c) and included from src/vm.c:
|
||||
*
|
||||
* 1) A process-local registry that assigns monotonically increasing positive
|
||||
* integer identifiers to hiredis connection pointers (redisContext*).
|
||||
* VM opcodes pass integer ids on the stack instead of raw pointers, keeping
|
||||
* the bytecode portable and preventing accidental misuse of pointers.
|
||||
*
|
||||
* 2) Utilities to convert hiredis reply objects (redisReply) into Fun VM
|
||||
* Value instances, recursively mapping arrays and supporting basic numeric
|
||||
* and string types.
|
||||
*
|
||||
* Build-time feature flag
|
||||
* -----------------------
|
||||
* The code is compiled only when the CMake option FUN_WITH_REDIS is enabled
|
||||
* (i.e., the preprocessor symbol FUN_WITH_REDIS is defined). When disabled,
|
||||
* this file contributes no symbols and the corresponding opcodes are compiled
|
||||
* into stubs that return neutral values.
|
||||
*
|
||||
* Ownership and lifetime
|
||||
* ----------------------
|
||||
* - The registry does NOT open or close Redis connections by itself; it merely
|
||||
* stores pointers created elsewhere (e.g., via redisConnectWithTimeout()).
|
||||
* - Adding an entry does not transfer ownership of the redisContext. Callers
|
||||
* remain responsible for invoking redisFree() at the appropriate time.
|
||||
* - Removing an entry from the registry does NOT free the connection; it only
|
||||
* forgets the mapping between id and pointer. The connect/close opcodes take
|
||||
* care of proper ownership transitions.
|
||||
* - Integer identifiers are monotonically increasing per process. Once a
|
||||
* handle id is deleted, it will not be reused within the same process
|
||||
* lifetime.
|
||||
*
|
||||
* Error handling
|
||||
* --------------
|
||||
* Functions here perform basic validation/allocations only. Allocation
|
||||
* failures return NULL (for lookups/additions) or are silently ignored (for
|
||||
* deletions of non-existent ids). No hiredis API calls are made here, so no
|
||||
* hiredis error codes are produced by this module itself.
|
||||
*
|
||||
* Thread-safety
|
||||
* -------------
|
||||
* The registry is a simple singly-linked list with no synchronization. It is
|
||||
* NOT thread-safe. If the VM uses Redis from multiple threads, the caller must
|
||||
* provide external synchronization around calls to these helpers.
|
||||
*
|
||||
* Example
|
||||
* -------
|
||||
* @code{.c}
|
||||
* // Open a hiredis connection elsewhere:
|
||||
* struct timeval tv = { .tv_sec = 2, .tv_usec = 0 };
|
||||
* redisContext *ctx = redisConnectWithTimeout("127.0.0.1", 6379, tv);
|
||||
* if (ctx && !ctx->err) {
|
||||
* // Register and get an id:
|
||||
* RedisHandle *h = redis_reg_add(ctx);
|
||||
* int id = h ? h->id : -1;
|
||||
*
|
||||
* // Later look it up:
|
||||
* RedisHandle *same = redis_reg_get(id);
|
||||
* if (same) {
|
||||
* // use same->ctx with hiredis APIs
|
||||
* }
|
||||
*
|
||||
* // When finished, drop the registry entry and free manually:
|
||||
* redis_reg_del(id);
|
||||
* redisFree(ctx);
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
#ifdef FUN_WITH_REDIS
|
||||
#include <hiredis/hiredis.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Forward declarations from the VM (available in the same TU via includes) */
|
||||
static Value hiredis_reply_to_value(const redisReply *r);
|
||||
|
||||
/**
|
||||
* @brief Node in a singly-linked list of registered Redis handles.
|
||||
*
|
||||
* Associates a monotonically increasing positive integer identifier with a raw
|
||||
* hiredis connection pointer. The list head is stored in a file-static global
|
||||
* (g_redis_handles).
|
||||
*/
|
||||
typedef struct RedisHandle {
|
||||
int id; /**< Positive identifier assigned by the registry. */
|
||||
redisContext *ctx; /**< Opaque pointer to a hiredis connection. */
|
||||
struct RedisHandle *next;/**< Next entry in the singly-linked list. */
|
||||
} RedisHandle;
|
||||
|
||||
/** @brief Global head of the Redis handle list (NULL denotes empty list). */
|
||||
static RedisHandle *g_redis_handles = NULL;
|
||||
/** @brief Next positive identifier to assign to a newly added handle. */
|
||||
static int g_redis_next_id = 1;
|
||||
|
||||
/**
|
||||
* @brief Add a hiredis connection handle to the registry.
|
||||
*
|
||||
* Allocates a new list node, assigns a fresh positive id, and prepends it to
|
||||
* the internal registry list. Ownership of the redisContext remains with the
|
||||
* caller; this registry does not free it during deletion.
|
||||
*
|
||||
* @param ctx Valid pointer to an opened hiredis connection.
|
||||
* @return Pointer to the newly created RedisHandle on success; NULL on
|
||||
* allocation failure. The returned pointer remains owned by the
|
||||
* registry; do not free it directly.
|
||||
*/
|
||||
static RedisHandle *redis_reg_add(redisContext *ctx) {
|
||||
RedisHandle *h = (RedisHandle *)calloc(1, sizeof(RedisHandle));
|
||||
if (!h) return NULL;
|
||||
h->id = g_redis_next_id++;
|
||||
h->ctx = ctx;
|
||||
h->next = g_redis_handles;
|
||||
g_redis_handles = h;
|
||||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Look up a registered Redis handle by id.
|
||||
*
|
||||
* Performs a linear search over the internal list to find a matching id.
|
||||
*
|
||||
* @param id Positive identifier previously returned by redis_reg_add().
|
||||
* @return Pointer to the RedisHandle entry if found; NULL otherwise.
|
||||
*
|
||||
* @note The returned pointer is owned by the registry and must not be freed by
|
||||
* the caller.
|
||||
*/
|
||||
static RedisHandle *redis_reg_get(int id) {
|
||||
for (RedisHandle *p = g_redis_handles; p; p = p->next)
|
||||
if (p->id == id) return p;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Remove a Redis handle entry from the registry.
|
||||
*
|
||||
* Deletes the list node associated with the given id.
|
||||
*
|
||||
* @param id Positive identifier of the entry to remove.
|
||||
*
|
||||
* @note This function does not free the underlying redisContext; the caller is
|
||||
* responsible for calling redisFree() if appropriate.
|
||||
* @note If the id does not exist, the function is a no-op.
|
||||
*/
|
||||
static void redis_reg_del(int id) {
|
||||
RedisHandle **pp = &g_redis_handles;
|
||||
while (*pp) {
|
||||
if ((*pp)->id == id) {
|
||||
RedisHandle *d = *pp;
|
||||
*pp = d->next;
|
||||
free(d);
|
||||
return;
|
||||
}
|
||||
pp = &(*pp)->next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert a hiredis reply to a Fun Value.
|
||||
*
|
||||
* Recursively maps hiredis reply types to the closest Fun representation:
|
||||
* - REDIS_REPLY_STRING / STATUS -> string
|
||||
* - REDIS_REPLY_INTEGER -> int
|
||||
* - REDIS_REPLY_NIL -> nil
|
||||
* - REDIS_REPLY_ARRAY -> array of recursively converted values
|
||||
* - REDIS_REPLY_DOUBLE (if available) -> float
|
||||
* - REDIS_REPLY_ERROR / default -> string (error text or "ERR")
|
||||
*
|
||||
* @param r Non-owning pointer to a redisReply.
|
||||
* @return Value converted from the reply. For NULL replies, returns nil.
|
||||
*/
|
||||
static Value hiredis_reply_to_value(const redisReply *r) {
|
||||
if (!r) return make_nil();
|
||||
switch (r->type) {
|
||||
case REDIS_REPLY_STRING:
|
||||
case REDIS_REPLY_STATUS:
|
||||
return make_string(r->str ? r->str : "");
|
||||
case REDIS_REPLY_INTEGER:
|
||||
return make_int((int64_t)r->integer);
|
||||
case REDIS_REPLY_NIL:
|
||||
return make_nil();
|
||||
case REDIS_REPLY_ARRAY: {
|
||||
int n = (int)r->elements;
|
||||
if (n <= 0) {
|
||||
return make_array_from_values(NULL, 0);
|
||||
}
|
||||
Value *items = (Value *)calloc((size_t)n, sizeof(Value));
|
||||
if (!items) return make_array_from_values(NULL, 0);
|
||||
for (int i = 0; i < n; i++) {
|
||||
items[i] = hiredis_reply_to_value(r->element[i]);
|
||||
}
|
||||
Value arr = make_array_from_values(items, n);
|
||||
for (int i = 0; i < n; i++) free_value(items[i]);
|
||||
free(items);
|
||||
return arr;
|
||||
}
|
||||
#ifdef REDIS_REPLY_DOUBLE
|
||||
case REDIS_REPLY_DOUBLE:
|
||||
return make_float(r->dval);
|
||||
#endif
|
||||
case REDIS_REPLY_ERROR:
|
||||
default:
|
||||
return make_string(r->str ? r->str : "ERR");
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* FUN_WITH_REDIS */
|
||||
69
src/parser.c
69
src/parser.c
|
|
@ -2070,6 +2070,75 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos)
|
|||
free(name);
|
||||
return 1;
|
||||
}
|
||||
/* Redis builtins (hiredis) */
|
||||
if (strcmp(name, "redis_connect") == 0) {
|
||||
(*pos)++; /* '(' */
|
||||
if (!emit_expression(bc, src, len, pos)) {
|
||||
parser_fail(*pos, "redis_connect expects (host, port)");
|
||||
free(name);
|
||||
return 0;
|
||||
}
|
||||
if (!consume_char(src, len, pos, ',')) {
|
||||
parser_fail(*pos, "redis_connect expects (host, port)");
|
||||
free(name);
|
||||
return 0;
|
||||
}
|
||||
if (!emit_expression(bc, src, len, pos)) {
|
||||
parser_fail(*pos, "redis_connect expects (host, port)");
|
||||
free(name);
|
||||
return 0;
|
||||
}
|
||||
if (!consume_char(src, len, pos, ')')) {
|
||||
parser_fail(*pos, "Expected ')' after redis_connect args");
|
||||
free(name);
|
||||
return 0;
|
||||
}
|
||||
bytecode_add_instruction(bc, OP_REDIS_CONNECT, 0);
|
||||
free(name);
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(name, "redis_cmd") == 0) {
|
||||
(*pos)++; /* '(' */
|
||||
if (!emit_expression(bc, src, len, pos)) {
|
||||
parser_fail(*pos, "redis_cmd expects (handle, cmd)");
|
||||
free(name);
|
||||
return 0;
|
||||
}
|
||||
if (!consume_char(src, len, pos, ',')) {
|
||||
parser_fail(*pos, "redis_cmd expects (handle, cmd)");
|
||||
free(name);
|
||||
return 0;
|
||||
}
|
||||
if (!emit_expression(bc, src, len, pos)) {
|
||||
parser_fail(*pos, "redis_cmd expects (handle, cmd)");
|
||||
free(name);
|
||||
return 0;
|
||||
}
|
||||
if (!consume_char(src, len, pos, ')')) {
|
||||
parser_fail(*pos, "Expected ')' after redis_cmd args");
|
||||
free(name);
|
||||
return 0;
|
||||
}
|
||||
bytecode_add_instruction(bc, OP_REDIS_CMD, 0);
|
||||
free(name);
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(name, "redis_close") == 0) {
|
||||
(*pos)++; /* '(' */
|
||||
if (!emit_expression(bc, src, len, pos)) {
|
||||
parser_fail(*pos, "redis_close expects (handle)");
|
||||
free(name);
|
||||
return 0;
|
||||
}
|
||||
if (!consume_char(src, len, pos, ')')) {
|
||||
parser_fail(*pos, "Expected ')' after redis_close arg");
|
||||
free(name);
|
||||
return 0;
|
||||
}
|
||||
bytecode_add_instruction(bc, OP_REDIS_CLOSE, 0);
|
||||
free(name);
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(name, "sqlite_close") == 0) {
|
||||
(*pos)++; /* '(' */
|
||||
if (!emit_expression(bc, src, len, pos)) {
|
||||
|
|
|
|||
8
src/vm.c
8
src/vm.c
|
|
@ -73,6 +73,7 @@
|
|||
#include "extensions/sqlite.c"
|
||||
#include "extensions/xml2.c"
|
||||
#include "extensions/kcgi.c"
|
||||
#include "extensions/redis.c"
|
||||
|
||||
/* forward declarations for include mapping used in error reporting */
|
||||
extern char *preprocess_includes(const char *src);
|
||||
|
|
@ -1230,6 +1231,13 @@ void vm_run(VM *vm, Bytecode *entry) {
|
|||
#include "vm/sqlite/query.c"
|
||||
#endif
|
||||
|
||||
/* Redis ops */
|
||||
#ifdef FUN_WITH_REDIS
|
||||
#include "vm/redis/connect.c"
|
||||
#include "vm/redis/cmd.c"
|
||||
#include "vm/redis/close.c"
|
||||
#endif
|
||||
|
||||
/* C++ demo opcodes (guarded) */
|
||||
#ifdef FUN_WITH_CPP
|
||||
case OP_CPP_ADD: {
|
||||
|
|
|
|||
2
src/vm.h
2
src/vm.h
|
|
@ -66,6 +66,8 @@ static const char *opcode_names[] = {
|
|||
"JSON_PARSE", "JSON_STRINGIFY", "JSON_FROM_FILE", "JSON_TO_FILE",
|
||||
"CURL_GET", "CURL_POST", "CURL_DOWNLOAD",
|
||||
"SQLITE_OPEN", "SQLITE_CLOSE", "SQLITE_EXEC", "SQLITE_QUERY",
|
||||
/* Redis (hiredis) */
|
||||
"REDIS_CONNECT", "REDIS_CMD", "REDIS_CLOSE",
|
||||
"LIBSQL_OPEN", "LIBSQL_CLOSE", "LIBSQL_EXEC", "LIBSQL_QUERY",
|
||||
"PCSC_ESTABLISH", "PCSC_RELEASE", "PCSC_LIST_READERS", "PCSC_CONNECT", "PCSC_DISCONNECT", "PCSC_TRANSMIT",
|
||||
"PCRE2_TEST", "PCRE2_MATCH", "PCRE2_FINDALL",
|
||||
|
|
|
|||
45
src/vm/redis/close.c
Normal file
45
src/vm/redis/close.c
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* This file is part of the Fun programming language.
|
||||
* https://fun-lang.xyz/
|
||||
*
|
||||
* Copyright 2025 Johannes Findeisen <you@hanez.org>
|
||||
* Licensed under the terms of the Apache-2.0 license.
|
||||
* https://opensource.org/license/apache-2-0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file close.c
|
||||
* @brief Implements the OP_REDIS_CLOSE opcode (conditional build).
|
||||
*
|
||||
* Closes a previously opened Redis connection and removes its registry entry.
|
||||
*
|
||||
* Stack effect
|
||||
* ------------
|
||||
* OP_REDIS_CLOSE: (handle:int) -> Nil
|
||||
*
|
||||
* Behavior
|
||||
* --------
|
||||
* - When FUN_WITH_REDIS is enabled, looks up the handle, calls redisFree() on
|
||||
* the underlying connection if present, clears the pointer, and removes the
|
||||
* registry entry. Always pushes Nil.
|
||||
* - When FUN_WITH_REDIS is disabled, pops the argument and pushes Nil.
|
||||
*/
|
||||
|
||||
case OP_REDIS_CLOSE: {
|
||||
#ifdef FUN_WITH_REDIS
|
||||
Value vh = pop_value(vm);
|
||||
int hid = (int)vh.i;
|
||||
free_value(vh);
|
||||
RedisHandle *h = redis_reg_get(hid);
|
||||
if (h && h->ctx) {
|
||||
redisFree(h->ctx);
|
||||
h->ctx = NULL;
|
||||
}
|
||||
redis_reg_del(hid);
|
||||
push_value(vm, make_nil());
|
||||
#else
|
||||
Value v1 = pop_value(vm); free_value(v1);
|
||||
push_value(vm, make_nil());
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
58
src/vm/redis/cmd.c
Normal file
58
src/vm/redis/cmd.c
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* This file is part of the Fun programming language.
|
||||
* https://fun-lang.xyz/
|
||||
*
|
||||
* Copyright 2025 Johannes Findeisen <you@hanez.org>
|
||||
* Licensed under the terms of the Apache-2.0 license.
|
||||
* https://opensource.org/license/apache-2-0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file cmd.c
|
||||
* @brief Implements the OP_REDIS_CMD opcode (conditional build).
|
||||
*
|
||||
* Executes a Redis command using the synchronous hiredis API on a previously
|
||||
* opened handle and converts the reply to a Fun Value.
|
||||
*
|
||||
* Stack effect
|
||||
* ------------
|
||||
* OP_REDIS_CMD: (handle:int, cmd:string) -> reply:Value
|
||||
*
|
||||
* Behavior
|
||||
* --------
|
||||
* - The command string uses Redis inline protocol formatting (e.g.,
|
||||
* "PING", "SET key val", "LRANGE list 0 -1").
|
||||
* - When FUN_WITH_REDIS is enabled, the opcode looks up the connection by
|
||||
* handle id, issues redisCommand(), and converts the resulting reply to a
|
||||
* Fun Value:
|
||||
* - status/string -> string
|
||||
* - integer -> int
|
||||
* - nil -> nil
|
||||
* - array -> array of converted elements
|
||||
* On errors or lookup failures, pushes Nil.
|
||||
* - When FUN_WITH_REDIS is disabled, pops arguments and pushes Nil.
|
||||
*/
|
||||
|
||||
case OP_REDIS_CMD: {
|
||||
#ifdef FUN_WITH_REDIS
|
||||
Value vcmd = pop_value(vm);
|
||||
Value vh = pop_value(vm);
|
||||
int hid = (int)vh.i;
|
||||
char *cmd = value_to_string_alloc(&vcmd);
|
||||
free_value(vh);
|
||||
free_value(vcmd);
|
||||
RedisHandle *h = redis_reg_get(hid);
|
||||
if (!h || !h->ctx || !cmd) { if (cmd) free(cmd); push_value(vm, make_nil()); break; }
|
||||
redisReply *r = (redisReply *)redisCommand(h->ctx, cmd);
|
||||
free(cmd);
|
||||
if (!r) { push_value(vm, make_nil()); break; }
|
||||
Value out = hiredis_reply_to_value(r);
|
||||
freeReplyObject(r);
|
||||
push_value(vm, out);
|
||||
#else
|
||||
Value v1 = pop_value(vm); free_value(v1);
|
||||
Value v2 = pop_value(vm); free_value(v2);
|
||||
push_value(vm, make_nil());
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
65
src/vm/redis/connect.c
Normal file
65
src/vm/redis/connect.c
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* This file is part of the Fun programming language.
|
||||
* https://fun-lang.xyz/
|
||||
*
|
||||
* Copyright 2025 Johannes Findeisen <you@hanez.org>
|
||||
* Licensed under the terms of the Apache-2.0 license.
|
||||
* https://opensource.org/license/apache-2-0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file connect.c
|
||||
* @brief Implements the OP_REDIS_CONNECT opcode (conditional build).
|
||||
*
|
||||
* Establishes a synchronous TCP connection to a Redis-compatible server using
|
||||
* hiredis and registers the connection handle in the internal registry.
|
||||
*
|
||||
* Stack effect
|
||||
* ------------
|
||||
* OP_REDIS_CONNECT: (host:string, port:int) -> handle:int (>0) or 0 on error
|
||||
*
|
||||
* Behavior
|
||||
* --------
|
||||
* - When FUN_WITH_REDIS is enabled, attempts a blocking connect with a
|
||||
* 2-second timeout via redisConnectWithTimeout(). On success, the created
|
||||
* redisContext* is stored in the registry and the assigned positive handle
|
||||
* id is pushed. On failure, 0 is pushed.
|
||||
* - When FUN_WITH_REDIS is disabled, arguments are popped and 0 is pushed.
|
||||
*
|
||||
* Notes
|
||||
* -----
|
||||
* - The returned handle must be closed with OP_REDIS_CLOSE to release
|
||||
* resources and delete the registry entry.
|
||||
*/
|
||||
|
||||
case OP_REDIS_CONNECT: {
|
||||
#ifdef FUN_WITH_REDIS
|
||||
Value vport = pop_value(vm);
|
||||
Value vhost = pop_value(vm);
|
||||
int port = (int)vport.i;
|
||||
char *host = value_to_string_alloc(&vhost);
|
||||
free_value(vport);
|
||||
free_value(vhost);
|
||||
if (!host) { push_value(vm, make_int(0)); break; }
|
||||
struct timeval tv; tv.tv_sec = 2; tv.tv_usec = 0;
|
||||
redisContext *ctx = redisConnectWithTimeout(host, port, tv);
|
||||
free(host);
|
||||
if (!ctx || ctx->err) {
|
||||
if (ctx) redisFree(ctx);
|
||||
push_value(vm, make_int(0));
|
||||
break;
|
||||
}
|
||||
RedisHandle *h = redis_reg_add(ctx);
|
||||
if (!h) {
|
||||
redisFree(ctx);
|
||||
push_value(vm, make_int(0));
|
||||
break;
|
||||
}
|
||||
push_value(vm, make_int(h->id));
|
||||
#else
|
||||
Value v1 = pop_value(vm); free_value(v1);
|
||||
Value v2 = pop_value(vm); free_value(v2);
|
||||
push_value(vm, make_int(0));
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue