Fixed method/function name collisions.

This commit is contained in:
Scott Duensing 2026-07-02 22:26:24 -05:00
parent 0a82eb2ac9
commit 82c040395d
11 changed files with 72 additions and 44 deletions

View file

@ -399,7 +399,7 @@ bin/testTask: obj/testTask.o $(TASKADP) obj/calogHandle.o lib/libcalog.a lib/lib
$(CC) $(LDFLAGS) -pthread -o $@ $^ $(LUALIBS) -lstdc++ -lm
# export library test: publish from Lua, call by bare name from Lua + via callExport from JS.
bin/testExport: obj/testExport.o obj/calogExport.o lib/libcalog.a lib/liblua.a lib/libquickjs.a lib/libsquirrel.a lib/libs7.a | bin
bin/testExport: obj/testExport.o obj/calogExport.o lib/libcalog.a lib/liblua.a lib/libquickjs.a lib/libsquirrel.a lib/libs7.a lib/libmybasic.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ $(LUALIBS) -lstdc++ -ldl -lm
obj bin lib:

View file

@ -1,6 +1,6 @@
// calogExport.c -- calog export library (see calogExport.h). A process-wide, mutex-guarded
// map of name -> reference-counted function value. export/unexport/call are natives;
// __exportResolve is the hook the per-engine dynamic-name resolvers call to look a name up.
// map of name -> reference-counted function value. calogExport/calogUnexport/calogCall are
// natives; __calogExportResolve is the hook the per-engine dynamic-name resolvers look up.
//
// The registry state is STATIC (not heap), and never freed: the four natives are reachable
// (via any context, and via every Lua unknown-global lookup) right up until calogDestroy
@ -46,10 +46,10 @@ int32_t calogExportRegister(CalogT *calog) {
pthread_mutex_lock(&gInitMutex);
gRefCount++;
pthread_mutex_unlock(&gInitMutex);
calogRegisterInline(calog, "export", exportPublish, NULL);
calogRegisterInline(calog, "unexport", exportRemove, NULL);
calogRegisterInline(calog, "call", exportCall, NULL);
calogRegisterInline(calog, "__exportResolve", exportResolve, NULL);
calogRegisterInline(calog, "calogExport", exportPublish, NULL);
calogRegisterInline(calog, "calogUnexport", exportRemove, NULL);
calogRegisterInline(calog, "calogCall", exportCall, NULL);
calogRegisterInline(calog, "__calogExportResolve", exportResolve, NULL);
return calogOkE;
}
@ -84,13 +84,13 @@ static int32_t exportCall(CalogValueT *args, int32_t argCount, CalogValueT *resu
(void)userData;
calogValueNil(result);
if (argCount < 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "call expects (name, ...args)");
return calogFail(result, calogErrArgE, "calogCall expects (name, ...args)");
}
pthread_mutex_lock(&gMapMutex);
index = exportFindLocked(args[0].as.s.bytes);
if (index < 0) {
pthread_mutex_unlock(&gMapMutex);
return calogFail(result, calogErrNotFoundE, "call: no such exported function");
return calogFail(result, calogErrNotFoundE, "calogCall: no such exported function");
}
// Hold a reference across the (unlocked) call so a concurrent unexport can't free it.
fn = gEntries[index].fn;
@ -122,7 +122,7 @@ static int32_t exportPublish(CalogValueT *args, int32_t argCount, CalogValueT *r
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogFnE) {
return calogFail(result, calogErrArgE, "export expects (name, function)");
return calogFail(result, calogErrArgE, "calogExport expects (name, function)");
}
name = args[0].as.s.bytes;
pthread_mutex_lock(&gMapMutex);
@ -142,7 +142,7 @@ static int32_t exportPublish(CalogValueT *args, int32_t argCount, CalogValueT *r
grown = (ExportEntryT *)realloc(gEntries, (size_t)newCap * sizeof(ExportEntryT));
if (grown == NULL) {
pthread_mutex_unlock(&gMapMutex);
return calogFail(result, calogErrOomE, "export: out of memory");
return calogFail(result, calogErrOomE, "calogExport: out of memory");
}
gEntries = grown;
gCap = newCap;
@ -150,7 +150,7 @@ static int32_t exportPublish(CalogValueT *args, int32_t argCount, CalogValueT *r
nameCopy = strdup(name);
if (nameCopy == NULL) {
pthread_mutex_unlock(&gMapMutex);
return calogFail(result, calogErrOomE, "export: out of memory");
return calogFail(result, calogErrOomE, "calogExport: out of memory");
}
calogFnRetain(args[1].as.fn);
gEntries[gCount].name = nameCopy;
@ -167,7 +167,7 @@ static int32_t exportRemove(CalogValueT *args, int32_t argCount, CalogValueT *re
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "unexport expects (name)");
return calogFail(result, calogErrArgE, "calogUnexport expects (name)");
}
pthread_mutex_lock(&gMapMutex);
index = exportFindLocked(args[0].as.s.bytes);
@ -188,7 +188,7 @@ static int32_t exportResolve(CalogValueT *args, int32_t argCount, CalogValueT *r
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "__exportResolve expects (name)");
return calogFail(result, calogErrArgE, "__calogExportResolve expects (name)");
}
pthread_mutex_lock(&gMapMutex);
index = exportFindLocked(args[0].as.s.bytes);

View file

@ -1,14 +1,17 @@
// calogExport.h -- calog export library: publish script functions other scripts can call.
//
// Registers natives so a script can share a function by name across contexts and engines:
// export(name, fn) publish a function value under a global name
// unexport(name) remove it
// call(name, ...args) call an exported function by name -- works in EVERY engine
// calogExport(name, fn) publish a function value under a global name
// calogUnexport(name) remove it
// calogCall(name, ...args) call an exported function by name -- works in EVERY engine
//
// The natives are calog-prefixed so they never collide with an engine's reserved words
// (plain `export` is a JavaScript keyword; plain `call` is a my-basic keyword).
//
// On engines with a runtime unknown-name hook (Lua, Squirrel, JavaScript, s7), an exported
// name is ALSO reachable by its BARE name, e.g. `luaExample(1, 2)`; engines that resolve
// names statically (Wren, Berry, my-basic) use call('luaExample', ...). Resolution is
// dynamic: a name exported at any time becomes callable immediately, and unexport takes
// names statically (Wren, Berry, my-basic) use calogCall('luaExample', ...). Resolution is
// dynamic: a name exported at any time becomes callable immediately, and calogUnexport takes
// effect at once.
//
// An exported function is an ordinary calog function value, so a call runs in the exporter's

View file

@ -95,9 +95,18 @@ void calogForEach(CalogT *broker, void (*visit)(const CalogEntryT *entry, void *
int64_t index;
for (index = 0; index < broker->slotCount; index++) {
if (broker->slots[index].name != NULL) {
visit(&broker->slots[index], ud);
const char *name;
name = broker->slots[index].name;
if (name == NULL) {
continue;
}
// Natives whose name begins with "__" are internal (e.g. the export resolver hook
// __calogExportResolve): still reachable through calogCall/calogLookup, but never
// exposed into an engine's global namespace, so scripts cannot see or shadow them.
if (name[0] == '_' && name[1] == '_') {
continue;
}
visit(&broker->slots[index], ud);
}
}

View file

@ -139,6 +139,8 @@ void calogPump(CalogT *calog); // run pending script->native calls on THIS
void calogSetErrorHandler(CalogT *calog, CalogErrorFnT fn, void *userData);
// ---- natives ----
// A name beginning with "__" is INTERNAL: it is callable via calogCall but is NOT exposed
// into any engine's global namespace, so scripts can neither see nor shadow it.
int32_t calogRegister(CalogT *calog, const char *name, CalogNativeFnT fn, void *userData); // runs on the host thread
int32_t calogRegisterInline(CalogT *calog, const char *name, CalogNativeFnT fn, void *userData); // runs on the calling script thread
int32_t calogCall(CalogT *calog, const char *name, CalogValueT *args, int32_t argCount, CalogValueT *result);

View file

@ -88,7 +88,8 @@ struct CalogT {
CalogT *calogBrokerCreate(void);
void calogBrokerDestroy(CalogT *calog);
CalogEntryT *calogLookup(CalogT *calog, const char *name);
// Visit every registered entry (used by createInterpreter to expose all natives).
// Visit every registered entry EXCEPT internal "__"-prefixed ones (used by createInterpreter
// to expose natives to scripts; __ natives stay reachable via calogCall but are not exposed).
// Safe only while the registry is frozen -- i.e. before any context starts.
void calogForEach(CalogT *calog, void (*visit)(const CalogEntryT *entry, void *ud), void *ud);

View file

@ -473,7 +473,7 @@ static int jsResolveOwnProperty(JSContext *ctx, JSPropertyDescriptor *desc, JSVa
return 0;
}
calogValueNil(&result);
status = calogCall(context->broker, "__exportResolve", &nameArg, 1, &result);
status = calogCall(context->broker, "__calogExportResolve", &nameArg, 1, &result);
calogValueFree(&nameArg);
if (status != calogOkE || result.type != calogFnE) {
calogValueFree(&result);

View file

@ -338,7 +338,7 @@ static int luaGlobalResolve(lua_State *L) {
return 1;
}
calogValueNil(&result);
status = calogCall(context->broker, "__exportResolve", &nameArg, 1, &result);
status = calogCall(context->broker, "__calogExportResolve", &nameArg, 1, &result);
calogValueFree(&nameArg);
if (status == calogOkE && result.type == calogFnE) {
if (luaPushValueDepth(L, &result, 0) != calogOkE) {

View file

@ -475,7 +475,7 @@ static s7_pointer s7ResolveUnbound(s7_scheme *sc, s7_pointer args) {
return s7_unspecified(sc);
}
calogValueNil(&result);
status = calogCall(context->broker, "__exportResolve", &nameArg, 1, &result);
status = calogCall(context->broker, "__calogExportResolve", &nameArg, 1, &result);
calogValueFree(&nameArg);
if (status == calogOkE && result.type == calogFnE) {
s7_let_set(sc, hookLet, s7_make_symbol(sc, "result"), s7FromValue(context, &result, 0));

View file

@ -485,7 +485,7 @@ static SQInteger squirrelResolveGet(HSQUIRRELVM v) {
return sq_throwerror(v, _SC("calog resolver: out of memory"));
}
calogValueNil(&result);
status = calogCall(context->broker, "__exportResolve", &nameArg, 1, &result);
status = calogCall(context->broker, "__calogExportResolve", &nameArg, 1, &result);
calogValueFree(&nameArg);
if (status == calogOkE && result.type == calogFnE) {
status = squirrelPushValueDepth(v, &result, 0);

View file

@ -1,6 +1,6 @@
// testExport.c -- exercises the export library. One Lua context publishes a function with
// export(); another Lua context calls it by its BARE name (via the globals __index resolver);
// a JavaScript context calls it via call. All calls run in the exporter's context.
// 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
@ -140,6 +140,7 @@ int main(void) {
CalogContextT *jsCaller;
CalogContextT *s7Caller;
CalogContextT *squirrelCaller;
CalogContextT *mbCaller;
CalogContextT *reloadV1;
CalogContextT *reloadV2;
int32_t i;
@ -165,7 +166,7 @@ int main(void) {
exporter = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(exporter,
"local function adder(a, b) return a + b end\n"
"export('adder', adder)\n"
"calogExport('adder', adder)\n"
"exporterReady()");
pumpUntilReady();
@ -173,13 +174,14 @@ int main(void) {
luaCaller = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(luaCaller,
"report(1, adder(2, 3))\n"
"report(2, call('adder', 10, 20))\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, call('adder', 100, 1));\n"
"report(3, calogCall('adder', 100, 1));\n"
"report(8, adder(9, 1));\n"
"done();");
@ -192,14 +194,24 @@ int main(void) {
"report(7, adder(50, 5));\n"
"done();");
pumpUntilDone(4);
// 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, "call reached the same exported function");
CHECK(atomic_load(&results[3]) == 101, "a JavaScript script called the exported Lua function via call");
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
@ -210,37 +222,38 @@ int main(void) {
atomic_store(&exporterReady, false);
calogContextEval(reloadV1,
"local function greet() return 111 end\n"
"export('greet', greet)\n"
"calogExport('greet', greet)\n"
"exporterReady()");
pumpUntilReady();
calogContextEval(luaCaller, "report(4, call('greet'))\n done()");
pumpUntilDone(5);
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"
"export('greet', greet)\n" // releases the old, dead-owner greet
"calogExport('greet', greet)\n" // releases the old, dead-owner greet
"exporterReady()");
pumpUntilReady();
calogContextEval(luaCaller, "report(5, call('greet'))\n done()");
pumpUntilDone(6);
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 __exportResolve
// (via the _G __index) -- the static registry must keep it safe, not a freed-state UAF.
// 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(7);
CHECK(atomic_load(&doneCount) >= 7, "resolving an unknown global after export shutdown is safe");
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);