266 lines
9.8 KiB
C
266 lines
9.8 KiB
C
// 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 <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <time.h>
|
|
|
|
#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;
|
|
}
|