// calogKv.c -- calog key-value library (see calogKv.h). A process-wide, mutex-guarded array // of {key bytes, key length, value}. kvSet/kvGet/kvHas/kvDelete/kvKeys are natives over that // shared store; every stored value is a deep copy, so the store owns pure data. // // The registry state is STATIC (not heap) and its bookkeeping is never freed: the five // natives stay reachable (via any context) right up until calogDestroy tears the broker // registry down, so freeing the state in calogKvShutdown -- which the contract requires // BEFORE calogDestroy -- would leave those natives dereferencing freed memory. Shutdown // instead just frees the stored keys + values and empties the array; the small static // bookkeeping persists for the process (no per-cycle leak), and a post-shutdown native call // simply sees an empty store. // // Function values are refused (calogErrUnsupportedE): storing a CalogFnT would drag script // callable lifecycle into a data store, so kv sidesteps it entirely and holds data only. #define _POSIX_C_SOURCE 200809L #include "calogKv.h" #include #include #include #include #define KV_INITIAL_CAP 8 #define KV_GROWTH 2 typedef struct KvEntryT { char *keyBytes; int64_t keyLen; CalogValueT value; } KvEntryT; // The store (heap array), guarded by gMapMutex. refCount (guarded by gInitMutex) counts // registered runtimes so the LAST calogKvShutdown frees the store's contents. static pthread_mutex_t gMapMutex = PTHREAD_MUTEX_INITIALIZER; static KvEntryT *gEntries = NULL; static int32_t gCount = 0; static int32_t gCap = 0; static pthread_mutex_t gInitMutex = PTHREAD_MUTEX_INITIALIZER; static int32_t gRefCount = 0; static bool kvContainsFn(const CalogValueT *value, int32_t depth); static int32_t kvDelete(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t kvFindLocked(const char *bytes, int64_t length); static int32_t kvGet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t kvHas(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t kvKeys(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t kvSet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); int32_t calogKvRegister(CalogT *calog) { pthread_mutex_lock(&gInitMutex); gRefCount++; pthread_mutex_unlock(&gInitMutex); calogRegisterInline(calog, "kvSet", kvSet, NULL); calogRegisterInline(calog, "kvGet", kvGet, NULL); calogRegisterInline(calog, "kvHas", kvHas, NULL); calogRegisterInline(calog, "kvDelete", kvDelete, NULL); calogRegisterInline(calog, "kvKeys", kvKeys, NULL); return calogOkE; } void calogKvShutdown(void) { pthread_mutex_lock(&gInitMutex); if (gRefCount > 0) { gRefCount--; } if (gRefCount <= 0) { int32_t index; pthread_mutex_lock(&gMapMutex); for (index = 0; index < gCount; index++) { free(gEntries[index].keyBytes); calogValueFree(&gEntries[index].value); } free(gEntries); gEntries = NULL; gCount = 0; gCap = 0; pthread_mutex_unlock(&gMapMutex); } pthread_mutex_unlock(&gInitMutex); } static bool kvContainsFn(const CalogValueT *value, int32_t depth) { int64_t index; // kv stores only data (copied by value); a function value -- at the top level OR nested in // an aggregate -- would retain a CalogFnT into the process-wide store past its owner's life, // so reject any value that carries one. Too-deep is rejected as unverifiable. if (depth >= CALOG_MAX_DEPTH) { return true; } if (value->type == calogFnE) { return true; } if (value->type == calogAggE) { for (index = 0; index < value->as.agg->arrayCount; index++) { if (kvContainsFn(&value->as.agg->array[index], depth + 1)) { return true; } } for (index = 0; index < value->as.agg->pairCount; index++) { if (kvContainsFn(&value->as.agg->pairs[index].key, depth + 1)) { return true; } if (kvContainsFn(&value->as.agg->pairs[index].value, depth + 1)) { return true; } } } return false; } static int32_t kvDelete(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { int32_t index; (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "kvDelete expects (key)"); } pthread_mutex_lock(&gMapMutex); index = kvFindLocked(args[0].as.s.bytes, args[0].as.s.length); if (index >= 0) { free(gEntries[index].keyBytes); calogValueFree(&gEntries[index].value); gEntries[index] = gEntries[gCount - 1]; gCount--; } pthread_mutex_unlock(&gMapMutex); return calogOkE; } static int32_t kvFindLocked(const char *bytes, int64_t length) { int32_t index; for (index = 0; index < gCount; index++) { if (gEntries[index].keyLen == length && memcmp(gEntries[index].keyBytes, bytes, (size_t)length) == 0) { return index; } } return -1; } static int32_t kvGet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { int32_t index; int32_t status; (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "kvGet expects (key)"); } status = calogOkE; pthread_mutex_lock(&gMapMutex); index = kvFindLocked(args[0].as.s.bytes, args[0].as.s.length); if (index >= 0) { status = calogValueCopy(result, &gEntries[index].value); } pthread_mutex_unlock(&gMapMutex); if (status != calogOkE) { calogValueFree(result); return calogFail(result, status, "kvGet: out of memory"); } return calogOkE; } static int32_t kvHas(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { int32_t index; (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "kvHas expects (key)"); } pthread_mutex_lock(&gMapMutex); index = kvFindLocked(args[0].as.s.bytes, args[0].as.s.length); pthread_mutex_unlock(&gMapMutex); calogValueBool(result, index >= 0); return calogOkE; } static int32_t kvKeys(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogAggT *agg; int32_t index; int32_t status; (void)args; (void)argCount; (void)userData; calogValueNil(result); status = calogAggCreate(&agg, calogListE); if (status != calogOkE) { return calogFail(result, status, "kvKeys: out of memory"); } pthread_mutex_lock(&gMapMutex); for (index = 0; index < gCount; index++) { CalogValueT key; status = calogValueString(&key, gEntries[index].keyBytes, gEntries[index].keyLen); if (status != calogOkE) { break; } status = calogAggPush(agg, &key); if (status != calogOkE) { calogValueFree(&key); break; } } pthread_mutex_unlock(&gMapMutex); if (status != calogOkE) { calogAggFree(agg); return calogFail(result, status, "kvKeys: out of memory"); } calogValueAgg(result, agg); return calogOkE; } static int32_t kvSet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogValueT copy; char *keyBytes; int64_t keyLen; int32_t index; int32_t status; (void)userData; calogValueNil(result); if (argCount != 2 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "kvSet expects (key, value)"); } if (kvContainsFn(&args[1], 0)) { return calogFail(result, calogErrUnsupportedE, "kv stores data, not functions"); } status = calogValueCopy(©, &args[1]); if (status != calogOkE) { calogValueFree(©); return calogFail(result, status, "kvSet: out of memory"); } keyLen = args[0].as.s.length; pthread_mutex_lock(&gMapMutex); index = kvFindLocked(args[0].as.s.bytes, keyLen); if (index >= 0) { // Replace: drop the old value, keep the existing key bytes. calogValueFree(&gEntries[index].value); gEntries[index].value = copy; pthread_mutex_unlock(&gMapMutex); return calogOkE; } if (gCount == gCap) { int32_t newCap; KvEntryT *grown; newCap = (gCap == 0) ? KV_INITIAL_CAP : gCap * KV_GROWTH; grown = (KvEntryT *)realloc(gEntries, (size_t)newCap * sizeof(KvEntryT)); if (grown == NULL) { pthread_mutex_unlock(&gMapMutex); calogValueFree(©); return calogFail(result, calogErrOomE, "kvSet: out of memory"); } gEntries = grown; gCap = newCap; } keyBytes = (char *)malloc((size_t)keyLen + 1); if (keyBytes == NULL) { pthread_mutex_unlock(&gMapMutex); calogValueFree(©); return calogFail(result, calogErrOomE, "kvSet: out of memory"); } if (keyLen > 0) { memcpy(keyBytes, args[0].as.s.bytes, (size_t)keyLen); } keyBytes[keyLen] = '\0'; gEntries[gCount].keyBytes = keyBytes; gEntries[gCount].keyLen = keyLen; gEntries[gCount].value = copy; gCount++; pthread_mutex_unlock(&gMapMutex); return calogOkE; }