// testEngineLua.c -- a real Lua interpreter running on a CalogContextT thread. // // Proves the CalogEngineT wiring: a Lua context creates its interpreter on its own // thread (calogLuaEngine.createInterpreter), exposes broker natives there, and runs // scripts via calogContextEval. The script calls one thread-agnostic native (report, // owner 0, run inline on the Lua thread) and one native owned by a DIFFERENT // context (doubleIt, owned by ctx2) -- the exposed-native trampoline routes that // call through the broker to ctx2's thread and back, with no callback in the // script. Built under ASan+UBSan; the threading core is covered by testActor. #include "calog.h" #include #include #include #include #define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__) #define DOUBLE_EXPECTED 42 #define PURE_EXPECTED 6 static CalogT *broker = NULL; static CalogContextT *luaCtx = NULL; static CalogContextT *workerCtx = NULL; static _Atomic int64_t reportedValue = 0; static _Atomic uint32_t reportedCtxId = 0; static _Atomic uint32_t doubleItCtxId = 0; static CalogFnT *_Atomic storedCb = NULL; 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 nativeDoubleIt(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 nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static void testCallbackAcrossContexts(void); static void testCrossContextFromScript(void); static void testFailedInterpreter(void); static void testPureScript(void); static void testScriptError(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 nativeDoubleIt(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; // Owned by workerCtx: routing must run this on workerCtx's thread, so record // the id it actually ran on. atomic_store(&doubleItCtxId, calogCurrentId()); if (argCount != 1 || args[0].type != calogIntE) { return calogFail(result, calogErrArgE, "doubleIt expects one integer"); } calogValueInt(result, args[0].as.i * 2); return calogOkE; } static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; if (argCount != 1 || args[0].type != calogIntE) { return calogFail(result, calogErrArgE, "report expects one integer"); } atomic_store(&reportedCtxId, calogCurrentId()); atomic_store(&reportedValue, args[0].as.i); 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"); } // Runs on luaCtx's thread; the closure is owned by luaCtx. Retain it so it // outlives this call (the trampoline frees the args afterward). calogFnRetain(args[0].as.fn); atomic_store(&storedCb, args[0].as.fn); return calogOkE; } static void testCallbackAcrossContexts(void) { CalogFnT *callback; CalogValueT arg; CalogValueT result; int32_t status; // A Lua closure is captured on luaCtx's thread via setCb (owned by luaCtx). status = calogContextEval(luaCtx, "setCb(function(x) return x + 100 end)", &result); CHECK(status == calogOkE, "captured a Lua closure as a CalogFnT on its owner thread"); calogValueFree(&result); callback = atomic_load(&storedCb); CHECK(callback != NULL, "callback was stored"); // Invoke it from the MAIN thread: calogFnInvoke must marshal to luaCtx's thread // (design.md sec 10) so the closure runs on its owner, never on the main thread. 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 context"); calogValueFree(&result); calogValueFree(&arg); // Drop the last reference from the main thread: the luaL_unref must run on // luaCtx's thread, not here (ASan/TSan would flag an off-thread touch). calogFnRelease(callback); } static void testCrossContextFromScript(void) { CalogValueT result; int32_t status; // doubleIt is owned by workerCtx; report is thread-agnostic. The script reads // as plain synchronous code -- the cross-context hop is invisible to it. status = calogContextEval(luaCtx, "report(doubleIt(21))", &result); CHECK(status == calogOkE, "script with a cross-context call ran without error"); CHECK(atomic_load(&reportedValue) == DOUBLE_EXPECTED, "cross-context doubleIt result flowed back into the script"); CHECK(atomic_load(&doubleItCtxId) == calogContextId(workerCtx), "doubleIt executed on the worker context's thread"); CHECK(atomic_load(&reportedCtxId) == calogContextId(luaCtx), "report executed on the Lua context's own thread"); calogValueFree(&result); } static void testFailedInterpreter(void) { CalogContextT *failCtx; const char *badNames[1]; CalogConfigT badConfig; CalogValueT result; int32_t status; // A config naming a native that was never registered makes createInterpreter // fail on the context's thread (interp stays NULL). The eval must come back as // an error rather than dereferencing the NULL interpreter. badNames[0] = "noSuchNative"; badConfig.exposeNames = badNames; badConfig.exposeCount = 1; calogContextCreate(broker, &calogLuaEngine, &badConfig, &failCtx); calogContextStart(failCtx); status = calogContextEval(failCtx, "report(1)", &result); CHECK(status != calogOkE, "eval on a context whose interpreter failed to init returns an error, not a crash"); calogValueFree(&result); calogContextDestroy(failCtx); } static void testPureScript(void) { CalogValueT result; int32_t status; status = calogContextEval(luaCtx, "report(1 + 2 + 3)", &result); CHECK(status == calogOkE && atomic_load(&reportedValue) == PURE_EXPECTED, "a second eval on the live interpreter ran"); calogValueFree(&result); } static void testScriptError(void) { CalogValueT result; int32_t status; // A syntax error must come back as a failing status with the error in result // (the single channel), and must not wedge the interpreter. status = calogContextEval(luaCtx, "@@@ not valid lua @@@", &result); CHECK(status != calogOkE && result.type == calogStringE, "script error surfaced through result, not a crash"); calogValueFree(&result); } int main(void) { const char *exposeNames[3]; CalogConfigT luaConfig; broker = calogCreate(); if (broker == NULL) { printf("broker create failed\n"); return 1; } // Worker context hosts doubleIt; it has no interpreter (a pure native owner). calogContextCreate(broker, NULL, NULL, &workerCtx); exposeNames[0] = "doubleIt"; exposeNames[1] = "report"; exposeNames[2] = "setCb"; luaConfig.exposeNames = exposeNames; luaConfig.exposeCount = 3; calogContextCreate(broker, &calogLuaEngine, &luaConfig, &luaCtx); // setCb is owned by luaCtx, so register it once luaCtx exists (for its id), but // before calogContextStart -- createInterpreter exposes the names on the thread. calogRegister(broker, "doubleIt", nativeDoubleIt, NULL, calogContextId(workerCtx)); calogRegister(broker, "report", nativeReport, NULL, 0); calogRegister(broker, "setCb", nativeSetCb, NULL, calogContextId(luaCtx)); calogContextStart(workerCtx); calogContextStart(luaCtx); testCrossContextFromScript(); testPureScript(); testScriptError(); testFailedInterpreter(); testCallbackAcrossContexts(); calogDestroy(broker); printf("\n%d checks, %d failed\n", testsRun, testsFailed); fflush(stdout); if (testsFailed != 0) { return 1; } return 0; }