// janetAdapter.c -- Janet engine adapter for the broker. // // Each calog context owns a thread-local Janet VM (janet_init/janet_deinit on the context thread; // the whole Janet state is a JANET_THREAD_LOCAL janet_vm, so N contexts are N independent VMs). // Values marshal both ways through CalogValueT. Janet has no separate integer engine type at the // script level worth preserving past 2^53, so calog ints cross as doubles (janet_wrap_number) -- // like Lua/JS/Wren -- and a number egresses through the canonical double classifier. Strings are // length-prefixed and binary-safe; nil round-trips. // // Callables ride on Janet's callable-abstract mechanism (JanetAbstractType.call): a Janet C function // carries no user data and so cannot bind a native name, so an exposed native (gNativeType) and a // foreign CalogFnT pushed in (gForeignType) are each a small abstract whose .call handler dispatches // (calogCall by name, or calogFnInvoke) and whose .gc handler frees the binding. A Janet function // handed OUT becomes a CalogFnT that janet_gcroots the function so the GC keeps it alive until the // callable is released (janetScriptRelease, on the owner thread). // // Error model: a failed native/marshal calls janet_panic (a longjmp out of the C frame back to the // nearest fiber). Because janet_panic never returns, every handler frees the heap CalogValueT // arguments it owns and copies any message into a stack buffer BEFORE panicking. Janet only collects // at VM instruction boundaries, so C-side marshalling never races the GC; the one value that must // survive across VM calls (a Janet function out) is explicitly gcrooted. #define _POSIX_C_SOURCE 200809L #include "janetAdapter.h" #include "janet.h" #include #include #include #include #include #include #include // Starting size of the buffer that captures what janet_dobytes reports (an error line plus a stack // trace). It grows as needed, so this only saves a few early reallocations. #define JANET_ERROR_CAPTURE_CAP 256 struct CalogJanetT { JanetTable *env; CalogT *broker; uint64_t ctxId; CalogLimitStateT *limits; // sandbox limits (mem cap + deadline), or NULL if unlimited JanetVM *janetVm; // this context's thread-local VM, for the watchdog's interrupt pthread_t watchdog; // deadline watchdog thread (only when time-limited) bool hasWatchdog; _Atomic bool wdStop; // asks the watchdog to exit }; // Per-allocation header: block size and the context it is charged to (NULL if allocated while // unlimited, e.g. the base VM before the cap is armed -- so its later free never drives memUsed // negative). Mirrors the mruby adapter's header allocator. typedef struct JanetAllocHdrT { size_t size; CalogLimitStateT *owner; } JanetAllocHdrT; // The limit state and VM of the Janet context on THIS thread (each owns its thread), plus a // one-shot guard so an over-cap run interrupts the VM only once. Janet's allocator is reached // through global macros with no VM argument, so it resolves the running context here. static _Thread_local CalogLimitStateT *gJanetLimits = NULL; static _Thread_local JanetVM *gJanetVm = NULL; static _Thread_local bool gJanetInterrupted = false; // Backs an exposed broker native: an abstract whose .call dispatches through calogCall by name. typedef struct JanetNativeT { CalogJanetT *context; char *name; } JanetNativeT; // Backs a foreign CalogFnT pushed into this VM: an abstract whose .call invokes the retained fn. typedef struct JanetForeignT { CalogJanetT *context; CalogFnT *fn; } JanetForeignT; // Backs a CalogFnT exported from this VM: the owning context and the gcrooted Janet function. typedef struct JanetScriptT { CalogJanetT *context; JanetFunction *function; } JanetScriptT; static void janetCharge(CalogLimitStateT *owner, int64_t delta); static Janet janetForeignCall(void *p, int32_t argc, Janet *argv); static int janetForeignGc(void *data, size_t len); static void janetFreeArgs(CalogValueT *args, int32_t argCount); static int32_t janetFromValue(CalogJanetT *context, const CalogValueT *value, Janet *out, int32_t depth); static int32_t janetMarshalArgs(CalogJanetT *context, int32_t argc, Janet *argv, CalogValueT **out); static Janet janetNativeCall(void *p, int32_t argc, Janet *argv); static int janetNativeGc(void *data, size_t len); static int32_t janetScriptInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static void janetScriptRelease(CalogFnT *callable); static int32_t janetToValue(CalogJanetT *context, Janet value, CalogValueT *out, int32_t depth); static void janetSealHostAccess(JanetTable *env); static void *janetWatchdog(void *arg); static int32_t janetWrapFunction(CalogJanetT *context, JanetFunction *function, CalogFnT **out); // Charge `delta` bytes against `owner` (non-NULL) and, the first time it crosses the cap this run, // interrupt the VM so the bytecode loop suspends at its next back-jump or call. Never refuses (Janet // exit(1)s on a NULL allocation), so the bound is one-allocation granular. memUsed clamps at zero. static void janetCharge(CalogLimitStateT *owner, int64_t delta) { owner->memUsed += delta; if (owner->memUsed < 0) { owner->memUsed = 0; } if (owner->memCap > 0 && owner->memUsed > owner->memCap && !gJanetInterrupted && gJanetVm != NULL) { gJanetInterrupted = true; janet_interpreter_interrupt(gJanetVm); } } // Janet's allocator, routed here from the janet.h macros. Prepends a {size, owner} header so free // and realloc can uncharge exactly; charges the running context (NULL owner when unlimited). void *calogJanetMalloc(size_t size) { JanetAllocHdrT *base; CalogLimitStateT *owner; size_t total; total = sizeof(JanetAllocHdrT) + size; base = (JanetAllocHdrT *)malloc(total); if (base == NULL) { return NULL; } owner = (gJanetLimits != NULL && gJanetLimits->memCap > 0) ? gJanetLimits : NULL; base->size = total; base->owner = owner; if (owner != NULL) { janetCharge(owner, (int64_t)total); } return (void *)(base + 1); } void *calogJanetCalloc(size_t nmemb, size_t size) { JanetAllocHdrT *base; CalogLimitStateT *owner; size_t data; size_t total; if (size != 0 && nmemb > (SIZE_MAX - sizeof(JanetAllocHdrT)) / size) { return NULL; // overflow } data = nmemb * size; total = sizeof(JanetAllocHdrT) + data; base = (JanetAllocHdrT *)malloc(total); if (base == NULL) { return NULL; } memset((void *)(base + 1), 0, data); owner = (gJanetLimits != NULL && gJanetLimits->memCap > 0) ? gJanetLimits : NULL; base->size = total; base->owner = owner; if (owner != NULL) { janetCharge(owner, (int64_t)total); } return (void *)(base + 1); } void *calogJanetRealloc(void *ptr, size_t size) { JanetAllocHdrT *base; JanetAllocHdrT *grown; size_t oldTotal; size_t newTotal; if (ptr == NULL) { return calogJanetMalloc(size); } base = ((JanetAllocHdrT *)ptr) - 1; oldTotal = base->size; newTotal = sizeof(JanetAllocHdrT) + size; grown = (JanetAllocHdrT *)realloc(base, newTotal); if (grown == NULL) { return NULL; } grown->size = newTotal; // a resize keeps the block's original owner if (grown->owner != NULL) { janetCharge(grown->owner, (int64_t)newTotal - (int64_t)oldTotal); } return (void *)(grown + 1); } void calogJanetFree(void *ptr) { JanetAllocHdrT *base; if (ptr == NULL) { return; } base = ((JanetAllocHdrT *)ptr) - 1; if (base->owner != NULL) { base->owner->memUsed -= (int64_t)base->size; if (base->owner->memUsed < 0) { base->owner->memUsed = 0; } } free(base); } // Shut every door out of the VM that does not go through a calog native. // // calog's contract is that a script reaches the host only through registered natives, where the // allow-list, the memory cap and the wall-clock budget can see it. Janet ships its own filesystem, // subprocess, socket, environment and FFI bindings, none of which pass through calogCall, so no // policy calog can express applies to them -- and ffi/ is worse than the rest together, since jitfn // executes attacker-supplied machine code inside this process. // // janet_sandbox is Janet's own mechanism and covers most of it: a guarded cfun asserts against the // mask and raises an ordinary Janet error, which the adapter reports like any other script failure. // Two bindings are NOT guarded and have to go by hand -- os/exit calls exit() directly (taking the // host down mid-run, around calogExit's ordered teardown) and os/cwd leaks the working directory. // Defining a name to nil is how janet_def removes an existing binding from the env table. // // Everything dropped has a gated equivalent: fs* for files, procRun for processes, net*/http* for // sockets, calogExit for ending the run. Compilation is deliberately left alone -- Janet scripts // need it and it reaches nothing outside the VM. static void janetSealHostAccess(JanetTable *env) { static const char *const removed[] = { "os/cwd", "os/exit" }; size_t index; janet_sandbox(JANET_SANDBOX_FS | JANET_SANDBOX_ENV | JANET_SANDBOX_FFI | JANET_SANDBOX_SUBPROCESS | JANET_SANDBOX_NET | JANET_SANDBOX_DYNAMIC_MODULES | JANET_SANDBOX_SIGNAL); for (index = 0; index < sizeof(removed) / sizeof(removed[0]); index++) { janet_def(env, removed[index], janet_wrap_nil(), NULL); } } // The watchdog: Janet has no in-loop hook, so a separate thread interrupts the VM // (janet_interpreter_interrupt is cross-thread-safe) for either reason a script must stop -- the // runtime was latched aborting, or a time-limited context ran past its deadline. It polls at a // coarse interval and exits promptly when asked (at context teardown). // // Every context gets one, not just time-limited ones: the abort watch is what lets Ctrl-C reach a // script that calls no natives at all, which nothing else can interrupt on this engine. It does NOT // call calogAbortRaise -- that marks the CALLING context and this is a foreign thread -- which is // why calogJanetRun asks calogAborting rather than calogAbortRaised. Janet can afford that looser // question because its own JANET_DO_ERROR_RUNTIME flag already separates a parse failure from a // script that actually ran, so a syntax error still reports mid-teardown. static void *janetWatchdog(void *arg) { CalogJanetT *context; CalogLimitStateT *limits; struct timespec tick; context = (CalogJanetT *)arg; limits = context->limits; tick.tv_sec = 0; tick.tv_nsec = 5 * 1000 * 1000; // 5 ms while (!atomic_load(&context->wdStop)) { if (calogAborting(context->broker)) { janet_interpreter_interrupt(context->janetVm); break; } if (limits != NULL && limits->deadlineMs != 0 && calogMonotonicMillis() >= limits->deadlineMs) { janet_interpreter_interrupt(context->janetVm); break; } nanosleep(&tick, NULL); } return NULL; } // Callable abstracts. Defined after the prototypes so the designated initializers can name the // handlers. Not registered with janet_register_abstract_type: calling and GC need no registration // (only marshalling would), and the pointer identity is enough to recognize a foreign value on the // way back out. static const JanetAbstractType gForeignType = { .name = "calog/foreign", .gc = janetForeignGc, .call = janetForeignCall }; static const JanetAbstractType gNativeType = { .name = "calog/native", .gc = janetNativeGc, .call = janetNativeCall }; int32_t calogJanetCreate(CalogJanetT **out, CalogT *broker, uint64_t ctxId, CalogLimitStateT *limits) { CalogJanetT *context; *out = NULL; context = (CalogJanetT *)calloc(1, sizeof(*context)); if (context == NULL) { return calogErrOomE; } janet_init(); // The core env is memoized and gcrooted internally by Janet, so the defs installed by // calogJanetExpose survive collection for the VM's lifetime. context->env = janet_core_env(NULL); janetSealHostAccess(context->env); context->broker = broker; context->ctxId = ctxId; context->limits = limits; context->janetVm = janet_local_vm(); // this context thread's VM, for the watchdog's interrupt // Arm the limits AFTER the base VM + core env are built, so the base runtime is neither charged // nor deadline-checked (the memory cap must exceed the base runtime, as with Lua). gJanetVm is // always set so the allocator can interrupt on an over-cap allocation. gJanetVm = context->janetVm; if (limits != NULL && limits->memCap > 0) { gJanetLimits = limits; } // Started for every context: the watchdog carries the abort watch, which an unlimited context // needs just as much as a limited one (see janetWatchdog). atomic_store(&context->wdStop, false); if (pthread_create(&context->watchdog, NULL, janetWatchdog, context) == 0) { context->hasWatchdog = true; } *out = context; return calogOkE; } void calogJanetDestroy(CalogJanetT *context) { if (context == NULL) { return; } // Stop and join the deadline watchdog before tearing down the VM it interrupts. if (context->hasWatchdog) { atomic_store(&context->wdStop, true); pthread_join(context->watchdog, NULL); } // This thread's allocator pointers alias the context's limit state, freed with the context. gJanetLimits = NULL; gJanetVm = NULL; // Tears down the thread-local VM, firing every remaining abstract's .gc handler: exposed // natives free their name, live foreign callables release their CalogFnT. janet_deinit(); free(context); } int32_t calogJanetExport(CalogJanetT *context, const char *name, CalogFnT **out) { Janet resolved; JanetBindingType binding; *out = NULL; resolved = janet_wrap_nil(); binding = janet_resolve(context->env, janet_csymbol(name), &resolved); if (binding == JANET_BINDING_NONE || janet_type(resolved) != JANET_FUNCTION) { return calogErrNotFoundE; } return janetWrapFunction(context, janet_unwrap_function(resolved), out); } int32_t calogJanetExpose(CalogJanetT *context, const char *name) { JanetNativeT *native; char *copy; if (calogLookup(context->broker, name) == NULL) { return calogErrNotFoundE; } // Copy the name first so the abstract is fully formed the instant it exists (its .gc handler // frees this copy). janet_abstract aborts rather than returns NULL on OOM. copy = strdup(name); if (copy == NULL) { return calogErrOomE; } native = (JanetNativeT *)janet_abstract(&gNativeType, sizeof(JanetNativeT)); native->context = context; native->name = copy; janet_def(context->env, name, janet_wrap_abstract(native), NULL); return calogOkE; } // Call target for a foreign CalogFnT pushed into this VM (gForeignType.call). p is the abstract's // JanetForeignT payload; the arguments are argv[0..argc). static Janet janetForeignCall(void *p, int32_t argc, Janet *argv) { JanetForeignT *foreign; CalogJanetT *context; CalogValueT *args; CalogValueT result; Janet out; int32_t status; foreign = (JanetForeignT *)p; context = foreign->context; args = NULL; if (janetMarshalArgs(context, argc, argv, &args) != calogOkE) { janet_panic("failed to marshal a function-value argument"); } status = calogFnInvoke(foreign->fn, args, argc, &result); janetFreeArgs(args, argc); if (status != calogOkE) { char message[CALOG_ERR_MSG_CAP]; snprintf(message, sizeof(message), "%s", (result.type == calogStringE) ? result.as.s.bytes : "function value failed"); calogValueFree(&result); janet_panic(message); } status = janetFromValue(context, &result, &out, 0); calogValueFree(&result); if (status != calogOkE) { janet_panic("failed to marshal the function-value result"); } return out; } static int janetForeignGc(void *data, size_t len) { JanetForeignT *foreign; (void)len; foreign = (JanetForeignT *)data; calogFnRelease(foreign->fn); return 0; } static void janetFreeArgs(CalogValueT *args, int32_t argCount) { int32_t index; if (args == NULL) { return; } for (index = 0; index < argCount; index++) { calogValueFree(&args[index]); } free(args); } static int32_t janetFromValue(CalogJanetT *context, const CalogValueT *value, Janet *out, int32_t depth) { *out = janet_wrap_nil(); if (depth >= CALOG_MAX_DEPTH) { return calogErrDepthE; } switch (value->type) { case calogNilE: return calogOkE; case calogBoolE: *out = janet_wrap_boolean(value->as.b ? 1 : 0); return calogOkE; case calogIntE: // Doubles, not wrap_integer: preserve calog's 64-bit ints up to 2^53 (the shared // ceiling of every double-number engine). *out = janet_wrap_number((double)value->as.i); return calogOkE; case calogRealE: *out = janet_wrap_number(value->as.r); return calogOkE; case calogStringE: *out = janet_wrap_string(janet_string((const uint8_t *)value->as.s.bytes, (int32_t)value->as.s.length)); return calogOkE; case calogAggE: { CalogAggT *aggregate; int64_t index; int32_t status; Janet child; aggregate = value->as.agg; // No GC runs while this executes (no bytecode), so the partially built container and // the child in hand are safe without rooting. if (calogAggIsKeyed(aggregate)) { JanetTable *table; table = janet_table((int32_t)(aggregate->arrayCount + aggregate->pairCount)); for (index = 0; index < aggregate->arrayCount; index++) { status = janetFromValue(context, &aggregate->array[index], &child, depth + 1); if (status != calogOkE) { return status; } janet_table_put(table, janet_wrap_number((double)index), child); } for (index = 0; index < aggregate->pairCount; index++) { Janet key; status = janetFromValue(context, &aggregate->pairs[index].key, &key, depth + 1); if (status != calogOkE) { return status; } status = janetFromValue(context, &aggregate->pairs[index].value, &child, depth + 1); if (status != calogOkE) { return status; } janet_table_put(table, key, child); } *out = janet_wrap_table(table); return calogOkE; } JanetArray *array; array = janet_array((int32_t)aggregate->arrayCount); for (index = 0; index < aggregate->arrayCount; index++) { status = janetFromValue(context, &aggregate->array[index], &child, depth + 1); if (status != calogOkE) { return status; } janet_array_push(array, child); } *out = janet_wrap_array(array); return calogOkE; } case calogFnE: { JanetForeignT *foreign; foreign = (JanetForeignT *)janet_abstract(&gForeignType, sizeof(JanetForeignT)); foreign->context = context; foreign->fn = value->as.fn; calogFnRetain(value->as.fn); *out = janet_wrap_abstract(foreign); return calogOkE; } } return calogErrTypeE; } static int32_t janetMarshalArgs(CalogJanetT *context, int32_t argc, Janet *argv, CalogValueT **out) { CalogValueT *args; int32_t index; int32_t status; *out = NULL; if (argc <= 0) { return calogOkE; } args = (CalogValueT *)calloc((size_t)argc, sizeof(CalogValueT)); if (args == NULL) { return calogErrOomE; } for (index = 0; index < argc; index++) { status = janetToValue(context, argv[index], &args[index], 0); if (status != calogOkE) { int32_t cleanup; for (cleanup = 0; cleanup < index; cleanup++) { calogValueFree(&args[cleanup]); } free(args); return status; } } *out = args; return calogOkE; } // Call target for an exposed broker native (gNativeType.call). p is the abstract's JanetNativeT // payload; the arguments are argv[0..argc). static Janet janetNativeCall(void *p, int32_t argc, Janet *argv) { JanetNativeT *native; CalogJanetT *context; CalogValueT *args; CalogValueT result; Janet out; int32_t status; native = (JanetNativeT *)p; context = native->context; args = NULL; if (janetMarshalArgs(context, argc, argv, &args) != calogOkE) { janet_panic("failed to marshal a native argument"); } status = calogCall(context->broker, native->name, args, argc, &result); janetFreeArgs(args, argc); if (status != calogOkE) { char message[CALOG_ERR_MSG_CAP]; snprintf(message, sizeof(message), "%s", (result.type == calogStringE) ? result.as.s.bytes : "native call failed"); calogValueFree(&result); janet_panic(message); } status = janetFromValue(context, &result, &out, 0); calogValueFree(&result); if (status != calogOkE) { janet_panic("failed to marshal the native result"); } return out; } static int janetNativeGc(void *data, size_t len) { JanetNativeT *native; (void)len; native = (JanetNativeT *)data; free(native->name); return 0; } static int32_t janetScriptInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { JanetScriptT *holder; CalogJanetT *context; Janet *argv; Janet out; JanetSignal signal; int32_t index; int32_t status; holder = (JanetScriptT *)userData; context = holder->context; calogValueNil(result); argv = NULL; if (argCount > 0) { argv = (Janet *)calloc((size_t)argCount, sizeof(Janet)); if (argv == NULL) { return calogFail(result, calogErrOomE, "out of memory invoking janet callback"); } } // No GC runs while argv is built (no bytecode); janet_pcall copies argv into the fiber stack // before running, so the values are rooted for the duration of the call. for (index = 0; index < argCount; index++) { status = janetFromValue(context, &args[index], &argv[index], 0); if (status != calogOkE) { free(argv); return calogFail(result, status, "failed to marshal a janet callback argument"); } } out = janet_wrap_nil(); signal = janet_pcall(holder->function, argCount, argv, &out, NULL); free(argv); if (signal != JANET_SIGNAL_OK) { const char *message; // A limit trip during the callback (allocator interrupt or the watchdog's deadline) must // retire the context here too, like the eval path -- else the interrupt errors the callback // but the context runs on. calogJanetRun's reset of gJanetInterrupted covers this nested call. CalogLimitStateT *limits; limits = context->limits; if (limits != NULL) { uint64_t now; now = calogMonotonicMillis(); if (gJanetInterrupted || (limits->deadlineMs != 0 && now >= limits->deadlineMs)) { calogCurrentRetire(); } } message = (janet_type(out) == JANET_STRING) ? (const char *)janet_unwrap_string(out) : "janet callback failed"; return calogFail(result, calogErrArgE, message); } return janetToValue(context, out, result, 0); } static void janetScriptRelease(CalogFnT *callable) { JanetScriptT *holder; holder = (JanetScriptT *)calogFnUserData(callable); // Runs on the owner thread, so the thread-local gc root list is this VM's. janet_gcunroot(janet_wrap_function(holder->function)); free(holder); } static int32_t janetToValue(CalogJanetT *context, Janet value, CalogValueT *out, int32_t depth) { calogValueNil(out); if (depth >= CALOG_MAX_DEPTH) { return calogErrDepthE; } switch (janet_type(value)) { case JANET_NIL: return calogOkE; case JANET_BOOLEAN: calogValueBool(out, janet_unwrap_boolean(value) != 0); return calogOkE; case JANET_NUMBER: // Canonical double classification: an exact integer becomes an int, else a real. calogValueFromDouble(out, janet_unwrap_number(value)); return calogOkE; case JANET_STRING: case JANET_SYMBOL: case JANET_KEYWORD: { JanetString bytes; bytes = janet_unwrap_string(value); return calogValueString(out, (const char *)bytes, (int64_t)janet_string_length(bytes)); } case JANET_BUFFER: { JanetBuffer *buffer; buffer = janet_unwrap_buffer(value); return calogValueString(out, (const char *)buffer->data, (int64_t)buffer->count); } case JANET_ARRAY: { JanetArray *array; CalogAggT *aggregate; int32_t index; int32_t status; array = janet_unwrap_array(value); status = calogAggCreate(&aggregate, calogListE); if (status != calogOkE) { return status; } for (index = 0; index < array->count; index++) { CalogValueT element; status = janetToValue(context, array->data[index], &element, depth + 1); if (status != calogOkE) { calogAggFree(aggregate); return status; } status = calogAggPush(aggregate, &element); if (status != calogOkE) { calogValueFree(&element); calogAggFree(aggregate); return status; } } calogValueAgg(out, aggregate); return calogOkE; } case JANET_TUPLE: { const Janet *tuple; CalogAggT *aggregate; int32_t count; int32_t index; int32_t status; tuple = janet_unwrap_tuple(value); count = janet_tuple_length(tuple); status = calogAggCreate(&aggregate, calogListE); if (status != calogOkE) { return status; } for (index = 0; index < count; index++) { CalogValueT element; status = janetToValue(context, tuple[index], &element, depth + 1); if (status != calogOkE) { calogAggFree(aggregate); return status; } status = calogAggPush(aggregate, &element); if (status != calogOkE) { calogValueFree(&element); calogAggFree(aggregate); return status; } } calogValueAgg(out, aggregate); return calogOkE; } case JANET_TABLE: { JanetTable *table; CalogAggT *aggregate; int32_t index; int32_t status; table = janet_unwrap_table(value); status = calogAggCreate(&aggregate, calogMapE); if (status != calogOkE) { return status; } for (index = 0; index < table->capacity; index++) { CalogValueT key; CalogValueT val; if (janet_checktype(table->data[index].key, JANET_NIL)) { continue; } status = janetToValue(context, table->data[index].key, &key, depth + 1); if (status != calogOkE) { calogAggFree(aggregate); return status; } status = janetToValue(context, table->data[index].value, &val, depth + 1); if (status != calogOkE) { calogValueFree(&key); calogAggFree(aggregate); return status; } status = calogAggSet(aggregate, &key, &val); if (status != calogOkE) { calogValueFree(&key); calogValueFree(&val); calogAggFree(aggregate); return status; } } calogValueAgg(out, aggregate); return calogOkE; } case JANET_STRUCT: { const JanetKV *st; CalogAggT *aggregate; int32_t capacity; int32_t index; int32_t status; st = janet_unwrap_struct(value); capacity = janet_struct_capacity(st); status = calogAggCreate(&aggregate, calogMapE); if (status != calogOkE) { return status; } for (index = 0; index < capacity; index++) { CalogValueT key; CalogValueT val; if (janet_checktype(st[index].key, JANET_NIL)) { continue; } status = janetToValue(context, st[index].key, &key, depth + 1); if (status != calogOkE) { calogAggFree(aggregate); return status; } status = janetToValue(context, st[index].value, &val, depth + 1); if (status != calogOkE) { calogValueFree(&key); calogAggFree(aggregate); return status; } status = calogAggSet(aggregate, &key, &val); if (status != calogOkE) { calogValueFree(&key); calogValueFree(&val); calogAggFree(aggregate); return status; } } calogValueAgg(out, aggregate); return calogOkE; } case JANET_FUNCTION: { CalogFnT *callable; int32_t status; status = janetWrapFunction(context, janet_unwrap_function(value), &callable); if (status != calogOkE) { return status; } calogValueFn(out, callable); return calogOkE; } case JANET_ABSTRACT: { void *abstract; abstract = janet_unwrap_abstract(value); // Our own foreign callable coming back out: return the wrapped fn, retained. if (janet_abstract_type(abstract) == &gForeignType) { JanetForeignT *foreign; foreign = (JanetForeignT *)abstract; calogFnRetain(foreign->fn); calogValueFn(out, foreign->fn); return calogOkE; } return calogOkE; // a foreign abstract has no calog analogue; stays nil } default: return calogOkE; // fiber, cfunction, pointer: no analogue; stays nil } } // Wrap a Janet function as a host-visible CalogFnT. The function is pinned as a GC root until the // callable's last reference drops (janetScriptRelease unroots it). static int32_t janetWrapFunction(CalogJanetT *context, JanetFunction *function, CalogFnT **out) { JanetScriptT *holder; int32_t status; *out = NULL; holder = (JanetScriptT *)malloc(sizeof(*holder)); if (holder == NULL) { return calogErrOomE; } holder->context = context; holder->function = function; janet_gcroot(janet_wrap_function(function)); status = calogFnCreate(out, context->broker, janetScriptInvoke, holder, janetScriptRelease, context->ctxId); if (status != calogOkE) { janet_gcunroot(janet_wrap_function(function)); free(holder); return status; } return calogOkE; } int32_t calogJanetRun(CalogJanetT *context, const char *source) { JanetBuffer *captured; Janet errKey; Janet out; bool aborted; int32_t status; int flags; gJanetInterrupted = false; // one-shot per run: a fresh over-cap interrupt may fire again // Unlike every other engine, Janet reports a failed script itself, from inside janet_dobytes // (the error line plus a stack trace, through janet_eprintf). That is the right output for a // script that failed and exactly the wrong output for one that calogExit unwound on purpose, so // capture it and decide afterwards. janet_eprintf honors the "err" dynamic binding; the binding // must go in the TOP dyn table, because janet_dobytes prints once janet_continue has returned // and no fiber is current. A script's own (eprint) still goes straight to stderr -- inside a // fiber the lookup finds this env's bindings, not the top table. captured = janet_buffer(JANET_ERROR_CAPTURE_CAP); errKey = janet_ckeywordv("err"); // Janet does not mark the top dyn table, so an entry parked there is invisible to the collector: // root both halves by hand or a collection inside the script frees them under the binding. janet_gcroot(errKey); janet_gcroot(janet_wrap_buffer(captured)); janet_setdyn("err", janet_wrap_buffer(captured)); out = janet_wrap_nil(); flags = janet_dobytes(context->env, (const uint8_t *)source, (int32_t)strlen(source), "calog", &out); janet_setdyn("err", janet_wrap_nil()); // calogExit (calogAbortAll) unwinding this script is not a failure: drop what Janet reported and // hand back success, so the runner neither prints a trace nor retires the context as broken. // Only a RUNTIME trip can be an abort -- a parse or compile error still reports, mid-teardown or // not, since no native ran to ask for one. aborted = ((flags & JANET_DO_ERROR_RUNTIME) != 0) && calogAborting(context->broker); status = (flags != 0 && !aborted) ? calogErrArgE : calogOkE; if (flags != 0) { // If a sandbox limit tripped -- the allocator interrupted the VM this run (gJanetInterrupted, // which survives a GC that later drops memUsed back under the cap) or the wall-clock deadline // passed -- retire the context so it is torn down rather than run again past its budget. Our // only interrupt sources are limit violations, so any trip is a kill. CalogLimitStateT *limits; limits = context->limits; if (limits != NULL) { uint64_t now; now = calogMonotonicMillis(); if (gJanetInterrupted || (limits->deadlineMs != 0 && now >= limits->deadlineMs)) { calogCurrentRetire(); } } if (status != calogOkE) { // Relayed verbatim, so a failing janet script reads exactly as it did before. fwrite(captured->data, 1, (size_t)captured->count, stderr); } } janet_gcunroot(janet_wrap_buffer(captured)); janet_gcunroot(errKey); return status; }