// testExport.c -- exercises the export library. One Lua context publishes a function with // calogExport(); another Lua context calls it by its BARE name (via the globals __index resolver); // a JavaScript context calls it via calogCall. All calls run in the exporter's context. #define _POSIX_C_SOURCE 200809L #include "calog.h" #include "calogExport.h" #include #include #include #include #define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__) #define PUMP_LIMIT 4000 // s7 interns a handful of permanent strings it never frees (by design); suppress those so // the leak checker still catches real leaks in the export paths. const char *__lsan_default_suppressions(void); const char *__lsan_default_suppressions(void) { return "leak:make_permanent_string\n"; } #define RESULT_SLOTS 12 static CalogT *calog = NULL; static _Atomic int64_t results[RESULT_SLOTS]; static _Atomic bool exporterReady = false; static _Atomic int32_t doneCount = 0; static _Atomic int32_t errorCount = 0; static int32_t testsRun = 0; static int32_t testsFailed = 0; static void checkImpl(bool condition, const char *message, const char *file, int32_t line); static int64_t asInt(const CalogValueT *value); static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReady(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static void onError(uint64_t contextId, const char *message, void *userData); static void pumpUntilDone(int32_t target); static void pumpUntilReady(void); static int64_t asInt(const CalogValueT *value) { if (value->type == calogIntE) { return value->as.i; } return (int64_t)value->as.r; } static void checkImpl(bool condition, const char *message, const char *file, int32_t line) { testsRun++; if (!condition) { testsFailed++; printf("FAIL %s:%d %s\n", file, line, message); } } static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; (void)userData; atomic_fetch_add(&doneCount, 1); calogValueNil(result); return calogOkE; } static int32_t nativeReady(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; (void)userData; atomic_store(&exporterReady, true); calogValueNil(result); return calogOkE; } static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { int64_t tag; (void)userData; calogValueNil(result); if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE) || (args[1].type != calogIntE && args[1].type != calogRealE)) { return calogFail(result, calogErrArgE, "report expects (tag, value)"); } tag = asInt(&args[0]); if (tag < 0 || tag >= RESULT_SLOTS) { return calogFail(result, calogErrArgE, "report: tag out of range"); } atomic_store(&results[tag], asInt(&args[1])); return calogOkE; } static void onError(uint64_t contextId, const char *message, void *userData) { (void)contextId; (void)userData; fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)"); atomic_fetch_add(&errorCount, 1); } static void pumpUntilDone(int32_t target) { struct timespec ts = { 0, 500000 }; int i; for (i = 0; i < PUMP_LIMIT; i++) { calogPump(calog); if (atomic_load(&doneCount) >= target) { calogPump(calog); return; } nanosleep(&ts, NULL); } } static void pumpUntilReady(void) { struct timespec ts = { 0, 500000 }; int i; for (i = 0; i < PUMP_LIMIT; i++) { calogPump(calog); if (atomic_load(&exporterReady) || atomic_load(&errorCount) != 0) { return; } nanosleep(&ts, NULL); } } int main(void) { CalogContextT *exporter; CalogContextT *luaCaller; CalogContextT *jsCaller; CalogContextT *s7Caller; CalogContextT *squirrelCaller; CalogContextT *mbCaller; CalogContextT *reloadV1; CalogContextT *reloadV2; int32_t i; calog = calogCreate(); if (calog == NULL) { printf("calog create failed\n"); return 1; } calogSetErrorHandler(calog, onError, NULL); calogRegister(calog, "report", nativeReport, NULL); calogRegister(calog, "done", nativeDone, NULL); calogRegister(calog, "exporterReady", nativeReady, NULL); if (calogExportRegister(calog) != calogOkE) { printf("calogExportRegister failed\n"); return 1; } for (i = 0; i < RESULT_SLOTS; i++) { atomic_store(&results[i], -1); } // Publisher: define a function and export it. It must stay open so calls can run in it. exporter = calogContextOpen(calog, &calogLuaEngine); calogContextEval(exporter, "local function adder(a, b) return a + b end\n" "calogExport('adder', adder)\n" "exporterReady()"); pumpUntilReady(); // A different Lua context calls the exported function by its BARE name and via call. luaCaller = calogContextOpen(calog, &calogLuaEngine); calogContextEval(luaCaller, "report(1, adder(2, 3))\n" "report(2, calogCall('adder', 10, 20))\n" "report(10, __calogExportResolve == nil and 1 or 0)\n" // internal native must be hidden "done()"); // A JavaScript context reaches the same Lua function via call, then by BARE name. jsCaller = calogContextOpen(calog, &calogJsEngine); calogContextEval(jsCaller, "report(3, calogCall('adder', 100, 1));\n" "report(8, adder(9, 1));\n" "done();"); // Bare-name resolution in the other clean engines: s7 (unbound-variable hook) and // Squirrel (root-table _get delegate). s7Caller = calogContextOpen(calog, &calogS7Engine); calogContextEval(s7Caller, "(begin (report 6 (adder 100 5)) (done))"); squirrelCaller = calogContextOpen(calog, &calogSquirrelEngine); calogContextEval(squirrelCaller, "report(7, adder(50, 5));\n" "done();"); // my-basic resolves names statically (no bare-name), so it reaches the export via // calogCall -- which the calog prefix keeps clear of its CALL keyword. mbCaller = calogContextOpen(calog, &calogMyBasicEngine); calogContextEval(mbCaller, "x = calogCall(\"adder\", 30, 5)\n" "report(9, x)\n" "done()"); pumpUntilDone(5); CHECK(atomic_load(&results[1]) == 5, "a Lua script called an exported function by its bare name"); CHECK(atomic_load(&results[2]) == 30, "calogCall reached the same exported function"); CHECK(atomic_load(&results[3]) == 101, "a JavaScript script called the exported Lua function via calogCall"); CHECK(atomic_load(&results[8]) == 10, "a JavaScript script called the export by its bare name"); CHECK(atomic_load(&results[6]) == 105, "an s7 script called the export by its bare name"); CHECK(atomic_load(&results[7]) == 55, "a Squirrel script called the export by its bare name"); CHECK(atomic_load(&results[9]) == 35, "a my-basic script reached the export via calogCall"); CHECK(atomic_load(&results[10]) == 1, "the internal __calogExportResolve native is hidden from script globals"); CHECK(atomic_load(&errorCount) == 0, "no errors during export/import"); // Hot-reload: a script exports a function, is unloaded, then a NEW version re-exports the // same name. Re-exporting releases the old exported function -- whose owner context is now // gone -- which must free its struct directly instead of running the engine release on the // destroyed interpreter. Callers must then see the new version. reloadV1 = calogContextOpen(calog, &calogLuaEngine); atomic_store(&exporterReady, false); calogContextEval(reloadV1, "local function greet() return 111 end\n" "calogExport('greet', greet)\n" "exporterReady()"); pumpUntilReady(); calogContextEval(luaCaller, "report(4, calogCall('greet'))\n done()"); pumpUntilDone(6); calogContextClose(reloadV1); // greet stays exported, but its owner is gone reloadV2 = calogContextOpen(calog, &calogLuaEngine); atomic_store(&exporterReady, false); calogContextEval(reloadV2, "local function greet() return 222 end\n" "calogExport('greet', greet)\n" // releases the old, dead-owner greet "exporterReady()"); pumpUntilReady(); calogContextEval(luaCaller, "report(5, calogCall('greet'))\n done()"); pumpUntilDone(7); CHECK(atomic_load(&results[4]) == 111, "an exported function is callable before reload"); CHECK(atomic_load(&results[5]) == 222, "a reloaded script re-exports a new version after the old owner unloaded"); calogContextClose(reloadV2); // Release exports while the runtime + contexts are still alive, then tear the runtime down. calogExportShutdown(); // Regression: after shutdown, an unknown-global read still routes through // __calogExportResolve (via the _G __index) -- the static registry keeps it safe. calogContextEval(luaCaller, "local x = someUndefinedGlobalName\n done()"); pumpUntilDone(8); CHECK(atomic_load(&doneCount) >= 8, "resolving an unknown global after export shutdown is safe"); calogContextClose(luaCaller); calogContextClose(jsCaller); calogContextClose(s7Caller); calogContextClose(squirrelCaller); calogContextClose(mbCaller); calogContextClose(exporter); calogDestroy(calog); printf("\n%d checks, %d failed\n", testsRun, testsFailed); fflush(stdout); if (testsFailed != 0) { return 1; } return 0; }