// calogExport.c -- calog export library (see calogExport.h). A process-wide, mutex-guarded // map of name -> reference-counted function value. calogExport/calogUnexport/calogCall are // natives; __calogExportResolve is the hook the per-engine dynamic-name resolvers look up. // // The registry state is STATIC (not heap), and never freed: the four natives are reachable // (via any context, and via every Lua unknown-global lookup) right up until calogDestroy // tears the broker registry down, so freeing the state in calogExportShutdown -- which the // contract requires BEFORE calogDestroy -- would leave those natives dereferencing freed // memory. calogExportShutdown instead just releases the exported functions and empties the // map; the small static bookkeeping persists for the process (no per-cycle leak). #define _POSIX_C_SOURCE 200809L #include "calogExport.h" #include #include #include #include #include "calogInternal.h" typedef struct ExportEntryT { char *name; int64_t nameLen; CalogFnT *fn; } ExportEntryT; // The map (heap array), guarded by gMapMutex. refCount (guarded by gInitMutex) counts // registered runtimes so the LAST calogExportShutdown empties the map. static pthread_mutex_t gMapMutex = PTHREAD_MUTEX_INITIALIZER; static ExportEntryT *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 int32_t exportCall(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t exportFindFoldLocked(const char *bytes, int64_t length); static int32_t exportFindLocked(const char *bytes, int64_t length); static void exportFreeAll(void); static int32_t exportPublish(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t exportRemove(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t exportResolve(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t exportResolveByFind(CalogValueT *args, int32_t argCount, CalogValueT *result, int32_t (*find)(const char *, int64_t)); static int32_t exportResolveFold(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); int32_t calogExportRegister(CalogT *calog) { calogRegistryRetain(&gInitMutex, &gRefCount); calogRegisterInline(calog, "calogExport", exportPublish, NULL); calogRegisterInline(calog, "calogUnexport", exportRemove, NULL); calogRegisterInline(calog, "calogCall", exportCall, NULL); calogRegisterInline(calog, "__calogExportResolve", exportResolve, NULL); // Case-insensitive variant: my-basic uppercases every identifier at parse time, so its // bare-name resolver matches an export regardless of the case it was registered with. calogRegisterInline(calog, "__calogExportResolveFold", exportResolveFold, NULL); return calogOkE; } void calogExportShutdown(void) { // calogRegistryRelease holds gInitMutex for the whole call, including exportFreeAll below. calogRegistryRelease(&gInitMutex, &gRefCount, exportFreeAll); } static int32_t exportCall(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogFnT *fn; int32_t index; int32_t status; (void)userData; calogValueNil(result); if (argCount < 1 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "calogCall expects (name, ...args)"); } pthread_mutex_lock(&gMapMutex); index = exportFindLocked(args[0].as.s.bytes, args[0].as.s.length); if (index < 0) { pthread_mutex_unlock(&gMapMutex); return calogFail(result, calogErrNotFoundE, "calogCall: no such exported function"); } // Hold a reference across the (unlocked) call so a concurrent unexport can't free it. fn = gEntries[index].fn; calogFnRetain(fn); pthread_mutex_unlock(&gMapMutex); status = calogFnInvoke(fn, &args[1], argCount - 1, result); calogFnRelease(fn); return status; } static int32_t exportFindFoldLocked(const char *bytes, int64_t length) { int32_t index; int64_t at; for (index = 0; index < gCount; index++) { if (gEntries[index].nameLen != length) { continue; } for (at = 0; at < length; at++) { char lhs; char rhs; lhs = gEntries[index].name[at]; rhs = bytes[at]; if (lhs >= 'A' && lhs <= 'Z') { lhs = (char)(lhs - 'A' + 'a'); } if (rhs >= 'A' && rhs <= 'Z') { rhs = (char)(rhs - 'A' + 'a'); } if (lhs != rhs) { break; } } if (at == length) { return index; } } return -1; } static int32_t exportFindLocked(const char *bytes, int64_t length) { int32_t index; for (index = 0; index < gCount; index++) { if (gEntries[index].nameLen == length && memcmp(gEntries[index].name, bytes, (size_t)length) == 0) { return index; } } return -1; } // Invoked by calogRegistryRelease while gInitMutex is held, exactly once, when the last // reference drops. static void exportFreeAll(void) { int32_t index; pthread_mutex_lock(&gMapMutex); for (index = 0; index < gCount; index++) { free(gEntries[index].name); calogFnRelease(gEntries[index].fn); } free(gEntries); gEntries = NULL; gCount = 0; gCap = 0; pthread_mutex_unlock(&gMapMutex); } static int32_t exportPublish(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { void *buffer; int64_t capacity; const char *name; char *nameCopy; int64_t nameLen; int32_t index; int32_t status; (void)userData; calogValueNil(result); if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogFnE) { return calogFail(result, calogErrArgE, "calogExport expects (name, function)"); } name = args[0].as.s.bytes; nameLen = args[0].as.s.length; pthread_mutex_lock(&gMapMutex); index = exportFindLocked(name, nameLen); if (index >= 0) { // Replace: drop the old function, retain the new one. calogFnRelease(gEntries[index].fn); calogFnRetain(args[1].as.fn); gEntries[index].fn = args[1].as.fn; pthread_mutex_unlock(&gMapMutex); return calogOkE; } buffer = gEntries; capacity = gCap; status = calogGrow(&buffer, &capacity, (int64_t)gCount + 1, sizeof(ExportEntryT)); if (status != calogOkE) { pthread_mutex_unlock(&gMapMutex); return calogFail(result, status, "calogExport: out of memory"); } gEntries = (ExportEntryT *)buffer; gCap = (int32_t)capacity; nameCopy = (char *)malloc((size_t)nameLen + 1); if (nameCopy == NULL) { pthread_mutex_unlock(&gMapMutex); return calogFail(result, calogErrOomE, "calogExport: out of memory"); } if (nameLen > 0) { memcpy(nameCopy, name, (size_t)nameLen); } nameCopy[nameLen] = '\0'; calogFnRetain(args[1].as.fn); gEntries[gCount].name = nameCopy; gEntries[gCount].nameLen = nameLen; gEntries[gCount].fn = args[1].as.fn; gCount++; pthread_mutex_unlock(&gMapMutex); return calogOkE; } static int32_t exportRemove(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, "calogUnexport expects (name)"); } pthread_mutex_lock(&gMapMutex); index = exportFindLocked(args[0].as.s.bytes, args[0].as.s.length); if (index >= 0) { free(gEntries[index].name); calogFnRelease(gEntries[index].fn); gEntries[index] = gEntries[gCount - 1]; gCount--; } pthread_mutex_unlock(&gMapMutex); return calogOkE; } static int32_t exportResolve(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; return exportResolveByFind(args, argCount, result, exportFindLocked); } static int32_t exportResolveByFind(CalogValueT *args, int32_t argCount, CalogValueT *result, int32_t (*find)(const char *, int64_t)) { int32_t index; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "export resolve expects (name)"); } pthread_mutex_lock(&gMapMutex); index = find(args[0].as.s.bytes, args[0].as.s.length); if (index >= 0) { CalogFnT *fn; fn = gEntries[index].fn; // The result carries a reference; whoever consumes it releases that reference. calogFnRetain(fn); calogValueFn(result, fn); } pthread_mutex_unlock(&gMapMutex); return calogOkE; } static int32_t exportResolveFold(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; return exportResolveByFind(args, argCount, result, exportFindFoldLocked); }