# calog **C: Any Language, One Gateway.** calog embeds scripting into a C or C++ program. You register your native C functions **once**, and they become callable from **any** embedded engine — Lua, JavaScript, Scheme, Squirrel, Wren, Berry, or MY-BASIC — with values passed through a single canonical type. Add a new scripting language to your app by linking one more archive; your native functions don't change. ```c #include "calog.h" CalogT *calog = calogCreate(); calogRegister(calog, "hostLog", hostLog, NULL); // your C function, once CalogContextT *ctx = calogContextOpen(calog, &calogJsEngine); calogContextEval(ctx, "hostLog('hello from JavaScript, ' + 6 * 7)"); ``` Swap `&calogJsEngine` for `&calogLuaEngine`, `&calogSquirrelEngine`, `&calogS7Engine`, `&calogWrenEngine`, `&calogBerryEngine`, or `&calogMyBasicEngine` — nothing else changes. --- ## Why calog - **One API, many languages.** Register a native once; call it from every engine. `calog.h` is the entire public surface. - **One value type.** Everything crosses the boundary as a `CalogValueT` — nil, bool, int, real, binary-safe string, a hybrid list/map aggregate, or a script function value. No per-engine glue in your code. - **Natives run on your thread, serialized.** A script calling a native is marshalled to the host thread and run there during `calogPump()`, one at a time — so your C code is never called concurrently and needs **no locking**. (An opt-in escape hatch runs a native inline on the script's thread when you want it.) - **Fire-and-forget scripts.** Scripts run on their own context threads; the host stays responsive and drives everything from its own loop. - **Callbacks both ways.** A script can hand you a function value to keep and call later (routed back to the engine that owns it); and you can hand a native to a script as a callable value with `calogFnFromNative` (every engine but MY-BASIC). - **Many runtimes.** Independent `CalogT` runtimes coexist in one process; one host thread can drive several. - **Load by filename.** `calogContextLoad(calog, "config")` finds `config.lua` / `config.js` / `config.nut` / `config.bas` and runs it on the matching engine. - **Reproducible & rigorous.** Every engine is vendored and built from source (no system packages). The core is warning-clean on gcc **and** clang (`-Wall -Wextra -Werror -Wconversion -Wsign-conversion`) and runs clean under AddressSanitizer, UBSan, and ThreadSanitizer. ## Engines | Engine | Vtable | Extension | Notes | |---------------|------------------------|-----------|-----------------------------------------| | Lua 5.4 | `calogLuaEngine` | `.lua` | | | JavaScript | `calogJsEngine` | `.js` | QuickJS-ng (ES2023+, BigInt → int64) | | Squirrel 3.2 | `calogSquirrelEngine` | `.nut` | C++ VM | | MY-BASIC | `calogMyBasicEngine` | `.bas` | interpreters serialize at load | | Scheme | `calogS7Engine` | `.scm` | s7; 64-bit ints | | Wren | `calogWrenEngine` | `.wren` | doubles only; call via `Calog.call(…)` | | Berry | `calogBerryEngine` | `.be` | scalars + callbacks | A native can return a keyed `CalogValueT` record and **every** engine reads its fields with native syntax (`user.name`, `user["name"]`, `(user "name")`), and a script can hand a list/map *back* to C on **every** engine. (Wren's C API can't enumerate a map's keys, so calog carries a small documented patch to the vendored Wren that adds one.) A host function value also crosses *into* a script on every engine but MY-BASIC (which has no first-class callable values). You can also bring your own: `CalogEngineT` is a public four-function vtable. --- ## Build Requirements: a POSIX system (pthreads), `gcc` or `clang`, and GNU `make`. Developed on Linux; macOS should work, Windows via WSL. ```sh make # builds lib/libcalog.a, the per-engine archives, tests, and the example make test # runs the full suite under AddressSanitizer + UBSan ``` The engines are vendored under `vendor/` and compiled from source, so the build is self-contained. ### Linking your program Link against `lib/libcalog.a` plus the archive for **each engine you actually use** — unused engines (and their vendored runtimes) stay out of your binary. ```sh # a JavaScript-only host: cc -Isrc myhost.c lib/libcalog.a lib/libquickjs.a -pthread -lm -o myhost ``` Per-engine link needs: | Engine | Archive | Extra libs (Linux) | |----------|---------------------|--------------------| | Lua | `lib/liblua.a` | `-ldl -lm` | | JS | `lib/libquickjs.a` | `-lm` | | Squirrel | `lib/libsquirrel.a` | `-lstdc++ -lm` | | MY-BASIC | `lib/libmybasic.a` | `-lm` | | Scheme | `lib/libs7.a` | `-ldl -lm` | | Wren | `lib/libwren.a` | `-lm` | | Berry | `lib/libberry.a` | `-lm` | Everything links with `-pthread`. --- ## A complete example ```c #include "calog.h" #include #include // A native the host provides. It runs on the host thread during calogPump, serialized // with every other native, so it can touch host state without locking. static int32_t hostLog(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "hostLog expects one string"); } printf("[host] %s\n", args[0].as.s.bytes); return calogOkE; } int main(void) { CalogT *calog = calogCreate(); calogRegister(calog, "hostLog", hostLog, NULL); CalogContextT *ctx = calogContextOpen(calog, &calogJsEngine); calogContextEval(ctx, "hostLog('hello from JavaScript, ' + (6 * 7))"); // The script runs on its own thread and calls hostLog; the host services those // calls by pumping in its own loop. (A real app pumps inside its main loop.) struct timespec tick = { 0, 1000000 }; // 1 ms for (int i = 0; i < 50; i++) { calogPump(calog); nanosleep(&tick, NULL); } calogContextClose(ctx); calogDestroy(calog); return 0; } ``` See [`examples/embed.c`](examples/embed.c) for this, ready to build (`make` produces `bin/embed`). --- ## How it works ### The canonical value Every value crossing the C/script boundary is a `CalogValueT`: ```c typedef enum { calogNilE, calogBoolE, calogIntE, calogRealE, calogStringE, calogAggE, calogFnE } CalogTypeE; struct CalogValueT { CalogTypeE type; union { bool b; int64_t i; double r; CalogStringT s; CalogAggT *agg; CalogFnT *fn; } as; }; ``` Build them with the constructors, and free owned values you're done with: ```c CalogValueT v; calogValueInt(&v, 42); calogValueString(&v, "binary\0safe", 11); // length-prefixed, NUL-safe // ... calogValueBool / calogValueReal / calogValueAgg / calogValueFn ... calogValueFree(&v); ``` `CalogAggT` is a hybrid **list + map** (`calogAggPush`, `calogAggSet`, `calogAggGet`), so it round-trips onto engines that model sequences and dictionaries differently. Strings are binary-safe (length-prefixed, always NUL-terminated for convenience). ### Registering natives ```c // Runs on the HOST thread (serialized). This is what you want almost always. calogRegister(calog, "add", add, NULL); // Escape hatch: runs INLINE on the calling script's thread. Faster (no host hop), // but YOU guarantee it is thread-safe. calogRegisterInline(calog, "clockNow", clockNow, NULL); ``` A native has one signature and reports failure through `result`: ```c static int32_t add(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; 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; } ``` Every registered native is automatically visible in every context you open. ### Contexts, scripts, and the pump A **context** is one engine instance on its own thread. `calogContextEval` is **fire-and-forget**: it queues the script and returns immediately. ```c CalogContextT *ctx = calogContextOpen(calog, &calogLuaEngine); calogContextEval(ctx, "hostLog('running')"); // returns now; script runs on ctx's thread ``` Because natives are serviced on the host thread, the host must **pump**: ```c for (;;) { calogPump(calog); // runs any pending native calls here, then returns do_other_host_work(); } ``` Script results come back by **calling natives** — `calogContextEval` doesn't return a value. Script errors are delivered to an optional handler on the host thread during `calogPump`: ```c static void onError(uint64_t contextId, const char *message, void *userData) { fprintf(stderr, "script %llu failed: %s\n", (unsigned long long)contextId, message); } calogSetErrorHandler(calog, onError, NULL); // default logs to stderr ``` ### Callbacks: script functions as values A script can pass you a function. Keep it (retain), call it later, release it when done — calog runs the call on the engine that owns it and returns the result to you: ```c static CalogFnT *savedCb = NULL; static int32_t onEvent(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogFnE) { return calogFail(result, calogErrArgE, "onEvent expects a function"); } calogFnRetain(args[0].as.fn); // keep it beyond this call savedCb = args[0].as.fn; return calogOkE; } // ...later, from the host: CalogValueT arg, out; calogValueInt(&arg, 42); calogFnInvoke(savedCb, &arg, 1, &out); // routed to the script's thread, result returned here calogValueFree(&out); calogValueFree(&arg); calogFnRelease(savedCb); ``` It works the other way too: wrap one of your natives as a function value with `calogFnFromNative` and return it from a native, and the script gets a callable it can invoke (which routes back to your host thread). Every engine but MY-BASIC supports this. ```c static int32_t getAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogFnT *fn; (void)args; (void)argCount; (void)userData; calogValueNil(result); calogFnFromNative(&fn, calog, add, NULL); // 'add' is one of your natives calogValueFn(result, fn); // the script can now call getAdder()(2, 3) return calogOkE; } ``` ### Loading a script by filename Register the engines you want searchable (order is priority), then load by base name: ```c calogRegisterEngine(calog, &calogLuaEngine); calogRegisterEngine(calog, &calogJsEngine); CalogContextT *ctx = calogContextLoad(calog, "config"); // tries config.lua, then config.js ``` Or let the build decide which engines exist. Compile the calling file with `-DCALOG_WITH_LUA -DCALOG_WITH_JS …` and call the header-inline helper — it registers exactly the engines you linked, and references no others: ```c calogRegisterBuiltinEngines(calog); CalogContextT *ctx = calogContextLoad(calog, "config"); ``` ### Multiple runtimes A `CalogT` is a self-contained runtime; you can create several in one process. Each is driven by a host thread, and one thread may drive several by pumping each in turn. Keep values and callbacks within a single runtime — they don't cross between runtimes. --- ## API at a glance ```c // runtime CalogT *calogCreate(void); void calogDestroy(CalogT *); void calogPump(CalogT *); // service native calls here void calogSetErrorHandler(CalogT *, CalogErrorFnT, void *); // natives int32_t calogRegister(CalogT *, const char *name, CalogNativeFnT, void *userData); // host thread int32_t calogRegisterInline(CalogT *, const char *name, CalogNativeFnT, void *userData); // caller thread int32_t calogCall(CalogT *, const char *name, CalogValueT *args, int32_t n, CalogValueT *result); int32_t calogFail(CalogValueT *result, int32_t status, const char *message); const char *calogTypeName(CalogTypeE); // values void calogValueNil/Bool/Int/Real/Agg/Fn(...); int32_t calogValueString(CalogValueT *, const char *bytes, int64_t length); int32_t calogValueCopy(CalogValueT *dst, const CalogValueT *src); void calogValueMove(CalogValueT *dst, CalogValueT *src); void calogValueFree(CalogValueT *); bool calogValueEquals(const CalogValueT *, const CalogValueT *); // aggregates (hybrid list + map) int32_t calogAggCreate(CalogAggT **, CalogKindE); void calogAggFree(CalogAggT *); int32_t calogAggPush(CalogAggT *, CalogValueT *value); int32_t calogAggSet(CalogAggT *, CalogValueT *key, CalogValueT *value); CalogValueT *calogAggGet(CalogAggT *, const CalogValueT *key); // function values (script callbacks) int32_t calogFnInvoke(CalogFnT *, CalogValueT *args, int32_t n, CalogValueT *result); void calogFnRetain(CalogFnT *); void calogFnRelease(CalogFnT *); // contexts + engines CalogContextT *calogContextOpen(CalogT *, const CalogEngineT *); void calogRegisterEngine(CalogT *, const CalogEngineT *); CalogContextT *calogContextLoad(CalogT *, const char *baseFileName); int32_t calogContextEval(CalogContextT *, const char *source); // fire-and-forget void calogContextClose(CalogContextT *); uint64_t calogContextId(const CalogContextT *); uint64_t calogCurrentId(void); // 0 on the host thread CalogT *calogCurrent(void); ``` Statuses are `CalogStatusE` (`calogOkE`, `calogErrArgE`, `calogErrOomE`, …); `calogOkE` is 0. --- ## Testing ```sh make test # full suite under AddressSanitizer + UBSan make tsan # ThreadSanitizer: actor core + Lua make tsansq # ThreadSanitizer: Squirrel make tsanjs # ThreadSanitizer: JavaScript make tsanmb # ThreadSanitizer: MY-BASIC ``` The ThreadSanitizer targets run under `setarch -R` (ASLR off) so TSan's shadow allocator is happy on all kernels. ## Project layout ``` src/ core (broker, values, actor) + one subdir per engine (lua, js, squirrel, mybasic) calog.h the entire public API tests/ the test programs examples/ embed.c — a minimal host vendor/ vendored engine sources, built from source design.md the full design rationale and internals ``` ## Design [`design.md`](design.md) is the long-form record: the canonical value model, the actor/threading design, the function-value lifecycle, per-engine notes, and the build system. ## License calog is released under the [MIT License](LICENSE.md). The vendored engines (Lua, Duktape, Squirrel, MY-BASIC) each retain their own MIT licenses — see [`LICENSE.md`](LICENSE.md) for the third-party notices.