// berryAdapter.c -- Berry engine adapter for the broker. // // Every exposed native is a Berry native closure carrying two upvalues -- the context // (a comptr) and the native's name -- so the trampoline recovers its binding via // be_getupval, marshals the Berry arguments to CalogValueT, and dispatches through // calogCall (honoring the actor route hook). Marshalling covers scalars and binary-safe // strings; aggregates cross both ways (out as a Berry list/map instance -- a host record // materializes as a map -- and a script's list/map is read back by iterating its raw // container). Functions cross both ways too: a Berry function out becomes a refcounted // CalogFnT kept reachable by a hidden global (a GC root); a foreign CalogFnT pushed in // becomes a native closure the script calls directly (tracked on the context and released // at destroy, since Berry has no per-value finalizer). // // Error model: on failure the trampoline raises a Berry exception (be_raise, which // longjmps out of the VM) after releasing any CalogValueT it owns. #define _POSIX_C_SOURCE 200809L #include "berryAdapter.h" #include "berry.h" #include #include #include #include #define BERRY_REF_CAP 32 #define BERRY_FOREIGN_INITIAL 8 struct CalogBerryT { bvm *vm; CalogT *broker; uint64_t ctxId; CalogLimitStateT *limits; // sandbox limits (mem cap + deadline), or NULL if unlimited int32_t nextRef; CalogFnT **foreignFns; // foreign function values pushed into this VM int32_t foreignCount; int32_t foreignCap; int32_t *freeRefs; // reclaimed _calog_fn_N slot numbers, reused before minting new ones int32_t freeCount; int32_t freeCap; }; // The limit state of the Berry context running on THIS thread, or NULL if it is unlimited. // Berry's memory choke point (be_realloc) and its heartbeat hook have no per-call userdata, // and each context owns its dedicated thread (create/run/destroy all run on it), so a // thread-local pointer -- set once at create -- resolves the running context exactly, the // same pattern my-basic uses for its process-global allocator (see mybasicAdapter.c). static _Thread_local CalogLimitStateT *gBerryLimits = NULL; // The broker of the Berry context running on THIS thread. The heartbeat hook gets only the bvm, so // this is how it reaches the runtime to ask whether the abort latch is set. Armed for every context, // limited or not, because an unlimited script must be interruptible too. static _Thread_local CalogT *gBerryBroker = NULL; // Backs a CalogFnT exported from this VM: the owning context, the reclaimable slot // number, and the name of the hidden global that keeps the Berry function GC-reachable. typedef struct BerryExportT { CalogBerryT *context; int32_t refNum; char refName[BERRY_REF_CAP]; } BerryExportT; static int32_t berryCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static void berryCallableRelease(CalogFnT *callable); static int32_t berryExportValue(CalogBerryT *context, int index, CalogFnT **out); static int berryForeignCall(bvm *vm); static int32_t berryFromValue(CalogBerryT *context, const CalogValueT *value, int32_t depth); static void berryObsHook(bvm *vm, int event, ...); static int32_t berryTrackForeign(CalogBerryT *context, CalogFnT *callable); static int32_t berryToValue(CalogBerryT *context, int index, CalogValueT *out, int32_t depth); static int berryTrampoline(bvm *vm); int32_t calogBerryCreate(CalogBerryT **out, CalogT *broker, uint64_t ctxId, CalogLimitStateT *limits) { CalogBerryT *context; *out = NULL; context = (CalogBerryT *)calloc(1, sizeof(*context)); if (context == NULL) { return calogErrOomE; } // The base VM is built before the cap is armed, so the runtime itself is never refused // (the cap must exceed the base runtime, as with Lua); Berry still counts these bytes in // vm->gc.usage, so a subsequent grow is measured against the true live total. context->vm = be_vm_new(); if (context->vm == NULL) { free(context); return calogErrOomE; } context->broker = broker; context->ctxId = ctxId; context->limits = limits; // Arm this thread's limit pointer (read by the be_realloc cap check and the heartbeat // hook). A time-limited context also gets the heartbeat hook, which Berry fires from its // instruction loop every 2^19 instructions (BE_USE_PERF_COUNTERS is enabled in the // vendored berry_conf.h) -- often enough to notice a wall-clock deadline inside a tight // loop. An unlimited context leaves the pointer NULL and installs no hook (zero overhead). gBerryLimits = limits; gBerryBroker = broker; be_set_obs_hook(context->vm, berryObsHook); *out = context; return calogOkE; } // Charge check for the vendored be_realloc cap patch: non-zero when growing the running // context's live heap by `growth` bytes would exceed its cap. Also mirrors the current live // usage into memUsed for reporting. NULL/uncapped context -> never refuses. int calogBerryMemOverCap(size_t usage, size_t growth) { CalogLimitStateT *limits; limits = gBerryLimits; if (limits == NULL || limits->memCap <= 0) { return 0; } limits->memUsed = (int64_t)usage; return (int64_t)usage + (int64_t)growth > limits->memCap; } // Berry's instruction-loop heartbeat: if the wall-clock deadline has passed, retire this context and // raise a Berry error to unwind the running script (be_raise longjmps to the protected frame). A // runaway (non-catching) script unwinds to the be_pcall in calogBerryRun, which lets the deferred // retire fire; a script that deliberately catches this in try/except and loops can defeat the budget // and pin its thread -- an accepted cooperative-model limitation shared by every engine including the // Lua reference (see design.md sec 24). Other observability events are ignored. static void berryObsHook(bvm *vm, int event, ...) { CalogLimitStateT *limits; if (event != BE_OBS_VM_HEARTBEAT) { return; } // The runtime was latched aborting (calogExit, or a signal): unwind this script even though it // may never call a native. Deliberately no retire -- teardown order stays the host's. if (gBerryBroker != NULL && calogAborting(gBerryBroker)) { calogAbortRaise(); be_raise(vm, "calog_abort", CALOG_ABORT_MESSAGE); } limits = gBerryLimits; if (limits != NULL && limits->deadlineMs != 0 && calogMonotonicMillis() >= limits->deadlineMs) { calogCurrentRetire(); be_raise(vm, "calog_timeout", "context exceeded its time budget"); } } static int32_t berryCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { BerryExportT *export; CalogBerryT *context; bvm *vm; int base; int32_t index; int32_t status; int code; export = (BerryExportT *)userData; context = export->context; vm = context->vm; calogValueNil(result); base = be_top(vm); be_getglobal(vm, export->refName); if (be_isnil(vm, -1)) { be_pop(vm, be_top(vm) - base); return calogFail(result, calogErrDeadE, "berry callable no longer exists"); } for (index = 0; index < argCount; index++) { status = berryFromValue(context, &args[index], 0); if (status != calogOkE) { be_pop(vm, be_top(vm) - base); return calogFail(result, status, "failed to marshal argument into berry"); } } code = be_pcall(vm, argCount); if (code != BE_OK) { const char *message; message = be_tostring(vm, -1); status = calogFail(result, calogErrArgE, message != NULL ? message : "berry call failed"); be_pop(vm, be_top(vm) - base); return status; } status = berryToValue(context, base + 1, result, 0); be_pop(vm, be_top(vm) - base); return status; } static void berryCallableRelease(CalogFnT *callable) { BerryExportT *export; CalogBerryT *context; bvm *vm; void *buffer; int64_t cap64; export = (BerryExportT *)calogFnUserData(callable); context = export->context; vm = context->vm; // Drop the hidden global so the pinned function becomes collectable. be_pushnil(vm); be_setglobal(vm, export->refName); be_pop(vm, 1); // Reclaim the slot number so the next export reuses it instead of minting a fresh // hidden global. If the free list cannot grow, the number is simply not reused -- // no leak, just a missed optimization on this release. buffer = context->freeRefs; cap64 = context->freeCap; if (calogGrow(&buffer, &cap64, (int64_t)context->freeCount + 1, sizeof(int32_t)) == calogOkE) { context->freeRefs = (int32_t *)buffer; context->freeCap = (int32_t)cap64; context->freeRefs[context->freeCount] = export->refNum; context->freeCount++; } free(export); } void calogBerryDestroy(CalogBerryT *context) { int32_t index; if (context == NULL) { return; } // This thread's limit pointer aliases the context's limit state, which is freed with the // context; clear it so nothing on this (soon-joined) thread can read it afterward. gBerryLimits = NULL; gBerryBroker = NULL; // Release the foreign function values pushed into this VM (see berryTrackForeign). for (index = 0; index < context->foreignCount; index++) { calogFnRelease(context->foreignFns[index]); } free(context->foreignFns); free(context->freeRefs); if (context->vm != NULL) { be_vm_delete(context->vm); } free(context); } int32_t calogBerryExport(CalogBerryT *context, const char *globalName, CalogFnT **out) { bvm *vm; int base; int32_t status; vm = context->vm; *out = NULL; base = be_top(vm); if (!be_getglobal(vm, globalName)) { be_pop(vm, be_top(vm) - base); return calogErrNotFoundE; } if (!be_isfunction(vm, -1)) { be_pop(vm, be_top(vm) - base); return calogErrTypeE; } status = berryExportValue(context, -1, out); be_pop(vm, be_top(vm) - base); return status; } static int32_t berryExportValue(CalogBerryT *context, int index, CalogFnT **out) { bvm *vm; BerryExportT *export; int32_t status; vm = context->vm; *out = NULL; export = (BerryExportT *)malloc(sizeof(*export)); if (export == NULL) { return calogErrOomE; } export->context = context; if (context->freeCount > 0) { // Reuse a slot released by a prior berryCallableRelease instead of minting a // new global name, so the hidden global table stays bounded by the peak number // of simultaneously live exports. context->freeCount--; export->refNum = context->freeRefs[context->freeCount]; } else { export->refNum = context->nextRef; context->nextRef++; } snprintf(export->refName, sizeof(export->refName), "_calog_fn_%d", export->refNum); // Pin the function under a hidden global name (globals are GC roots). be_pushvalue(vm, index); be_setglobal(vm, export->refName); be_pop(vm, 1); status = calogFnCreate(out, context->broker, berryCallableInvoke, export, berryCallableRelease, context->ctxId); if (status != calogOkE) { be_pushnil(vm); be_setglobal(vm, export->refName); be_pop(vm, 1); free(export); return status; } return calogOkE; } int32_t calogBerryExpose(CalogBerryT *context, const char *name) { bvm *vm; CalogEntryT *entry; vm = context->vm; entry = calogLookup(context->broker, name); if (entry == NULL) { return calogErrNotFoundE; } // A native closure with two upvalues: the context (comptr) and the name. The // trampoline recovers them and dispatches through calogCall by name. be_pushntvclosure(vm, berryTrampoline, 2); be_pushcomptr(vm, context); be_setupval(vm, -2, 0); be_pop(vm, 1); be_pushstring(vm, name); be_setupval(vm, -2, 1); be_pop(vm, 1); be_setglobal(vm, name); be_pop(vm, 1); return calogOkE; } // Call trampoline for a foreign CalogFnT pushed into Berry as a native closure. Its two // upvalues are the context and the CalogFnT (both comptrs), pushed above the arguments. static int berryForeignCall(bvm *vm) { CalogBerryT *context; CalogFnT *callable; CalogValueT *args; CalogValueT result; int argc; int index; int32_t status; char message[CALOG_ERR_MSG_CAP]; argc = be_top(vm); // arguments occupy stack slots 1..argc be_getupval(vm, 0, 0); context = (CalogBerryT *)be_tocomptr(vm, -1); be_getupval(vm, 0, 1); callable = (CalogFnT *)be_tocomptr(vm, -1); be_pop(vm, 2); args = NULL; if (argc > 0) { args = (CalogValueT *)calloc((size_t)argc, sizeof(CalogValueT)); if (args == NULL) { be_raise(vm, "memory_error", "out of memory marshalling function-value args"); return 0; } } for (index = 0; index < argc; index++) { status = berryToValue(context, index + 1, &args[index], 0); if (status != calogOkE) { int cleanup; for (cleanup = 0; cleanup < index; cleanup++) { calogValueFree(&args[cleanup]); } free(args); be_raise(vm, "value_error", "failed to marshal a function-value argument"); return 0; } } status = calogFnInvoke(callable, args, argc, &result); for (index = 0; index < argc; index++) { calogValueFree(&args[index]); } free(args); if (status != calogOkE) { snprintf(message, sizeof(message), "%s", (result.type == calogStringE) ? result.as.s.bytes : "function value failed"); calogValueFree(&result); be_raise(vm, "calog_error", message); return 0; } status = berryFromValue(context, &result, 0); calogValueFree(&result); if (status != calogOkE) { be_pop(vm, 1); be_raise(vm, "type_error", "failed to marshal the function-value result"); return 0; } be_return(vm); } static int32_t berryFromValue(CalogBerryT *context, const CalogValueT *value, int32_t depth) { bvm *vm; vm = context->vm; if (depth >= CALOG_MAX_DEPTH) { be_pushnil(vm); return calogErrDepthE; } switch (value->type) { case calogNilE: be_pushnil(vm); return calogOkE; case calogBoolE: be_pushbool(vm, value->as.b); return calogOkE; case calogIntE: be_pushint(vm, (bint)value->as.i); return calogOkE; case calogRealE: be_pushreal(vm, (breal)value->as.r); return calogOkE; case calogStringE: be_pushnstring(vm, value->as.s.bytes, (size_t)value->as.s.length); return calogOkE; case calogAggE: { CalogAggT *aggregate; const char *className; int64_t index; int containerBase; int32_t childStatus; aggregate = value->as.agg; containerBase = be_top(vm); // Populate a raw container: anything keyed (or an explicit map) is a map with // the sequence part at integer keys, else a list. be_setindex and be_data_push // take the value on top and do not pop, so drop it after each. A failed // recursive marshal leaves a nil on top of the failing key (or bare), so // unwind the whole container (Berry GC owns it) and propagate the error -- // a single nil is left on top to keep this frame's own stack contract. if (calogAggIsKeyed(aggregate)) { className = "map"; be_newmap(vm); for (index = 0; index < aggregate->arrayCount; index++) { be_pushint(vm, (bint)index); childStatus = berryFromValue(context, &aggregate->array[index], depth + 1); if (childStatus != calogOkE) { be_pop(vm, be_top(vm) - containerBase); be_pushnil(vm); return childStatus; } be_setindex(vm, -3); be_pop(vm, 2); } for (index = 0; index < aggregate->pairCount; index++) { const CalogValueT *key; key = &aggregate->pairs[index].key; if (key->type == calogStringE) { be_pushnstring(vm, key->as.s.bytes, (size_t)key->as.s.length); } else if (key->type == calogIntE) { be_pushint(vm, (bint)key->as.i); } else { continue; // non-scalar key: no Berry equivalent, drop the pair } childStatus = berryFromValue(context, &aggregate->pairs[index].value, depth + 1); if (childStatus != calogOkE) { be_pop(vm, be_top(vm) - containerBase); be_pushnil(vm); return childStatus; } be_setindex(vm, -3); be_pop(vm, 2); } } else { className = "list"; be_newlist(vm); for (index = 0; index < aggregate->arrayCount; index++) { childStatus = berryFromValue(context, &aggregate->array[index], depth + 1); if (childStatus != calogOkE) { be_pop(vm, be_top(vm) - containerBase); be_pushnil(vm); return childStatus; } be_data_push(vm, -2); be_pop(vm, 1); } } // A raw map/list is not subscriptable in a script; wrap it in its class // instance (map(raw) / list(raw)). Calling a class leaves the instance and // init's (nil) return above the raw, so move the instance down over the raw // and drop the two spare slots. be_getbuiltin(vm, className); be_pushvalue(vm, -2); be_call(vm, 1); be_moveto(vm, -2, -3); be_pop(vm, 2); return calogOkE; } case calogFnE: { // Berry has no per-value finalizer, so track the foreign function on the // context (released at destroy) and wrap it in a native closure -- callable // as f(x) -- whose upvalues are the context and the CalogFnT. if (berryTrackForeign(context, value->as.fn) != calogOkE) { be_pushnil(vm); return calogErrOomE; } be_pushntvclosure(vm, berryForeignCall, 2); be_pushcomptr(vm, context); be_setupval(vm, -2, 0); be_pop(vm, 1); be_pushcomptr(vm, value->as.fn); be_setupval(vm, -2, 1); be_pop(vm, 1); calogFnRetain(value->as.fn); return calogOkE; } } be_pushnil(vm); return calogErrTypeE; } int32_t calogBerryRun(CalogBerryT *context, const char *source) { bvm *vm; bool compiled; int base; int code; vm = context->vm; base = be_top(vm); code = be_loadstring(vm, source); compiled = (code == BE_OK); if (compiled) { code = be_pcall(vm, 0); } if (code != BE_OK) { const char *message; // calogExit (calogAbortAll) unwound this script deliberately: not a failure, so report // nothing. Asked as "is this error ours", which can only be true of a script that // actually ran, so a syntax error still gets its diagnostic even mid-teardown. if (calogAbortRaised(context->broker)) { be_pop(vm, be_top(vm) - base); return calogOkE; } message = be_tostring(vm, -1); fprintf(stderr, "berry error: %s\n", message != NULL ? message : "(unknown)"); be_pop(vm, be_top(vm) - base); return calogErrArgE; } be_pop(vm, be_top(vm) - base); return calogOkE; } static int32_t berryToValue(CalogBerryT *context, int index, CalogValueT *out, int32_t depth) { bvm *vm; vm = context->vm; calogValueNil(out); if (depth >= CALOG_MAX_DEPTH) { return calogErrDepthE; } if (be_isnil(vm, index)) { return calogOkE; } if (be_isbool(vm, index)) { calogValueBool(out, be_tobool(vm, index) != 0); return calogOkE; } if (be_isint(vm, index)) { calogValueInt(out, (int64_t)be_toint(vm, index)); return calogOkE; } if (be_isreal(vm, index)) { calogValueReal(out, (double)be_toreal(vm, index)); return calogOkE; } if (be_isstring(vm, index)) { const char *bytes; int len; bytes = be_tostring(vm, index); len = be_strlen(vm, index); return calogValueString(out, bytes, (int64_t)len); } if (be_isfunction(vm, index)) { CalogFnT *callable; int32_t status; status = berryExportValue(context, index, &callable); if (status != calogOkE) { return status; } calogValueFn(out, callable); return calogOkE; } // A list/map instance holds its raw container in the hidden ".p" member. be_iter_* // take the CONTAINER index with the iterator on top of the stack (be_iter_next pushes // one value for a list, key+value for a map), so keep the container at -2 and restore // the iterator to the top after reading each entry. if (be_islistinstance(vm, index)) { CalogAggT *aggregate; int base; int32_t status; status = calogAggCreate(&aggregate, calogListE); if (status != calogOkE) { return status; } base = be_top(vm); be_getmember(vm, index, ".p"); // raw list -> top be_pushiter(vm, -1); // iterator on top; container at -2 while (be_iter_hasnext(vm, -2)) { CalogValueT element; be_iter_next(vm, -2); // value on top status = berryToValue(context, -1, &element, depth + 1); be_pop(vm, be_top(vm) - (base + 2)); // restore to [container, iterator] if (status != calogOkE) { be_pop(vm, 2); calogAggFree(aggregate); return status; } status = calogAggPush(aggregate, &element); if (status != calogOkE) { calogValueFree(&element); be_pop(vm, 2); calogAggFree(aggregate); return status; } } be_pop(vm, 2); // container + iterator calogValueAgg(out, aggregate); return calogOkE; } if (be_ismapinstance(vm, index)) { CalogAggT *aggregate; int base; int32_t status; status = calogAggCreate(&aggregate, calogMapE); if (status != calogOkE) { return status; } base = be_top(vm); be_getmember(vm, index, ".p"); // raw map -> top be_pushiter(vm, -1); // iterator on top; container at -2 while (be_iter_hasnext(vm, -2)) { CalogValueT key; CalogValueT value; be_iter_next(vm, -2); // key at -2, value at -1 (above the iterator) status = berryToValue(context, -2, &key, depth + 1); if (status != calogOkE) { be_pop(vm, be_top(vm) - base); calogAggFree(aggregate); return status; } status = berryToValue(context, -1, &value, depth + 1); if (status != calogOkE) { calogValueFree(&key); be_pop(vm, be_top(vm) - base); calogAggFree(aggregate); return status; } be_pop(vm, be_top(vm) - (base + 2)); // restore to [container, iterator] status = calogAggSet(aggregate, &key, &value); if (status != calogOkE) { calogValueFree(&key); calogValueFree(&value); be_pop(vm, 2); calogAggFree(aggregate); return status; } } be_pop(vm, 2); // container + iterator calogValueAgg(out, aggregate); return calogOkE; } // Other reference types have no CalogValueT equivalent. return calogErrUnsupportedE; } // Record a foreign function pushed into this VM so it is released at destroy (Berry has // no per-value finalizer to release it when the wrapping closure is collected). static int32_t berryTrackForeign(CalogBerryT *context, CalogFnT *callable) { if (context->foreignCount == context->foreignCap) { int32_t newCap; CalogFnT **resized; newCap = (context->foreignCap == 0) ? BERRY_FOREIGN_INITIAL : context->foreignCap * CALOG_GROWTH_FACTOR; resized = (CalogFnT **)realloc(context->foreignFns, (size_t)newCap * sizeof(CalogFnT *)); if (resized == NULL) { return calogErrOomE; } context->foreignFns = resized; context->foreignCap = newCap; } context->foreignFns[context->foreignCount] = callable; context->foreignCount++; return calogOkE; } static int berryTrampoline(bvm *vm) { CalogBerryT *context; const char *name; CalogValueT *args; CalogValueT result; int argc; int index; int32_t status; char message[CALOG_ERR_MSG_CAP]; argc = be_top(vm); // arguments occupy stack slots 1..argc // Recover the binding from the closure's upvalues (pushed above the args). be_getupval(vm, 0, 0); context = (CalogBerryT *)be_tocomptr(vm, -1); be_getupval(vm, 0, 1); name = be_tostring(vm, -1); // borrowed; valid until popped after the call args = NULL; if (argc > 0) { args = (CalogValueT *)calloc((size_t)argc, sizeof(CalogValueT)); if (args == NULL) { be_pop(vm, 2); be_raise(vm, "memory_error", "out of memory marshalling native arguments"); return 0; } } for (index = 0; index < argc; index++) { status = berryToValue(context, index + 1, &args[index], 0); if (status != calogOkE) { int cleanup; for (cleanup = 0; cleanup < index; cleanup++) { calogValueFree(&args[cleanup]); } free(args); be_pop(vm, 2); be_raise(vm, "type_error", "failed to marshal a native argument"); return 0; } } status = calogCall(context->broker, name, args, argc, &result); for (index = 0; index < argc; index++) { calogValueFree(&args[index]); } free(args); be_pop(vm, 2); // pop the two upvalues (name, context) if (status != calogOkE) { snprintf(message, sizeof(message), "%s", (result.type == calogStringE) ? result.as.s.bytes : "native call failed"); calogValueFree(&result); be_raise(vm, "calog_error", message); return 0; } status = berryFromValue(context, &result, 0); calogValueFree(&result); if (status != calogOkE) { be_pop(vm, 1); be_raise(vm, "type_error", "failed to marshal the native result"); return 0; } be_return(vm); }