// testEngineLua.c -- Lua on a context thread, driven the way a host drives calog: // register natives, open a context, fire-and-forget a script, and calogPump on the // host thread to service the script's native calls. Verifies host-thread dispatch, // the inline escape hatch, the sec-10 cross-thread callback, the error handler, and // concurrent contexts. Built under ASan+UBSan. #define _POSIX_C_SOURCE 200809L #include "calog.h" #include #include #include #include #include #define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__) #define PUMP_LIMIT 4000 // ~2s at 0.5ms/iter, a generous upper bound static CalogT *calog = NULL; static _Atomic int64_t reportedValue = 0; static _Atomic uint64_t reportedCtxId = 0xFFFFu; static _Atomic uint64_t inlineCtxId = 0xFFFFu; static _Atomic int32_t bumpCount = 0; static _Atomic bool scriptDone = false; static _Atomic int32_t errorCount = 0; static _Atomic uint64_t errorCtxId = 0; static CalogFnT *_Atomic storedCb = NULL; static _Atomic int32_t matchCount = 0; static _Atomic int32_t mismatchCount = 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 int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeCheckRuntime(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static void onError(uint64_t contextId, const char *message, void *userData); static void pumpUntilDone(void); static void testConcurrentContexts(void); static void testContextLoad(void); static void testCrossThreadCallback(void); static void testForeignFunction(void); static void testHostAndInlineNatives(void); static void testScriptError(void); static void testSingleThreadMultiRuntime(void); 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 nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) { return calogFail(result, calogErrArgE, "add expects two integers"); } calogValueInt(result, args[0].as.i + args[1].as.i); return calogOkE; } static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; (void)userData; atomic_fetch_add(&bumpCount, 1); calogValueNil(result); return calogOkE; } static int32_t nativeCheckRuntime(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; // userData is the runtime this native was registered under. Run from a script and // serviced by calogPump, it must see calogCurrent() == that runtime -- which holds // only if pump presents the pumping thread as the pumped runtime's host, so one // thread can host several runtimes. if (calogCurrent() == (CalogT *)userData) { atomic_fetch_add(&matchCount, 1); } else { atomic_fetch_add(&mismatchCount, 1); } calogValueNil(result); return calogOkE; } static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; (void)userData; atomic_store(&scriptDone, true); calogValueNil(result); return calogOkE; } static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogFnT *callable; int32_t status; (void)args; (void)argCount; (void)userData; calogValueNil(result); // A host-owned function value handed to the script; calling it routes back here. status = calogFnFromNative(&callable, calog, nativeAdd, NULL); if (status != calogOkE) { return calogFail(result, status, "getAdder could not allocate"); } calogValueFn(result, callable); return calogOkE; } static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogIntE) { return calogFail(result, calogErrArgE, "report expects one integer"); } atomic_store(&reportedCtxId, calogCurrentId()); // should be 0 (host thread) atomic_store(&reportedValue, args[0].as.i); return calogOkE; } static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; (void)userData; atomic_store(&inlineCtxId, calogCurrentId()); // should be the script's context id calogValueNil(result); return calogOkE; } static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogFnE) { return calogFail(result, calogErrArgE, "setCb expects one function"); } calogFnRetain(args[0].as.fn); atomic_store(&storedCb, args[0].as.fn); return calogOkE; } static void onError(uint64_t contextId, const char *message, void *userData) { (void)message; (void)userData; atomic_store(&errorCtxId, contextId); atomic_fetch_add(&errorCount, 1); } // Pump the host thread until the running script signals done (or errors), bounded. static void pumpUntilDone(void) { struct timespec ts = { 0, 500000 }; // 0.5 ms int errorsBefore; int i; errorsBefore = atomic_load(&errorCount); for (i = 0; i < PUMP_LIMIT; i++) { calogPump(calog); if (atomic_load(&scriptDone) || atomic_load(&errorCount) != errorsBefore) { calogPump(calog); // one more sweep to drain trailing calls return; } nanosleep(&ts, NULL); } } static void testHostAndInlineNatives(void) { CalogContextT *ctx; ctx = calogContextOpen(calog, &calogLuaEngine); CHECK(ctx != NULL, "opened a Lua context"); atomic_store(&scriptDone, false); calogContextEval(ctx, "report(42); reportInline(1); done()"); pumpUntilDone(); CHECK(atomic_load(&reportedValue) == 42, "host native received the argument"); CHECK(atomic_load(&reportedCtxId) == 0, "default native ran on the host thread (id 0)"); CHECK(atomic_load(&inlineCtxId) == calogContextId(ctx), "inline native ran on the script's own thread"); calogContextClose(ctx); } static void testCrossThreadCallback(void) { CalogContextT *ctx; CalogFnT *callback; CalogValueT arg; CalogValueT result; int32_t status; ctx = calogContextOpen(calog, &calogLuaEngine); atomic_store(&scriptDone, false); atomic_store(&storedCb, NULL); calogContextEval(ctx, "setCb(function(x) return x + 100 end); done()"); pumpUntilDone(); callback = atomic_load(&storedCb); CHECK(callback != NULL, "a Lua closure was captured as a CalogFnT"); // Invoke it from the host thread: it must marshal to the Lua context's thread // (sec 10). calogFnInvoke blocks-and-pumps the host queue while it waits. calogValueInt(&arg, 7); status = calogFnInvoke(callback, &arg, 1, &result); CHECK(status == calogOkE && result.type == calogIntE && result.as.i == 107, "cross-thread callable invoke routed to the owner"); calogValueFree(&result); calogValueFree(&arg); calogFnRelease(callback); calogContextClose(ctx); } static void testForeignFunction(void) { CalogContextT *ctx; ctx = calogContextOpen(calog, &calogLuaEngine); atomic_store(&scriptDone, false); atomic_store(&reportedValue, 0); // The script receives a host-owned function value and calls it. calogContextEval(ctx, "report(getAdder()(2, 3)); done()"); pumpUntilDone(); CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host"); calogContextClose(ctx); } static void testScriptError(void) { CalogContextT *ctx; int32_t before; ctx = calogContextOpen(calog, &calogLuaEngine); before = atomic_load(&errorCount); atomic_store(&scriptDone, false); calogContextEval(ctx, "@@@ not valid lua @@@"); pumpUntilDone(); CHECK(atomic_load(&errorCount) == before + 1, "a fire-and-forget script error reached the error handler"); CHECK(atomic_load(&errorCtxId) == calogContextId(ctx), "the error names the failing context"); calogContextClose(ctx); } static void testConcurrentContexts(void) { CalogContextT *ctxs[3]; struct timespec ts = { 0, 500000 }; int32_t i; atomic_store(&bumpCount, 0); for (i = 0; i < 3; i++) { ctxs[i] = calogContextOpen(calog, &calogLuaEngine); calogContextEval(ctxs[i], "bump(); bump(); bump()"); } for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 9; i++) { calogPump(calog); nanosleep(&ts, NULL); } CHECK(atomic_load(&bumpCount) == 9, "three concurrent contexts all dispatched to the host thread"); for (i = 0; i < 3; i++) { calogContextClose(ctxs[i]); } } static void testContextLoad(void) { CalogContextT *ctx; struct timespec ts = { 0, 500000 }; FILE *file; int32_t i; // Registration makes the Lua engine (extension "lua") a load candidate. calogRegisterEngine(calog, &calogLuaEngine); // A file whose extension matches a registered engine is found, loaded, and run. file = fopen("calogLoadTest.lua", "wb"); CHECK(file != NULL, "wrote a temporary .lua script"); if (file != NULL) { fputs("bump()", file); fclose(file); } atomic_store(&bumpCount, 0); ctx = calogContextLoad(calog, "calogLoadTest"); CHECK(ctx != NULL, "calogContextLoad opened a context for the matching .lua file"); for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 1; i++) { calogPump(calog); nanosleep(&ts, NULL); } CHECK(atomic_load(&bumpCount) == 1, "the loaded script ran on its new context"); if (ctx != NULL) { calogContextClose(ctx); } remove("calogLoadTest.lua"); // No file with a registered extension exists -> NULL. ctx = calogContextLoad(calog, "calogNoSuchScript"); CHECK(ctx == NULL, "calogContextLoad returns NULL when no file matches"); } static void testSingleThreadMultiRuntime(void) { CalogT *calogB; CalogContextT *ctxA; CalogContextT *ctxB; struct timespec ts = { 0, 500000 }; int32_t i; // This one thread now hosts TWO runtimes. Each runs a script that calls a native // registered under a different runtime; pumping each in turn must make calogCurrent() // resolve to the runtime being pumped -- not whichever was created last. calogB = calogCreate(); CHECK(calogB != NULL, "created a second runtime on this same thread"); calogRegister(calog, "checkRuntime", nativeCheckRuntime, calog); calogRegister(calogB, "checkRuntime", nativeCheckRuntime, calogB); atomic_store(&matchCount, 0); atomic_store(&mismatchCount, 0); ctxA = calogContextOpen(calog, &calogLuaEngine); ctxB = calogContextOpen(calogB, &calogLuaEngine); calogContextEval(ctxA, "checkRuntime()"); calogContextEval(ctxB, "checkRuntime()"); for (i = 0; i < PUMP_LIMIT && (atomic_load(&matchCount) + atomic_load(&mismatchCount)) < 2; i++) { calogPump(calog); calogPump(calogB); nanosleep(&ts, NULL); } CHECK(atomic_load(&matchCount) == 2, "one thread pumped two runtimes; each native saw its own runtime"); CHECK(atomic_load(&mismatchCount) == 0, "no native resolved to the wrong runtime while sharing a host thread"); calogContextClose(ctxA); calogContextClose(ctxB); calogDestroy(calogB); } int main(void) { calog = calogCreate(); if (calog == NULL) { printf("calog create failed\n"); return 1; } calogSetErrorHandler(calog, onError, NULL); calogRegister(calog, "report", nativeReport, NULL); calogRegister(calog, "setCb", nativeSetCb, NULL); calogRegister(calog, "done", nativeDone, NULL); calogRegister(calog, "bump", nativeBump, NULL); calogRegister(calog, "getAdder", nativeGetAdder, NULL); calogRegisterInline(calog, "reportInline", nativeReportInline, NULL); testHostAndInlineNatives(); testCrossThreadCallback(); testForeignFunction(); testScriptError(); testConcurrentContexts(); testSingleThreadMultiRuntime(); testContextLoad(); calogDestroy(calog); printf("\n%d checks, %d failed\n", testsRun, testsFailed); fflush(stdout); if (testsFailed != 0) { return 1; } return 0; }