// testEngineJs.c -- a real Duktape (JavaScript) heap running on a CalogContextT // thread, the fourth engine. // // Proves the CalogEngineT wiring and that the actor core drives JS exactly as it drives // Lua/Squirrel: a JS script on jsCtx's thread calls a thread-agnostic native // (report), a native owned by a different context (doubleIt, routed cross-thread), // and -- exercising design.md sec 10 -- a JS closure captured on jsCtx's thread is // invoked and released from the main thread, both marshalled back to the owner. // Built under ASan+UBSan. #include "calog.h" #include #include #include #include #define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__) #define DOUBLE_EXPECTED 42 #define CALLBACK_INPUT 7 #define CALLBACK_EXPECTED 107 static CalogT *broker = NULL; static CalogContextT *jsCtx = 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 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; 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 jsCtx's thread; the closure is owned by jsCtx. Retain it past this call. 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; // Capture a JS closure on jsCtx's thread via setCb (owned by jsCtx). status = calogContextEval(jsCtx, "setCb(function(x) { return x + 100; })", &result); CHECK(status == calogOkE, "captured a JS closure as a CalogFnT on its owner thread"); calogValueFree(&result); callback = atomic_load(&storedCb); CHECK(callback != NULL, "callback was stored"); // Invoke from the MAIN thread: calogFnInvoke must marshal to jsCtx's thread // (design.md sec 10), so the closure runs on its owner heap, never here. calogValueInt(&arg, CALLBACK_INPUT); status = calogFnInvoke(callback, &arg, 1, &result); CHECK(status == calogOkE && result.type == calogIntE && result.as.i == CALLBACK_EXPECTED, "cross-thread callable invoke routed to the owner context"); calogValueFree(&result); calogValueFree(&arg); // Release from the main thread: the registry-slot drop must run on jsCtx's heap. calogFnRelease(callback); } static void testCrossContextFromScript(void) { CalogValueT result; int32_t status; status = calogContextEval(jsCtx, "report(doubleIt(21))", &result); CHECK(status == calogOkE, "JS 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(jsCtx), "report executed on the JS context's own thread"); calogValueFree(&result); } static void testScriptError(void) { CalogValueT result; int32_t status; status = calogContextEval(jsCtx, "@@@ not valid js @@@", &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 jsConfig; broker = calogCreate(); if (broker == NULL) { printf("broker create failed\n"); return 1; } calogContextCreate(broker, NULL, NULL, &workerCtx); exposeNames[0] = "doubleIt"; exposeNames[1] = "report"; exposeNames[2] = "setCb"; jsConfig.exposeNames = exposeNames; jsConfig.exposeCount = 3; calogContextCreate(broker, &calogJsEngine, &jsConfig, &jsCtx); // setCb is owned by jsCtx, so register it once jsCtx exists (for its id), before // calogContextStart 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(jsCtx)); calogContextStart(workerCtx); calogContextStart(jsCtx); testCrossContextFromScript(); testScriptError(); testCallbackAcrossContexts(); calogDestroy(broker); printf("\n%d checks, %d failed\n", testsRun, testsFailed); fflush(stdout); if (testsFailed != 0) { return 1; } return 0; }