| examples | ||
| libs | ||
| src | ||
| tests | ||
| vendor | ||
| .gitattributes | ||
| .gitignore | ||
| design.md | ||
| LICENSE.md | ||
| Makefile | ||
| README.md | ||
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.
#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.his 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
CalogTruntimes coexist in one process; one host thread can drive several. - Load by filename.
calogContextLoad(calog, "config")findsconfig.lua/config.js/config.nut/config.basand 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.
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.
# 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
#include "calog.h"
#include <stdio.h>
#include <time.h>
// 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 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:
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:
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
// 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:
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.
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:
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:
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:
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.
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:
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:
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
// 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
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 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. The vendored engines (Lua,
Duktape, Squirrel, MY-BASIC) each retain their own MIT licenses — see
LICENSE.md for the third-party notices.