740 lines
28 KiB
C
740 lines
28 KiB
C
// 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 <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#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);
|
|
}
|