diff --git a/LICENSE.md b/LICENSE.md index 450fe2d..d6511d9 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -55,5 +55,11 @@ applies to its files; the full text ships alongside each engine's source. - **Wren** — Copyright (c) 2013-2021 Robert Nystrom and Wren Contributors. MIT License. `vendor/wren/LICENSE` (amalgamated into `vendor/wren/wren.c`). + > Note: `vendor/wren/wren.c` and `wren.h` carry one small calog patch adding a public + > C map-key iterator (`wrenGetMapCapacity` / `wrenGetMapEntry`), which upstream Wren + > lacks; it mirrors Wren's own internal `map_iterate`. The additions are bracketed by + > `--- calog patch ---` comments. Re-apply them if the amalgamation is regenerated from + > upstream (`util/generate_amalgamation.py`). + - **Berry** — Copyright (c) 2018-2026 Guan Wenliang and Berry contributors. MIT License. `vendor/berry/` (see the upstream `LICENSE`). diff --git a/README.md b/README.md index 73dbe59..c816097 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,9 @@ Swap `&calogJsEngine` for `&calogLuaEngine`, `&calogSquirrelEngine`, 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; you keep it and call - it later, and calog routes the call back to the engine that owns it. +- **Callbacks both ways.** A script can hand you a function value to keep and call later + (routed back to the engine that owns it); and you can hand a native to a script as a + callable value with `calogFnFromNative` (every engine but MY-BASIC). - **Many runtimes.** Independent `CalogT` runtimes coexist in one process; one host thread can drive several. - **Load by filename.** `calogContextLoad(calog, "config")` finds `config.lua` / @@ -60,8 +61,11 @@ Swap `&calogJsEngine` for `&calogLuaEngine`, `&calogSquirrelEngine`, | 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")`). Handing a list/map *back* to -C is complete on Lua/JS/Squirrel/MY-BASIC and a v1 limit on Scheme/Wren/Berry. +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. @@ -267,6 +271,21 @@ calogValueFree(&arg); calogFnRelease(savedCb); ``` +It works the other way too: wrap one of your natives as a function value with +`calogFnFromNative` and return it from a native, and the script gets a callable it can +invoke (which routes back to your host thread). Every engine but MY-BASIC supports this. + +```c +static int32_t getAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + CalogFnT *fn; + (void)args; (void)argCount; (void)userData; + calogValueNil(result); + calogFnFromNative(&fn, calog, add, NULL); // 'add' is one of your natives + calogValueFn(result, fn); // the script can now call getAdder()(2, 3) + return calogOkE; +} +``` + ### Loading a script by filename Register the engines you want searchable (order is priority), then load by base name: diff --git a/design.md b/design.md index 85726e8..65c8746 100644 --- a/design.md +++ b/design.md @@ -839,10 +839,9 @@ marshals the return; `squirrelCallableRelease` `sq_release`s on the owner thread Single-threaded `testSquirrel` covers export+invoke-from-C, a closure passed as an argument and called back through the broker, and the not-found/type-error paths; ASan-clean (no addref/release leak). Caveat (same as Lua): release exported -callables before `squirrelContextDestroy`. Remaining limit: the reverse direction -(a foreign `CallableT` pushed INTO Squirrel so a script can call it) returns -`brokerErrUnsupportedE` -- Squirrel has no clean callable-userdata-with-`__gc` like -Lua, so it needs a class instance with a `_call` metamethod + release hook. +callables before `squirrelContextDestroy`. (The reverse direction -- a foreign `CalogFnT` +pushed INTO Squirrel -- is now supported too: a native closure whose one free variable is +a release-hooked userdata holding the `CalogFnT`; see sec 18.) `make test` runs all seven binaries (411 checks). `make tsan` covers the actor core and the Lua engine path; `make tsansq` the Squirrel path. @@ -900,8 +899,9 @@ nil. The sharp edge: `be_pcall(vm, argc)` leaves the **result in the function's (`be_modtab.c`). A subtlety for records: `be_newmap`/`be_newlist` push *raw* containers that a script cannot subscript, so an aggregate crossing out is wrapped in its `map`/`list` class instance (`map(raw)` via `be_getbuiltin` + `be_call`, then `be_moveto`/`be_pop` to -drop the raw and `init`'s nil return) -- then `user['name']` works. Reading a script's -list/map back in is a v1 limit. +drop the raw and `init`'s nil return) -- then `user['name']` works. Ingress reverses it: +a list/map instance holds its raw container in the hidden `.p` member, iterated with +`be_pushiter`/`be_iter_next` (see sec 18). **s7 Scheme** (`.scm`) uses the *current* official s7 (an older mirror lacked `s7_free`, which would leak a heap per context). Since s7 native functions carry no user data, all @@ -937,3 +937,70 @@ Berry/s7/Wren. **Verified** across all seven engines: `make test` (531 checks, including a materialized- record read per new engine), ASan/UBSan clean, a `tsan` target clean for each, and gcc + clang strict on the core. + +## 18. Closing the v1 marshalling limits (function-into-script, aggregate ingress) + +The engines above shipped with two directional gaps in the value bridge: a foreign +function value could not be pushed *into* a script (only Lua did it), and a script could +not hand a keyed aggregate (map) *back* to C on the three newest engines. Both are now +closed everywhere they can be, with one honest exception each. + +**New public API** `calogFnFromNative(out, calog, fn, userData)` -- wraps one of your +natives as a host-owned function value (owner id 0, runs on the host thread during +`calogPump`, like `calogRegister` but anonymous). Without it, function-into-script was +unusable from `calog.h` alone (`calogFnCreate` is internal), so a host could only forward +a *script*-derived callable, never one of its own natives. Return the result from a native +and the script gets a callable that routes back to the host. + +**Function value -> script** (each `*FromValue` `calogFnE` case): an engine callable +object wraps the `CalogFnT*`, a trampoline marshals the script's args -> `calogFnInvoke` +-> marshals the result, and a finalizer runs `calogFnRelease`; `calogFnRetain` at push. +Per engine: +- **Lua** (pre-existing): userdata + `__call` + `__gc` metatable -- the reference for the rest. +- **JS**: a `JSClassDef` with both `.call` and `.finalizer`; the finalizer only receives + the runtime, so `JS_SetRuntimeOpaque` carries the context to it. +- **Squirrel**: a native closure whose single free variable is a release-hooked userdata + holding the `CalogFnT` (freeing the closure frees the userdata -> the hook releases). +- **s7**: an applicable c-object (`s7_make_c_type` + `s7_c_type_set_ref` for the call, + `s7_c_type_set_free` for release); the `ref` fn gets `(obj . args)`, so the object is + `s7_car`. Script calls `(f ...)`. +- **Berry**: no per-value finalizer exists, so the `CalogFnT`s pushed into a context are + *tracked on the context* and released together in `calogBerryDestroy`; the callable is a + native closure over `(context, CalogFnT)` comptr upvalues. (`berryFromValue` gained the + context parameter so it could record them.) +- **Wren**: a `foreign class CalogFn { construct new() {} foreign call(args) }` whose + `call` takes a *list* (Wren method arity is fixed, so a list absorbs any argument count); + finalize releases. Script calls `f.call([...])`. Gotcha: Wren requires newlines between + class members, so the preamble is multi-line. +- **MY-BASIC**: inherent gap -- BASIC has no first-class callable values to invoke. + +**Aggregate ingress** (`*ToValue` map/list): Lua/JS/Squirrel/MY-BASIC already read both. +Added: +- **s7**: read a hash-table by `s7_make_iterator` + `s7_iterate` (each yields a + `(key . value)` cons; gotcha: at end `s7_iterate` returns a *non-pair* sentinel even + when the at-end flag was still false, so guard `if (!s7_is_pair(pair)) break`). +- **Berry**: a list/map instance's raw container is its hidden `.p` member; iterate with + `be_pushiter`/`be_iter_hasnext`/`be_iter_next` (which take the *container* index with the + iterator kept on top, pushing one value for a list and key+value for a map -- restore the + stack to `[container, iterator]` after each entry). +- **Wren**: a `List` reads back directly. A `Map` needs key enumeration, which upstream + Wren's C API lacks (`wrenGetMapValue` is by-key only) -- so calog adds a small patch to + the vendored `wren.c`/`wren.h` (`wrenGetMapCapacity` / `wrenGetMapEntry`, mirroring Wren's + own internal `map_iterate`), and the adapter walks the raw table skipping empty slots. + With the patch, Wren too reads maps back in. (Documented in LICENSE.md; re-apply if the + amalgamation is regenerated.) + +**Deliberately not "fixed" (inherent to the engine's value model):** MY-BASIC 32-bit ints +(over 2^31 range-checked to an error, not silently truncated), MY-BASIC NUL-in-string +truncation and serialize-at-load, and JS/Wren int64 above 2^53 (IEEE doubles -- JS *could* +emit a BigInt but that breaks arithmetic mixing with Number, a worse trap than the +documented precision edge). `WREN_MAX_CALL_ARITY` (16) is pinned to Wren's own engine +limit (`MAX_PARAMETERS`) and can't be raised. `MB_BANK_SIZE` (the MY-BASIC native cap) was +32 -- the one hard cap a real app could hit, since MY-BASIC natives can't carry userdata +so each needs a hand-materialized slot trampoline; it is now **256** (the trampoline bank +and its `[MB_BANK_SIZE]` table are regenerated together, so a count mismatch fails to +compile). Berry's `BE_BYTES_MAX_SIZE` was likewise raised from 32 kb to 256 MB. + +**Verified**: `make test` (539 checks) with a `testForeignFunction` per engine and a +`testMapIngress` on s7 and Berry (the Berry one nests a list to exercise list ingress); +ASan-clean on every engine (retain/release balanced); gcc strict; per-engine `tsan*`. diff --git a/src/berry/berryAdapter.c b/src/berry/berryAdapter.c index aba963d..d57c01c 100644 --- a/src/berry/berryAdapter.c +++ b/src/berry/berryAdapter.c @@ -4,10 +4,12 @@ // (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; a Berry function crossing out becomes a refcounted CalogFnT kept reachable by -// a uniquely-named hidden global (a GC root), dropped by setting that global to nil on -// release. An aggregate crossing out becomes a Berry list or map instance (a host record -// materializes as a map); reading a script's list/map back in is a v1 limit. +// 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. @@ -23,14 +25,18 @@ #include #include -#define BERRY_ERR_CAP 256 -#define BERRY_REF_CAP 32 +#define BERRY_ERR_CAP 256 +#define BERRY_REF_CAP 32 +#define BERRY_FOREIGN_INITIAL 8 struct CalogBerryT { - bvm *vm; - CalogT *broker; - uint64_t ctxId; - int32_t nextRef; + bvm *vm; + CalogT *broker; + uint64_t ctxId; + int32_t nextRef; + CalogFnT **foreignFns; // foreign function values pushed into this VM + int32_t foreignCount; + int32_t foreignCap; }; // Backs a CalogFnT exported from this VM: the owning context and the name of the hidden @@ -43,7 +49,9 @@ typedef struct 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 int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth); +static int berryForeignCall(bvm *vm); +static int32_t berryFromValue(CalogBerryT *context, const CalogValueT *value, int32_t depth); +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); @@ -86,7 +94,7 @@ static int32_t berryCallableInvoke(CalogValueT *args, int32_t argCount, CalogVal return calogFail(result, calogErrDeadE, "berry callable no longer exists"); } for (index = 0; index < argCount; index++) { - status = berryFromValue(vm, &args[index], 0); + 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"); @@ -121,9 +129,16 @@ static void berryCallableRelease(CalogFnT *callable) { void calogBerryDestroy(CalogBerryT *context) { + int32_t index; + if (context == NULL) { return; } + // 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); if (context->vm != NULL) { be_vm_delete(context->vm); } @@ -207,7 +222,65 @@ int32_t calogBerryExpose(CalogBerryT *context, const char *name) { } -static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth) { +// 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[BERRY_ERR_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; + } + berryFromValue(context, &result, 0); + calogValueFree(&result); + 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; @@ -241,7 +314,7 @@ static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth) be_newmap(vm); for (index = 0; index < aggregate->arrayCount; index++) { be_pushint(vm, (bint)index); - berryFromValue(vm, &aggregate->array[index], depth + 1); + berryFromValue(context, &aggregate->array[index], depth + 1); be_setindex(vm, -3); be_pop(vm, 2); } @@ -255,7 +328,7 @@ static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth) } else { continue; // non-scalar key: no Berry equivalent, drop the pair } - berryFromValue(vm, &aggregate->pairs[index].value, depth + 1); + berryFromValue(context, &aggregate->pairs[index].value, depth + 1); be_setindex(vm, -3); be_pop(vm, 2); } @@ -263,7 +336,7 @@ static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth) className = "list"; be_newlist(vm); for (index = 0; index < aggregate->arrayCount; index++) { - berryFromValue(vm, &aggregate->array[index], depth + 1); + berryFromValue(context, &aggregate->array[index], depth + 1); be_data_push(vm, -2); be_pop(vm, 1); } @@ -279,10 +352,24 @@ static int32_t berryFromValue(bvm *vm, const CalogValueT *value, int32_t depth) be_pop(vm, 2); return calogOkE; } - case calogFnE: - // Pushing a foreign function value into Berry is a v1 limit. - be_pushnil(vm); - return calogErrUnsupportedE; + 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; @@ -352,11 +439,110 @@ static int32_t berryToValue(CalogBerryT *context, int index, CalogValueT *out, i calogValueFn(out, callable); return calogOkE; } - // Reading a script's list/map into an aggregate is a v1 limit. + // 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; @@ -408,7 +594,7 @@ static int berryTrampoline(bvm *vm) { be_raise(vm, "calog_error", message); return 0; } - status = berryFromValue(vm, &result, 0); + status = berryFromValue(context, &result, 0); calogValueFree(&result); if (status != calogOkE) { be_pop(vm, 1); diff --git a/src/berry/berryAdapter.h b/src/berry/berryAdapter.h index 424b64d..915b31b 100644 --- a/src/berry/berryAdapter.h +++ b/src/berry/berryAdapter.h @@ -3,9 +3,9 @@ // Exposes broker-registered native functions into a Berry VM, marshals values between // Berry and CalogValueT by value (binary-safe strings; scalars), and exports Berry // functions as refcounted CalogFnT handles (kept alive by a GC-rooted hidden global, -// dropped on release). An aggregate crossing OUT to a script becomes a Berry list or map -// instance (so a host record is readable as user['name']); reading a script's list/map -// back IN is a v1 limit -> calogErrUnsupportedE. +// dropped on release). Aggregates cross both ways: OUT a host record is a Berry map +// instance (user['name']); IN a script's list/map is read by iterating its raw container. +// A foreign CalogFnT pushed IN becomes a native closure the script calls directly. // // Lifetime note (as with Lua/JS): release every CalogFnT obtained from calogBerryExport // (and drop every function value marshalled out) BEFORE calogBerryDestroy, since the diff --git a/src/calog.h b/src/calog.h index 70e2e67..b9160e6 100644 --- a/src/calog.h +++ b/src/calog.h @@ -166,6 +166,11 @@ int32_t calogAggPush(CalogAggT *aggregate, CalogValueT *value); int32_t calogAggSet(CalogAggT *aggregate, CalogValueT *key, CalogValueT *value); // ---- function values ---- +// Wrap a host native as a host-owned function value (runs on the host thread during +// calogPump, like calogRegister, but anonymous). Hand the result to a script via a +// native's result to give the script a callable; release your reference with +// calogFnRelease once you no longer hold it. +int32_t calogFnFromNative(CalogFnT **out, CalogT *calog, CalogNativeFnT fn, void *userData); int32_t calogFnInvoke(CalogFnT *fn, CalogValueT *args, int32_t argCount, CalogValueT *result); void calogFnRetain(CalogFnT *fn); void calogFnRelease(CalogFnT *fn); diff --git a/src/js/jsAdapter.c b/src/js/jsAdapter.c index 2068147..e69ba7f 100644 --- a/src/js/jsAdapter.c +++ b/src/js/jsAdapter.c @@ -37,6 +37,7 @@ struct CalogJsT { JSContext *ctx; CalogT *broker; uint64_t ctxId; + JSClassID fnClassId; // class for a foreign CalogFnT pushed into JS (callable + finalized) }; // Backs a CalogFnT exported from this context: the retained JS function value (kept @@ -49,6 +50,8 @@ typedef struct JsExportT { static int32_t jsCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static void jsCallableRelease(CalogFnT *callable); static int32_t jsExportValue(CalogJsT *context, JSValueConst fn, CalogFnT **out); +static JSValue jsForeignCall(JSContext *ctx, JSValueConst funcObj, JSValueConst thisVal, int argc, JSValueConst *argv, int flags); +static void jsForeignFinalize(JSRuntime *rt, JSValueConst val); static JSValue jsFromValue(JSContext *ctx, const CalogValueT *value, int32_t depth); static int32_t jsToValue(JSContext *ctx, JSValueConst val, CalogValueT *out, int32_t depth); static JSValue jsTrampoline(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValueConst *func_data); @@ -76,6 +79,19 @@ int32_t calogJsCreate(CalogJsT **out, CalogT *broker, uint64_t ctxId) { context->broker = broker; context->ctxId = ctxId; JS_SetContextOpaque(context->ctx, context); + // A callable, finalized class wraps a foreign CalogFnT pushed into JS. The runtime + // opaque lets the finalizer (which gets only the runtime) recover the context. + JS_SetRuntimeOpaque(context->rt, context); + JS_NewClassID(context->rt, &context->fnClassId); + { + JSClassDef def; + def.class_name = "CalogFn"; + def.finalizer = jsForeignFinalize; + def.gc_mark = NULL; + def.call = jsForeignCall; + def.exotic = NULL; + JS_NewClass(context->rt, context->fnClassId, &def); + } *out = context; return calogOkE; } @@ -238,6 +254,68 @@ int32_t calogJsExpose(CalogJsT *context, const char *name) { } +// Call handler for a foreign CalogFnT wrapped as a JS object: marshal the JS arguments, +// invoke the function value (routed to its owning context), and marshal the result back. +static JSValue jsForeignCall(JSContext *ctx, JSValueConst funcObj, JSValueConst thisVal, int argc, JSValueConst *argv, int flags) { + CalogJsT *context; + CalogFnT *callable; + CalogValueT *cargs; + CalogValueT result; + JSValue jsResult; + int index; + int32_t status; + + (void)thisVal; + (void)flags; + context = (CalogJsT *)JS_GetContextOpaque(ctx); + callable = (CalogFnT *)JS_GetOpaque(funcObj, context->fnClassId); + cargs = NULL; + if (argc > 0) { + cargs = (CalogValueT *)calloc((size_t)argc, sizeof(CalogValueT)); + if (cargs == NULL) { + return JS_ThrowOutOfMemory(ctx); + } + } + for (index = 0; index < argc; index++) { + status = jsToValue(ctx, argv[index], &cargs[index], 0); + if (status != calogOkE) { + int cleanup; + for (cleanup = 0; cleanup < index; cleanup++) { + calogValueFree(&cargs[cleanup]); + } + free(cargs); + return JS_ThrowTypeError(ctx, "failed to marshal a function-value argument"); + } + } + status = calogFnInvoke(callable, cargs, argc, &result); + for (index = 0; index < argc; index++) { + calogValueFree(&cargs[index]); + } + free(cargs); + if (status != calogOkE) { + jsResult = JS_ThrowTypeError(ctx, "%s", (result.type == calogStringE) ? result.as.s.bytes : "function value failed"); + calogValueFree(&result); + return jsResult; + } + jsResult = jsFromValue(ctx, &result, 0); + calogValueFree(&result); + return jsResult; +} + + +// Finalizer for that wrapper: drop the reference the push took (see the calogFnE case). +static void jsForeignFinalize(JSRuntime *rt, JSValueConst val) { + CalogJsT *context; + CalogFnT *callable; + + context = (CalogJsT *)JS_GetRuntimeOpaque(rt); + callable = (CalogFnT *)JS_GetOpaque(val, context->fnClassId); + if (callable != NULL) { + calogFnRelease(callable); + } +} + + static JSValue jsFromValue(JSContext *ctx, const CalogValueT *value, int32_t depth) { if (depth > CALOG_MAX_DEPTH) { return JS_ThrowRangeError(ctx, "value nesting too deep"); @@ -301,10 +379,20 @@ static JSValue jsFromValue(JSContext *ctx, const CalogValueT *value, int32_t dep } return container; } - case calogFnE: - // Pushing a foreign function value INTO JavaScript is a v1 fidelity limit - // (export OUT is supported via calogJsExport). - return JS_ThrowTypeError(ctx, "pushing a foreign function into JavaScript is unsupported"); + case calogFnE: { + // Wrap the foreign function in a callable, finalized JS object; retain the + // handle for the wrapper's lifetime (the finalizer releases it). + CalogJsT *context; + JSValue obj; + context = (CalogJsT *)JS_GetContextOpaque(ctx); + obj = JS_NewObjectClass(ctx, context->fnClassId); + if (JS_IsException(obj)) { + return obj; + } + JS_SetOpaque(obj, value->as.fn); + calogFnRetain(value->as.fn); + return obj; + } } return JS_ThrowTypeError(ctx, "unknown value type"); } diff --git a/src/mybasic/mybasicAdapter.c b/src/mybasic/mybasicAdapter.c index 6a65606..3f45909 100644 --- a/src/mybasic/mybasicAdapter.c +++ b/src/mybasic/mybasicAdapter.c @@ -15,7 +15,7 @@ #include #include -#define MB_BANK_SIZE 32 +#define MB_BANK_SIZE 256 #define MB_INITIAL_ARGS 8 #define MB_NAME_MAX 128 #define MB_NATIVE_ERROR SE_RN_FAILED_TO_OPERATE @@ -90,13 +90,265 @@ MB_TRAMPOLINE(28) MB_TRAMPOLINE(29) MB_TRAMPOLINE(30) MB_TRAMPOLINE(31) +MB_TRAMPOLINE(32) +MB_TRAMPOLINE(33) +MB_TRAMPOLINE(34) +MB_TRAMPOLINE(35) +MB_TRAMPOLINE(36) +MB_TRAMPOLINE(37) +MB_TRAMPOLINE(38) +MB_TRAMPOLINE(39) +MB_TRAMPOLINE(40) +MB_TRAMPOLINE(41) +MB_TRAMPOLINE(42) +MB_TRAMPOLINE(43) +MB_TRAMPOLINE(44) +MB_TRAMPOLINE(45) +MB_TRAMPOLINE(46) +MB_TRAMPOLINE(47) +MB_TRAMPOLINE(48) +MB_TRAMPOLINE(49) +MB_TRAMPOLINE(50) +MB_TRAMPOLINE(51) +MB_TRAMPOLINE(52) +MB_TRAMPOLINE(53) +MB_TRAMPOLINE(54) +MB_TRAMPOLINE(55) +MB_TRAMPOLINE(56) +MB_TRAMPOLINE(57) +MB_TRAMPOLINE(58) +MB_TRAMPOLINE(59) +MB_TRAMPOLINE(60) +MB_TRAMPOLINE(61) +MB_TRAMPOLINE(62) +MB_TRAMPOLINE(63) +MB_TRAMPOLINE(64) +MB_TRAMPOLINE(65) +MB_TRAMPOLINE(66) +MB_TRAMPOLINE(67) +MB_TRAMPOLINE(68) +MB_TRAMPOLINE(69) +MB_TRAMPOLINE(70) +MB_TRAMPOLINE(71) +MB_TRAMPOLINE(72) +MB_TRAMPOLINE(73) +MB_TRAMPOLINE(74) +MB_TRAMPOLINE(75) +MB_TRAMPOLINE(76) +MB_TRAMPOLINE(77) +MB_TRAMPOLINE(78) +MB_TRAMPOLINE(79) +MB_TRAMPOLINE(80) +MB_TRAMPOLINE(81) +MB_TRAMPOLINE(82) +MB_TRAMPOLINE(83) +MB_TRAMPOLINE(84) +MB_TRAMPOLINE(85) +MB_TRAMPOLINE(86) +MB_TRAMPOLINE(87) +MB_TRAMPOLINE(88) +MB_TRAMPOLINE(89) +MB_TRAMPOLINE(90) +MB_TRAMPOLINE(91) +MB_TRAMPOLINE(92) +MB_TRAMPOLINE(93) +MB_TRAMPOLINE(94) +MB_TRAMPOLINE(95) +MB_TRAMPOLINE(96) +MB_TRAMPOLINE(97) +MB_TRAMPOLINE(98) +MB_TRAMPOLINE(99) +MB_TRAMPOLINE(100) +MB_TRAMPOLINE(101) +MB_TRAMPOLINE(102) +MB_TRAMPOLINE(103) +MB_TRAMPOLINE(104) +MB_TRAMPOLINE(105) +MB_TRAMPOLINE(106) +MB_TRAMPOLINE(107) +MB_TRAMPOLINE(108) +MB_TRAMPOLINE(109) +MB_TRAMPOLINE(110) +MB_TRAMPOLINE(111) +MB_TRAMPOLINE(112) +MB_TRAMPOLINE(113) +MB_TRAMPOLINE(114) +MB_TRAMPOLINE(115) +MB_TRAMPOLINE(116) +MB_TRAMPOLINE(117) +MB_TRAMPOLINE(118) +MB_TRAMPOLINE(119) +MB_TRAMPOLINE(120) +MB_TRAMPOLINE(121) +MB_TRAMPOLINE(122) +MB_TRAMPOLINE(123) +MB_TRAMPOLINE(124) +MB_TRAMPOLINE(125) +MB_TRAMPOLINE(126) +MB_TRAMPOLINE(127) +MB_TRAMPOLINE(128) +MB_TRAMPOLINE(129) +MB_TRAMPOLINE(130) +MB_TRAMPOLINE(131) +MB_TRAMPOLINE(132) +MB_TRAMPOLINE(133) +MB_TRAMPOLINE(134) +MB_TRAMPOLINE(135) +MB_TRAMPOLINE(136) +MB_TRAMPOLINE(137) +MB_TRAMPOLINE(138) +MB_TRAMPOLINE(139) +MB_TRAMPOLINE(140) +MB_TRAMPOLINE(141) +MB_TRAMPOLINE(142) +MB_TRAMPOLINE(143) +MB_TRAMPOLINE(144) +MB_TRAMPOLINE(145) +MB_TRAMPOLINE(146) +MB_TRAMPOLINE(147) +MB_TRAMPOLINE(148) +MB_TRAMPOLINE(149) +MB_TRAMPOLINE(150) +MB_TRAMPOLINE(151) +MB_TRAMPOLINE(152) +MB_TRAMPOLINE(153) +MB_TRAMPOLINE(154) +MB_TRAMPOLINE(155) +MB_TRAMPOLINE(156) +MB_TRAMPOLINE(157) +MB_TRAMPOLINE(158) +MB_TRAMPOLINE(159) +MB_TRAMPOLINE(160) +MB_TRAMPOLINE(161) +MB_TRAMPOLINE(162) +MB_TRAMPOLINE(163) +MB_TRAMPOLINE(164) +MB_TRAMPOLINE(165) +MB_TRAMPOLINE(166) +MB_TRAMPOLINE(167) +MB_TRAMPOLINE(168) +MB_TRAMPOLINE(169) +MB_TRAMPOLINE(170) +MB_TRAMPOLINE(171) +MB_TRAMPOLINE(172) +MB_TRAMPOLINE(173) +MB_TRAMPOLINE(174) +MB_TRAMPOLINE(175) +MB_TRAMPOLINE(176) +MB_TRAMPOLINE(177) +MB_TRAMPOLINE(178) +MB_TRAMPOLINE(179) +MB_TRAMPOLINE(180) +MB_TRAMPOLINE(181) +MB_TRAMPOLINE(182) +MB_TRAMPOLINE(183) +MB_TRAMPOLINE(184) +MB_TRAMPOLINE(185) +MB_TRAMPOLINE(186) +MB_TRAMPOLINE(187) +MB_TRAMPOLINE(188) +MB_TRAMPOLINE(189) +MB_TRAMPOLINE(190) +MB_TRAMPOLINE(191) +MB_TRAMPOLINE(192) +MB_TRAMPOLINE(193) +MB_TRAMPOLINE(194) +MB_TRAMPOLINE(195) +MB_TRAMPOLINE(196) +MB_TRAMPOLINE(197) +MB_TRAMPOLINE(198) +MB_TRAMPOLINE(199) +MB_TRAMPOLINE(200) +MB_TRAMPOLINE(201) +MB_TRAMPOLINE(202) +MB_TRAMPOLINE(203) +MB_TRAMPOLINE(204) +MB_TRAMPOLINE(205) +MB_TRAMPOLINE(206) +MB_TRAMPOLINE(207) +MB_TRAMPOLINE(208) +MB_TRAMPOLINE(209) +MB_TRAMPOLINE(210) +MB_TRAMPOLINE(211) +MB_TRAMPOLINE(212) +MB_TRAMPOLINE(213) +MB_TRAMPOLINE(214) +MB_TRAMPOLINE(215) +MB_TRAMPOLINE(216) +MB_TRAMPOLINE(217) +MB_TRAMPOLINE(218) +MB_TRAMPOLINE(219) +MB_TRAMPOLINE(220) +MB_TRAMPOLINE(221) +MB_TRAMPOLINE(222) +MB_TRAMPOLINE(223) +MB_TRAMPOLINE(224) +MB_TRAMPOLINE(225) +MB_TRAMPOLINE(226) +MB_TRAMPOLINE(227) +MB_TRAMPOLINE(228) +MB_TRAMPOLINE(229) +MB_TRAMPOLINE(230) +MB_TRAMPOLINE(231) +MB_TRAMPOLINE(232) +MB_TRAMPOLINE(233) +MB_TRAMPOLINE(234) +MB_TRAMPOLINE(235) +MB_TRAMPOLINE(236) +MB_TRAMPOLINE(237) +MB_TRAMPOLINE(238) +MB_TRAMPOLINE(239) +MB_TRAMPOLINE(240) +MB_TRAMPOLINE(241) +MB_TRAMPOLINE(242) +MB_TRAMPOLINE(243) +MB_TRAMPOLINE(244) +MB_TRAMPOLINE(245) +MB_TRAMPOLINE(246) +MB_TRAMPOLINE(247) +MB_TRAMPOLINE(248) +MB_TRAMPOLINE(249) +MB_TRAMPOLINE(250) +MB_TRAMPOLINE(251) +MB_TRAMPOLINE(252) +MB_TRAMPOLINE(253) +MB_TRAMPOLINE(254) +MB_TRAMPOLINE(255) #undef MB_TRAMPOLINE static const mb_func_t mbTrampTable[MB_BANK_SIZE] = { - mbTramp0, mbTramp1, mbTramp2, mbTramp3, mbTramp4, mbTramp5, mbTramp6, mbTramp7, - mbTramp8, mbTramp9, mbTramp10, mbTramp11, mbTramp12, mbTramp13, mbTramp14, mbTramp15, - mbTramp16, mbTramp17, mbTramp18, mbTramp19, mbTramp20, mbTramp21, mbTramp22, mbTramp23, - mbTramp24, mbTramp25, mbTramp26, mbTramp27, mbTramp28, mbTramp29, mbTramp30, mbTramp31 + mbTramp0, mbTramp1, mbTramp2, mbTramp3, mbTramp4, mbTramp5, mbTramp6, mbTramp7, + mbTramp8, mbTramp9, mbTramp10, mbTramp11, mbTramp12, mbTramp13, mbTramp14, mbTramp15, + mbTramp16, mbTramp17, mbTramp18, mbTramp19, mbTramp20, mbTramp21, mbTramp22, mbTramp23, + mbTramp24, mbTramp25, mbTramp26, mbTramp27, mbTramp28, mbTramp29, mbTramp30, mbTramp31, + mbTramp32, mbTramp33, mbTramp34, mbTramp35, mbTramp36, mbTramp37, mbTramp38, mbTramp39, + mbTramp40, mbTramp41, mbTramp42, mbTramp43, mbTramp44, mbTramp45, mbTramp46, mbTramp47, + mbTramp48, mbTramp49, mbTramp50, mbTramp51, mbTramp52, mbTramp53, mbTramp54, mbTramp55, + mbTramp56, mbTramp57, mbTramp58, mbTramp59, mbTramp60, mbTramp61, mbTramp62, mbTramp63, + mbTramp64, mbTramp65, mbTramp66, mbTramp67, mbTramp68, mbTramp69, mbTramp70, mbTramp71, + mbTramp72, mbTramp73, mbTramp74, mbTramp75, mbTramp76, mbTramp77, mbTramp78, mbTramp79, + mbTramp80, mbTramp81, mbTramp82, mbTramp83, mbTramp84, mbTramp85, mbTramp86, mbTramp87, + mbTramp88, mbTramp89, mbTramp90, mbTramp91, mbTramp92, mbTramp93, mbTramp94, mbTramp95, + mbTramp96, mbTramp97, mbTramp98, mbTramp99, mbTramp100, mbTramp101, mbTramp102, mbTramp103, + mbTramp104, mbTramp105, mbTramp106, mbTramp107, mbTramp108, mbTramp109, mbTramp110, mbTramp111, + mbTramp112, mbTramp113, mbTramp114, mbTramp115, mbTramp116, mbTramp117, mbTramp118, mbTramp119, + mbTramp120, mbTramp121, mbTramp122, mbTramp123, mbTramp124, mbTramp125, mbTramp126, mbTramp127, + mbTramp128, mbTramp129, mbTramp130, mbTramp131, mbTramp132, mbTramp133, mbTramp134, mbTramp135, + mbTramp136, mbTramp137, mbTramp138, mbTramp139, mbTramp140, mbTramp141, mbTramp142, mbTramp143, + mbTramp144, mbTramp145, mbTramp146, mbTramp147, mbTramp148, mbTramp149, mbTramp150, mbTramp151, + mbTramp152, mbTramp153, mbTramp154, mbTramp155, mbTramp156, mbTramp157, mbTramp158, mbTramp159, + mbTramp160, mbTramp161, mbTramp162, mbTramp163, mbTramp164, mbTramp165, mbTramp166, mbTramp167, + mbTramp168, mbTramp169, mbTramp170, mbTramp171, mbTramp172, mbTramp173, mbTramp174, mbTramp175, + mbTramp176, mbTramp177, mbTramp178, mbTramp179, mbTramp180, mbTramp181, mbTramp182, mbTramp183, + mbTramp184, mbTramp185, mbTramp186, mbTramp187, mbTramp188, mbTramp189, mbTramp190, mbTramp191, + mbTramp192, mbTramp193, mbTramp194, mbTramp195, mbTramp196, mbTramp197, mbTramp198, mbTramp199, + mbTramp200, mbTramp201, mbTramp202, mbTramp203, mbTramp204, mbTramp205, mbTramp206, mbTramp207, + mbTramp208, mbTramp209, mbTramp210, mbTramp211, mbTramp212, mbTramp213, mbTramp214, mbTramp215, + mbTramp216, mbTramp217, mbTramp218, mbTramp219, mbTramp220, mbTramp221, mbTramp222, mbTramp223, + mbTramp224, mbTramp225, mbTramp226, mbTramp227, mbTramp228, mbTramp229, mbTramp230, mbTramp231, + mbTramp232, mbTramp233, mbTramp234, mbTramp235, mbTramp236, mbTramp237, mbTramp238, mbTramp239, + mbTramp240, mbTramp241, mbTramp242, mbTramp243, mbTramp244, mbTramp245, mbTramp246, mbTramp247, + mbTramp248, mbTramp249, mbTramp250, mbTramp251, mbTramp252, mbTramp253, mbTramp254, mbTramp255 }; diff --git a/src/s7/s7Adapter.c b/src/s7/s7Adapter.c index 3f6cb3c..6cc24a2 100644 --- a/src/s7/s7Adapter.c +++ b/src/s7/s7Adapter.c @@ -5,10 +5,11 @@ // "report" a), so the dispatcher recovers the name from its first argument and the // context from a hidden *calog-context* c-pointer, marshals the rest to CalogValueT, and // dispatches through calogCall (honoring the actor route hook). Marshalling covers -// scalars, binary-safe strings, and aggregates crossing out (a Scheme list, or a -// hash-table when keyed, so a record reads as (user "name")); reading a script's keyed -// value back in is a v1 limit. A Scheme procedure crossing out becomes a refcounted -// CalogFnT kept alive by s7_gc_protect (released with s7_gc_unprotect_at). +// scalars, binary-safe strings, and aggregates both ways (out: a Scheme list, or a +// hash-table when keyed, so a record reads as (user "name"); in: a list or a hash-table +// iterated back into an aggregate). Functions cross both ways: a Scheme procedure out is +// a refcounted CalogFnT kept alive by s7_gc_protect; a foreign CalogFnT pushed in is an +// applicable c-object the script calls as (f ...). // // Error model: a native failure raises a Scheme error via s7_error. calogS7Run wraps the // source in (catch #t (lambda () (eval-string ...)) handler) so both read and run errors @@ -34,6 +35,7 @@ struct CalogS7T { s7_scheme *sc; CalogT *broker; uint64_t ctxId; + s7_int fnType; // c-object type for a foreign CalogFnT pushed into Scheme (applicable + freed) }; // Backs a CalogFnT exported from this interpreter: the owning context, the protected @@ -49,6 +51,8 @@ static void s7CallableRelease(CalogFnT *callable); static s7_pointer s7DispatchNative(s7_scheme *sc, s7_pointer args); static char *s7EscapeSource(const char *source); static int32_t s7ExportValue(CalogS7T *context, s7_pointer proc, CalogFnT **out); +static s7_pointer s7ForeignApply(s7_scheme *sc, s7_pointer args); +static void s7ForeignFree(void *value); static s7_pointer s7FromValue(CalogS7T *context, const CalogValueT *value, int32_t depth); static int32_t s7ToValue(CalogS7T *context, s7_pointer obj, CalogValueT *out, int32_t depth); @@ -72,6 +76,11 @@ int32_t calogS7Create(CalogS7T **out, CalogT *broker, uint64_t ctxId) { // single generic native dispatcher. s7_define_variable(context->sc, S7_CONTEXT_VAR, s7_make_c_pointer(context->sc, context)); s7_define_function(context->sc, "%calog-call", s7DispatchNative, 1, 0, true, "calog native dispatch"); + // A c-object type for a foreign function pushed into Scheme: applicable via its ref + // hook (so (f 2 3) invokes it) and released by its free hook. + context->fnType = s7_make_c_type(context->sc, "calog-fn"); + s7_c_type_set_ref(context->sc, context->fnType, s7ForeignApply); + s7_c_type_set_free(context->sc, context->fnType, s7ForeignFree); *out = context; return calogOkE; } @@ -262,6 +271,72 @@ int32_t calogS7Expose(CalogS7T *context, const char *name) { } +// Applied when a foreign-function c-object is called: args is (obj arg1 arg2 ...), so the +// first element is the object itself. Marshal the rest, invoke, and return the result. +static s7_pointer s7ForeignApply(s7_scheme *sc, s7_pointer args) { + CalogS7T *context; + CalogFnT *callable; + s7_pointer rest; + s7_pointer node; + CalogValueT *cargs; + CalogValueT result; + s7_pointer sresult; + int argCount; + int index; + int32_t status; + + context = (CalogS7T *)s7_c_pointer(s7_name_to_value(sc, S7_CONTEXT_VAR)); + callable = (CalogFnT *)s7_c_object_value(s7_car(args)); + rest = s7_cdr(args); + argCount = s7_list_length(sc, rest); + cargs = NULL; + if (argCount > 0) { + cargs = (CalogValueT *)calloc((size_t)argCount, sizeof(CalogValueT)); + if (cargs == NULL) { + return s7_error(sc, s7_make_symbol(sc, "memory-error"), s7_make_string(sc, "out of memory marshalling function-value args")); + } + } + node = rest; + for (index = 0; index < argCount; index++) { + status = s7ToValue(context, s7_car(node), &cargs[index], 0); + if (status != calogOkE) { + int cleanup; + for (cleanup = 0; cleanup < index; cleanup++) { + calogValueFree(&cargs[cleanup]); + } + free(cargs); + return s7_error(sc, s7_make_symbol(sc, "wrong-type-arg"), s7_make_string(sc, "failed to marshal a function-value argument")); + } + node = s7_cdr(node); + } + status = calogFnInvoke(callable, cargs, argCount, &result); + for (index = 0; index < argCount; index++) { + calogValueFree(&cargs[index]); + } + free(cargs); + if (status != calogOkE) { + s7_pointer message; + message = s7_make_string(sc, (result.type == calogStringE) ? result.as.s.bytes : "function value failed"); + calogValueFree(&result); + return s7_error(sc, s7_make_symbol(sc, "calog-error"), message); + } + sresult = s7FromValue(context, &result, 0); + calogValueFree(&result); + return sresult; +} + + +// Free hook for that c-object: drop the reference the push took. +static void s7ForeignFree(void *value) { + CalogFnT *callable; + + callable = (CalogFnT *)value; + if (callable != NULL) { + calogFnRelease(callable); + } +} + + static s7_pointer s7FromValue(CalogS7T *context, const CalogValueT *value, int32_t depth) { s7_scheme *sc; @@ -322,9 +397,14 @@ static s7_pointer s7FromValue(CalogS7T *context, const CalogValueT *value, int32 return list; } } - case calogFnE: - // Pushing a foreign function value into Scheme is a v1 limit. - return s7_unspecified(sc); + case calogFnE: { + // Wrap the foreign function in an applicable c-object; retain for its lifetime + // (the free hook releases it). + s7_pointer obj; + obj = s7_make_c_object(sc, context->fnType, value->as.fn); + calogFnRetain(value->as.fn); + return obj; + } } return s7_unspecified(sc); } @@ -398,6 +478,49 @@ static int32_t s7ToValue(CalogS7T *context, s7_pointer obj, CalogValueT *out, in calogValueFn(out, callable); return calogOkE; } + if (s7_is_hash_table(obj)) { + CalogAggT *aggregate; + s7_pointer iter; + unsigned int loc; + int32_t status; + status = calogAggCreate(&aggregate, calogMapE); + if (status != calogOkE) { + return status; + } + iter = s7_make_iterator(sc, obj); // each iterate yields a (key . value) pair + loc = s7_gc_protect(sc, iter); + while (!s7_iterator_is_at_end(sc, iter)) { + s7_pointer pair; + CalogValueT key; + CalogValueT value; + pair = s7_iterate(sc, iter); // a (key . value) cons, or a non-pair at end + if (!s7_is_pair(pair)) { + break; + } + status = s7ToValue(context, s7_car(pair), &key, depth + 1); + if (status != calogOkE) { + break; + } + status = s7ToValue(context, s7_cdr(pair), &value, depth + 1); + if (status != calogOkE) { + calogValueFree(&key); + break; + } + status = calogAggSet(aggregate, &key, &value); + if (status != calogOkE) { + calogValueFree(&key); + calogValueFree(&value); + break; + } + } + s7_gc_unprotect_at(sc, loc); + if (status != calogOkE) { + calogAggFree(aggregate); + return status; + } + calogValueAgg(out, aggregate); + return calogOkE; + } if (s7_is_null(sc, obj)) { CalogAggT *aggregate; int32_t status; @@ -435,6 +558,6 @@ static int32_t s7ToValue(CalogS7T *context, s7_pointer obj, CalogValueT *out, in calogValueAgg(out, aggregate); return calogOkE; } - // #, eof, symbols, hash-tables, etc. -> nil (v1). + // #, eof, symbols, etc. -> nil. return calogOkE; } diff --git a/src/s7/s7Adapter.h b/src/s7/s7Adapter.h index 2ad7d27..19a5199 100644 --- a/src/s7/s7Adapter.h +++ b/src/s7/s7Adapter.h @@ -1,11 +1,11 @@ // s7Adapter.h -- s7 Scheme engine adapter for the broker. // // Exposes broker-registered native functions into an s7 interpreter, marshals values -// between Scheme and CalogValueT by value (binary-safe strings; scalars; aggregates -// cross OUT as a Scheme list, or a hash-table when keyed so a record reads as (user -// "name")), and exports Scheme procedures as refcounted CalogFnT handles (kept alive by -// s7_gc_protect, released with s7_gc_unprotect_at). Reading a script's keyed value (map) -// back IN is a v1 limit -> calogErrUnsupportedE. +// between Scheme and CalogValueT by value (binary-safe strings; scalars; aggregates both +// ways -- OUT as a Scheme list or a hash-table when keyed so a record reads as (user +// "name"), IN by iterating a list or hash-table back), and moves Scheme procedures out as +// refcounted CalogFnT handles (kept alive by s7_gc_protect). A foreign CalogFnT pushed IN +// becomes an applicable c-object the script calls as (f ...). // // Lifetime note (as with Lua/JS): release every CalogFnT obtained from calogS7Export // (and drop every procedure value marshalled out) BEFORE calogS7Destroy, since the diff --git a/src/squirrel/squirrelAdapter.c b/src/squirrel/squirrelAdapter.c index 71d72ee..708b4c2 100644 --- a/src/squirrel/squirrelAdapter.c +++ b/src/squirrel/squirrelAdapter.c @@ -52,6 +52,8 @@ static int32_t squirrelCallableInvoke(CalogValueT *args, int32_t argCount, Calo static void squirrelCallableRelease(CalogFnT *callable); static CalogSquirrelT *squirrelContextOf(HSQUIRRELVM v); static int32_t squirrelExportAt(CalogSquirrelT *context, SQInteger idx, CalogFnT **out); +static SQInteger squirrelForeignCall(HSQUIRRELVM v); +static SQInteger squirrelForeignRelease(SQUserPointer p, SQInteger size); static void squirrelPrint(HSQUIRRELVM v, const SQChar *format, ...); static int32_t squirrelPushValueDepth(HSQUIRRELVM v, const CalogValueT *value, int32_t depth); static int32_t squirrelToValueDepth(HSQUIRRELVM v, SQInteger idx, CalogValueT *out, int32_t depth); @@ -249,6 +251,78 @@ int32_t calogSquirrelExpose(CalogSquirrelT *context, const char *name) { } +// Call trampoline for a foreign CalogFnT pushed into Squirrel as a native closure. Its +// single free variable is a release-hooked userdata holding the CalogFnT; the VM pushes +// that free variable after the arguments, so it sits at the top of the stack. +static SQInteger squirrelForeignCall(HSQUIRRELVM v) { + CalogFnT *callable; + SQUserPointer ptr; + CalogValueT *args; + CalogValueT result; + SQInteger top; + int32_t argCount; + int32_t index; + int32_t status; + + top = sq_gettop(v); + sq_getuserdata(v, top, &ptr, NULL); + callable = *(CalogFnT **)ptr; + argCount = (int32_t)(top - 2); // 'this' at 1, args at 2..top-1, free variable at top + if (argCount < 0) { + argCount = 0; + } + args = NULL; + if (argCount > 0) { + args = (CalogValueT *)calloc((size_t)argCount, sizeof(CalogValueT)); + if (args == NULL) { + return sq_throwerror(v, _SC("out of memory marshalling function-value args")); + } + } + for (index = 0; index < argCount; index++) { + status = squirrelToValueDepth(v, index + 2, &args[index], 0); + if (status != calogOkE) { + int32_t cleanup; + for (cleanup = 0; cleanup < index; cleanup++) { + calogValueFree(&args[cleanup]); + } + free(args); + return sq_throwerror(v, _SC("failed to marshal a function-value argument")); + } + } + status = calogFnInvoke(callable, args, argCount, &result); + for (index = 0; index < argCount; index++) { + calogValueFree(&args[index]); + } + free(args); + if (status != calogOkE) { + const SQChar *message; + message = (result.type == calogStringE) ? result.as.s.bytes : _SC("function value failed"); + sq_throwerror(v, message); + calogValueFree(&result); + return SQ_ERROR; + } + status = squirrelPushValueDepth(v, &result, 0); + calogValueFree(&result); + if (status != calogOkE) { + return sq_throwerror(v, _SC("failed to marshal a function-value result")); + } + return 1; +} + + +// Release hook on that userdata: drop the reference the push took. +static SQInteger squirrelForeignRelease(SQUserPointer p, SQInteger size) { + CalogFnT *callable; + + (void)size; + callable = *(CalogFnT **)p; + if (callable != NULL) { + calogFnRelease(callable); + } + return 1; +} + + static void squirrelPrint(HSQUIRRELVM v, const SQChar *format, ...) { va_list args; @@ -342,10 +416,21 @@ static int32_t squirrelPushValueDepth(HSQUIRRELVM v, const CalogValueT *value, i } return calogOkE; } - case calogFnE: - // Pushing a foreign function value INTO Squirrel is the remaining v1 - // limit (export OUT is supported via calogSquirrelExport); see the header. - return calogErrUnsupportedE; + case calogFnE: { + // Wrap the foreign function in a native closure whose one free variable is a + // release-hooked userdata holding the CalogFnT (released when the closure is + // collected). Retain for the wrapper's lifetime. + CalogFnT **slot; + slot = (CalogFnT **)sq_newuserdata(v, sizeof(CalogFnT *)); + if (slot == NULL) { + return calogErrOomE; + } + *slot = value->as.fn; + sq_setreleasehook(v, -1, squirrelForeignRelease); + sq_newclosure(v, squirrelForeignCall, 1); // pops the userdata as the free variable + calogFnRetain(value->as.fn); + return calogOkE; + } } return calogErrTypeE; } diff --git a/src/squirrel/squirrelAdapter.h b/src/squirrel/squirrelAdapter.h index e1117db..ef6c3ec 100644 --- a/src/squirrel/squirrelAdapter.h +++ b/src/squirrel/squirrelAdapter.h @@ -8,13 +8,10 @@ // A Squirrel closure crossing the boundary becomes a refcounted CalogFnT over a // pinned HSQOBJECT (sq_addref/sq_release, mirroring Lua's luaL_ref lifecycle): // calogSquirrelExport fetches a named global closure, and a closure passed as a native -// argument is exported the same way during ingress marshalling. As with Lua, -// release every exported CalogFnT BEFORE calogSquirrelDestroy, since sq_release -// touches the VM. -// -// Remaining v1 fidelity limit: the reverse direction (a foreign CalogFnT -// marshalled INTO Squirrel so a script can call it) is not yet supported -- pushing -// such a value returns calogErrUnsupportedE. +// argument is exported the same way during ingress marshalling. A foreign CalogFnT going +// the other way (pushed INTO Squirrel) becomes a native closure whose one free variable +// is a release-hooked userdata, so a script can call it directly. As with Lua, release +// every exported CalogFnT BEFORE calogSquirrelDestroy, since sq_release touches the VM. #ifndef SQUIRREL_ADAPTER_H #define SQUIRREL_ADAPTER_H diff --git a/src/value.c b/src/value.c index 2506ffc..f0dbc8c 100644 --- a/src/value.c +++ b/src/value.c @@ -196,6 +196,12 @@ int32_t calogFnCreate(CalogFnT **out, CalogT *runtime, CalogNativeFnT fn, void * } +int32_t calogFnFromNative(CalogFnT **out, CalogT *calog, CalogNativeFnT fn, void *userData) { + // A host-owned callable (owner id 0 = host): no engine handle to release. + return calogFnCreate(out, calog, fn, userData, NULL, 0); +} + + void calogFnFinalize(CalogFnT *callable) { if (callable == NULL) { return; diff --git a/src/wren/wrenAdapter.c b/src/wren/wrenAdapter.c index 647e8c0..a853ade 100644 --- a/src/wren/wrenAdapter.c +++ b/src/wren/wrenAdapter.c @@ -44,15 +44,27 @@ typedef struct WrenExportT { static int32_t wrenCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static void wrenCallableRelease(CalogFnT *callable); static WrenHandle *wrenCallHandle(CalogWrenT *context, int arity); +static WrenForeignClassMethods wrenBindClass(WrenVM *vm, const char *module, const char *className); static WrenForeignMethodFn wrenBindMethod(WrenVM *vm, const char *module, const char *className, bool isStatic, const char *signature); static void wrenDispatch(WrenVM *vm); static void wrenError(WrenVM *vm, WrenErrorType type, const char *module, int line, const char *message); static int32_t wrenExportSlot(CalogWrenT *context, int slot, CalogFnT **out); +static void wrenForeignAllocate(WrenVM *vm); +static void wrenForeignFinalize(void *data); +static void wrenForeignInvoke(WrenVM *vm); static void wrenFromValue(CalogWrenT *context, const CalogValueT *value, int slot, int32_t depth); static int32_t wrenToValue(CalogWrenT *context, int slot, CalogValueT *out, int32_t depth); static void wrenWrite(WrenVM *vm, const char *text); -static const char WREN_PREAMBLE[] = "class Calog { foreign static call(name, args) }"; +// A foreign function pushed into Wren becomes a CalogFn instance; scripts invoke it as +// f.call([args]) (a list, since Wren method arity is fixed). Calog.call(name, args) +// reaches natives (Wren has no bare function calls). +static const char WREN_PREAMBLE[] = + "class Calog { foreign static call(name, args) }\n" + "foreign class CalogFn {\n" + " construct new() {}\n" + " foreign call(args)\n" + "}"; int32_t calogWrenCreate(CalogWrenT **out, CalogT *broker, uint64_t ctxId) { @@ -69,6 +81,7 @@ int32_t calogWrenCreate(CalogWrenT **out, CalogT *broker, uint64_t ctxId) { wrenInitConfiguration(&config); config.userData = context; config.bindForeignMethodFn = wrenBindMethod; + config.bindForeignClassFn = wrenBindClass; config.errorFn = wrenError; config.writeFn = wrenWrite; context->vm = wrenNewVM(&config); @@ -174,12 +187,30 @@ void calogWrenDestroy(CalogWrenT *context) { } +static WrenForeignClassMethods wrenBindClass(WrenVM *vm, const char *module, const char *className) { + WrenForeignClassMethods methods; + + (void)vm; + (void)module; + methods.allocate = NULL; + methods.finalize = NULL; + if (strcmp(className, "CalogFn") == 0) { + methods.allocate = wrenForeignAllocate; + methods.finalize = wrenForeignFinalize; + } + return methods; +} + + static WrenForeignMethodFn wrenBindMethod(WrenVM *vm, const char *module, const char *className, bool isStatic, const char *signature) { (void)vm; (void)module; if (strcmp(className, "Calog") == 0 && isStatic && strcmp(signature, "call(_,_)") == 0) { return wrenDispatch; } + if (strcmp(className, "CalogFn") == 0 && !isStatic && strcmp(signature, "call(_)") == 0) { + return wrenForeignInvoke; + } return NULL; } @@ -302,6 +333,82 @@ int32_t calogWrenExpose(CalogWrenT *context, const char *name) { } +// Allocate hook for CalogFn (only reached if a script does CalogFn.new(); the host +// creates instances directly with wrenSetSlotNewForeign). Start empty. +static void wrenForeignAllocate(WrenVM *vm) { + CalogFnT **data; + + data = (CalogFnT **)wrenSetSlotNewForeign(vm, 0, 0, sizeof(CalogFnT *)); + *data = NULL; +} + + +// Finalizer for a CalogFn instance: drop the reference the push took. +static void wrenForeignFinalize(void *data) { + CalogFnT *callable; + + callable = *(CalogFnT **)data; + if (callable != NULL) { + calogFnRelease(callable); + } +} + + +// CalogFn.call(args): invoke the wrapped function value with the argument list. +static void wrenForeignInvoke(WrenVM *vm) { + CalogWrenT *context; + CalogFnT *callable; + CalogValueT *cargs; + CalogValueT result; + int argCount; + int index; + int32_t status; + char message[WREN_ERR_CAP]; + + context = (CalogWrenT *)wrenGetUserData(vm); + callable = *(CalogFnT **)wrenGetSlotForeign(vm, 0); // slot 0 is the CalogFn instance + argCount = wrenGetListCount(vm, 1); // slot 1 is the argument list + cargs = NULL; + if (argCount > 0) { + cargs = (CalogValueT *)calloc((size_t)argCount, sizeof(CalogValueT)); + if (cargs == NULL) { + wrenSetSlotString(vm, 0, "out of memory marshalling function-value args"); + wrenAbortFiber(vm, 0); + return; + } + } + wrenEnsureSlots(vm, 3); // slot 2 is a scratch slot for list elements + for (index = 0; index < argCount; index++) { + wrenGetListElement(vm, 1, index, 2); + status = wrenToValue(context, 2, &cargs[index], 0); + if (status != calogOkE) { + int cleanup; + for (cleanup = 0; cleanup < index; cleanup++) { + calogValueFree(&cargs[cleanup]); + } + free(cargs); + wrenSetSlotString(vm, 0, "failed to marshal a function-value argument"); + wrenAbortFiber(vm, 0); + return; + } + } + status = calogFnInvoke(callable, cargs, argCount, &result); + for (index = 0; index < argCount; index++) { + calogValueFree(&cargs[index]); + } + free(cargs); + if (status != calogOkE) { + snprintf(message, sizeof(message), "%s", (result.type == calogStringE) ? result.as.s.bytes : "function value failed"); + calogValueFree(&result); + wrenSetSlotString(vm, 0, message); + wrenAbortFiber(vm, 0); + return; + } + wrenFromValue(context, &result, 0, 0); // return value in slot 0 + calogValueFree(&result); +} + + static void wrenFromValue(CalogWrenT *context, const CalogValueT *value, int slot, int32_t depth) { WrenVM *vm; @@ -365,10 +472,17 @@ static void wrenFromValue(CalogWrenT *context, const CalogValueT *value, int slo } return; } - case calogFnE: - // Pushing a foreign function value into Wren is a v1 limit. - wrenSetSlotNull(vm, slot); + case calogFnE: { + // Wrap the foreign function in a CalogFn instance (script calls f.call([...])); + // retain for its lifetime -- the finalizer releases it. + CalogFnT **data; + wrenEnsureSlots(vm, slot + 2); + wrenGetVariable(vm, "main", "CalogFn", slot + 1); + data = (CalogFnT **)wrenSetSlotNewForeign(vm, slot, slot + 1, sizeof(CalogFnT *)); + *data = value->as.fn; + calogFnRetain(value->as.fn); return; + } } wrenSetSlotNull(vm, slot); } @@ -451,8 +565,49 @@ static int32_t wrenToValue(CalogWrenT *context, int slot, CalogValueT *out, int3 calogValueAgg(out, aggregate); return calogOkE; } - case WREN_TYPE_MAP: - return calogErrUnsupportedE; // map read is a v1 limit (no key enumeration in the C API) + case WREN_TYPE_MAP: { + // Enumerate the map via the calog patch (wrenGetMapCapacity/wrenGetMapEntry): + // walk the raw table, skipping empty slots. slot+1 = key, slot+2 = value + // scratch; marshal the value before the key so a nested-aggregate key can't + // clobber the value slot. + CalogAggT *aggregate; + int capacity; + int index; + int32_t status; + status = calogAggCreate(&aggregate, calogMapE); + if (status != calogOkE) { + return status; + } + capacity = wrenGetMapCapacity(vm, slot); + wrenEnsureSlots(vm, slot + 3); + for (index = 0; index < capacity; index++) { + CalogValueT key; + CalogValueT value; + if (!wrenGetMapEntry(vm, slot, index, slot + 1, slot + 2)) { + continue; + } + status = wrenToValue(context, slot + 2, &value, depth + 1); + if (status != calogOkE) { + calogAggFree(aggregate); + return status; + } + status = wrenToValue(context, slot + 1, &key, depth + 1); + if (status != calogOkE) { + calogValueFree(&value); + calogAggFree(aggregate); + return status; + } + status = calogAggSet(aggregate, &key, &value); + if (status != calogOkE) { + calogValueFree(&key); + calogValueFree(&value); + calogAggFree(aggregate); + return status; + } + } + calogValueAgg(out, aggregate); + return calogOkE; + } case WREN_TYPE_FOREIGN: case WREN_TYPE_UNKNOWN: { // A non-primitive (typically a Fn) is captured as a callable. diff --git a/src/wren/wrenAdapter.h b/src/wren/wrenAdapter.h index b78e7a8..3ed8831 100644 --- a/src/wren/wrenAdapter.h +++ b/src/wren/wrenAdapter.h @@ -2,11 +2,12 @@ // // Wren has no bare function calls, so natives are reached through one foreign method: // scripts call Calog.call("name", [args]). The adapter marshals values between Wren and -// CalogValueT by value (binary-safe strings; scalars; an aggregate crosses OUT as a Wren -// List, or a Map when keyed so a record reads as user["name"]) and exports Wren functions -// as refcounted CalogFnT handles (a retained WrenHandle, released with wrenReleaseHandle). -// Numbers are IEEE doubles, so int64 magnitudes above 2^53 lose precision. Reading a -// script's list/map back IN is a v1 limit (Wren's C API cannot enumerate a Map's keys). +// CalogValueT by value (binary-safe strings; scalars; aggregates both ways -- OUT as a +// Wren List, or a Map when keyed so a record reads as user["name"]; IN a List or a Map, +// the Map via a small calog patch to wren.c that enumerates map keys, which upstream's C +// API cannot). Functions cross both ways: OUT as a refcounted CalogFnT over a retained +// WrenHandle; a foreign CalogFnT pushed IN becomes a CalogFn instance the script calls as +// f.call([...]). Numbers are IEEE doubles, so int64 magnitudes above 2^53 lose precision. // // Lifetime note (as with Lua/JS): release every CalogFnT obtained from calogWrenExport // (and drop every function value marshalled out) BEFORE calogWrenDestroy, since the diff --git a/tests/testEngineBerry.c b/tests/testEngineBerry.c index d01e05d..2ceefaf 100644 --- a/tests/testEngineBerry.c +++ b/tests/testEngineBerry.c @@ -32,9 +32,12 @@ static int32_t testsRun = 0; static int32_t testsFailed = 0; static void checkImpl(bool condition, const char *message, const char *file, int32_t line); +static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); +static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); +static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); @@ -43,7 +46,9 @@ static void onError(uint64_t contextId, const char *message, void *userData); static void pumpUntilDone(void); static void testConcurrentContexts(void); static void testCrossThreadCallback(void); +static void testForeignFunction(void); static void testHostAndInlineNatives(void); +static void testMapIngress(void); static void testMaterializedRecord(void); static void testScriptError(void); @@ -57,6 +62,17 @@ static void checkImpl(bool condition, const char *message, const char *file, int } +static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + (void)userData; + calogValueNil(result); + 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; +} + + static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; @@ -77,6 +93,24 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu } +static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + CalogFnT *callable; + int32_t status; + + (void)args; + (void)argCount; + (void)userData; + calogValueNil(result); + // A host-owned function value handed to the script; calling it routes back here. + status = calogFnFromNative(&callable, calog, nativeAdd, NULL); + if (status != calogOkE) { + return calogFail(result, status, "getAdder could not allocate"); + } + calogValueFn(result, callable); + return calogOkE; +} + + static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogAggT *user; CalogValueT key; @@ -102,6 +136,26 @@ static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT * } +static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + CalogValueT key; + CalogValueT *found; + + (void)userData; + calogValueNil(result); + if (argCount != 1 || args[0].type != calogAggE) { + return calogFail(result, calogErrArgE, "mapAge expects a map"); + } + // Read a field out of a map the script built and handed to C (aggregate ingress). + calogValueString(&key, "age", 3); + found = calogAggGet(args[0].as.agg, &key); + calogValueFree(&key); + if (found != NULL && found->type == calogIntE) { + atomic_store(&reportedValue, found->as.i); + } + return calogOkE; +} + + static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); @@ -215,6 +269,39 @@ static void testMaterializedRecord(void) { } +static void testMapIngress(void) { + CalogContextT *ctx; + + ctx = calogContextOpen(calog, &calogBerryEngine); + atomic_store(&scriptDone, false); + atomic_store(&reportedValue, 0); + // The script builds a map (with a nested list, exercising list ingress too) and + // hands it to a native, which reads a field. + calogContextEval(ctx, "m = {'age': 9, 'tags': [1, 2]}\nmapAge(m)\ndone()"); + pumpUntilDone(); + + CHECK(atomic_load(&reportedValue) == 9, "native read a field from a map the script built"); + + calogContextClose(ctx); +} + + +static void testForeignFunction(void) { + CalogContextT *ctx; + + ctx = calogContextOpen(calog, &calogBerryEngine); + atomic_store(&scriptDone, false); + atomic_store(&reportedValue, 0); + // The script receives a host-owned function value and calls it. + calogContextEval(ctx, "u = getAdder()\nreport(u(2, 3))\ndone()"); + pumpUntilDone(); + + CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host"); + + calogContextClose(ctx); +} + + static void testCrossThreadCallback(void) { CalogContextT *ctx; CalogFnT *callback; @@ -293,10 +380,14 @@ int main(void) { calogRegister(calog, "bump", nativeBump, NULL); calogRegister(calog, "makeUser", nativeMakeUser, NULL); calogRegister(calog, "reportName", nativeReportName, NULL); + calogRegister(calog, "getAdder", nativeGetAdder, NULL); + calogRegister(calog, "mapAge", nativeMapAge, NULL); calogRegisterInline(calog, "reportInline", nativeReportInline, NULL); testHostAndInlineNatives(); testMaterializedRecord(); + testMapIngress(); + testForeignFunction(); testCrossThreadCallback(); testScriptError(); testConcurrentContexts(); diff --git a/tests/testEngineJs.c b/tests/testEngineJs.c index 851b0c8..4f646a8 100644 --- a/tests/testEngineJs.c +++ b/tests/testEngineJs.c @@ -30,8 +30,10 @@ static int32_t testsRun = 0; static int32_t testsFailed = 0; static void checkImpl(bool condition, const char *message, const char *file, int32_t line); +static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); +static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); @@ -39,6 +41,7 @@ static void onError(uint64_t contextId, const char *message, void *userData); static void pumpUntilDone(void); static void testConcurrentContexts(void); static void testCrossThreadCallback(void); +static void testForeignFunction(void); static void testHostAndInlineNatives(void); static void testScriptError(void); @@ -52,6 +55,17 @@ static void checkImpl(bool condition, const char *message, const char *file, int } +static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + (void)userData; + calogValueNil(result); + 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; +} + + static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; @@ -72,6 +86,24 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu } +static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + CalogFnT *callable; + int32_t status; + + (void)args; + (void)argCount; + (void)userData; + calogValueNil(result); + // A host-owned function value handed to the script; calling it routes back here. + status = calogFnFromNative(&callable, calog, nativeAdd, NULL); + if (status != calogOkE) { + return calogFail(result, status, "getAdder could not allocate"); + } + calogValueFn(result, callable); + return calogOkE; +} + + static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); @@ -176,6 +208,22 @@ static void testCrossThreadCallback(void) { } +static void testForeignFunction(void) { + CalogContextT *ctx; + + ctx = calogContextOpen(calog, &calogJsEngine); + atomic_store(&scriptDone, false); + atomic_store(&reportedValue, 0); + // The script receives a host-owned function value and calls it. + calogContextEval(ctx, "report(getAdder()(2, 3))\ndone()"); + pumpUntilDone(); + + CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host"); + + calogContextClose(ctx); +} + + static void testScriptError(void) { CalogContextT *ctx; int32_t before; @@ -225,10 +273,12 @@ int main(void) { calogRegister(calog, "setCb", nativeSetCb, NULL); calogRegister(calog, "done", nativeDone, NULL); calogRegister(calog, "bump", nativeBump, NULL); + calogRegister(calog, "getAdder", nativeGetAdder, NULL); calogRegisterInline(calog, "reportInline", nativeReportInline, NULL); testHostAndInlineNatives(); testCrossThreadCallback(); + testForeignFunction(); testScriptError(); testConcurrentContexts(); diff --git a/tests/testEngineLua.c b/tests/testEngineLua.c index 4828c2c..96d8bb8 100644 --- a/tests/testEngineLua.c +++ b/tests/testEngineLua.c @@ -33,9 +33,11 @@ static int32_t testsRun = 0; static int32_t testsFailed = 0; static void checkImpl(bool condition, const char *message, const char *file, int32_t line); +static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeCheckRuntime(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); +static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); @@ -44,6 +46,7 @@ static void pumpUntilDone(void); static void testConcurrentContexts(void); static void testContextLoad(void); static void testCrossThreadCallback(void); +static void testForeignFunction(void); static void testHostAndInlineNatives(void); static void testScriptError(void); static void testSingleThreadMultiRuntime(void); @@ -58,6 +61,17 @@ static void checkImpl(bool condition, const char *message, const char *file, int } +static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + (void)userData; + calogValueNil(result); + 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; +} + + static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; @@ -95,6 +109,24 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu } +static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + CalogFnT *callable; + int32_t status; + + (void)args; + (void)argCount; + (void)userData; + calogValueNil(result); + // A host-owned function value handed to the script; calling it routes back here. + status = calogFnFromNative(&callable, calog, nativeAdd, NULL); + if (status != calogOkE) { + return calogFail(result, status, "getAdder could not allocate"); + } + calogValueFn(result, callable); + return calogOkE; +} + + static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); @@ -202,6 +234,22 @@ static void testCrossThreadCallback(void) { } +static void testForeignFunction(void) { + CalogContextT *ctx; + + ctx = calogContextOpen(calog, &calogLuaEngine); + atomic_store(&scriptDone, false); + atomic_store(&reportedValue, 0); + // The script receives a host-owned function value and calls it. + calogContextEval(ctx, "report(getAdder()(2, 3)); done()"); + pumpUntilDone(); + + CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host"); + + calogContextClose(ctx); +} + + static void testScriptError(void) { CalogContextT *ctx; int32_t before; @@ -322,10 +370,12 @@ int main(void) { calogRegister(calog, "setCb", nativeSetCb, NULL); calogRegister(calog, "done", nativeDone, NULL); calogRegister(calog, "bump", nativeBump, NULL); + calogRegister(calog, "getAdder", nativeGetAdder, NULL); calogRegisterInline(calog, "reportInline", nativeReportInline, NULL); testHostAndInlineNatives(); testCrossThreadCallback(); + testForeignFunction(); testScriptError(); testConcurrentContexts(); testSingleThreadMultiRuntime(); diff --git a/tests/testEngineS7.c b/tests/testEngineS7.c index 0df17a6..512895b 100644 --- a/tests/testEngineS7.c +++ b/tests/testEngineS7.c @@ -41,9 +41,12 @@ static int32_t testsRun = 0; static int32_t testsFailed = 0; static void checkImpl(bool condition, const char *message, const char *file, int32_t line); +static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); +static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); +static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); @@ -52,7 +55,9 @@ static void onError(uint64_t contextId, const char *message, void *userData); static void pumpUntilDone(void); static void testConcurrentContexts(void); static void testCrossThreadCallback(void); +static void testForeignFunction(void); static void testHostAndInlineNatives(void); +static void testMapIngress(void); static void testMaterializedRecord(void); static void testScriptError(void); @@ -66,6 +71,17 @@ static void checkImpl(bool condition, const char *message, const char *file, int } +static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + (void)userData; + calogValueNil(result); + 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; +} + + static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; @@ -86,6 +102,24 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu } +static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + CalogFnT *callable; + int32_t status; + + (void)args; + (void)argCount; + (void)userData; + calogValueNil(result); + // A host-owned function value handed to the script; calling it routes back here. + status = calogFnFromNative(&callable, calog, nativeAdd, NULL); + if (status != calogOkE) { + return calogFail(result, status, "getAdder could not allocate"); + } + calogValueFn(result, callable); + return calogOkE; +} + + static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogAggT *user; CalogValueT key; @@ -111,6 +145,26 @@ static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT * } +static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + CalogValueT key; + CalogValueT *found; + + (void)userData; + calogValueNil(result); + if (argCount != 1 || args[0].type != calogAggE) { + return calogFail(result, calogErrArgE, "mapAge expects a map"); + } + // Read a field out of a map the script built and handed to C (aggregate ingress). + calogValueString(&key, "age", 3); + found = calogAggGet(args[0].as.agg, &key); + calogValueFree(&key); + if (found != NULL && found->type == calogIntE) { + atomic_store(&reportedValue, found->as.i); + } + return calogOkE; +} + + static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); @@ -224,6 +278,22 @@ static void testMaterializedRecord(void) { } +static void testMapIngress(void) { + CalogContextT *ctx; + + ctx = calogContextOpen(calog, &calogS7Engine); + atomic_store(&scriptDone, false); + atomic_store(&reportedValue, 0); + // The script builds a hash-table and hands it to a native, which reads a field. + calogContextEval(ctx, "(begin (define m (make-hash-table)) (hash-table-set! m \"age\" 9) (mapAge m) (done))"); + pumpUntilDone(); + + CHECK(atomic_load(&reportedValue) == 9, "native read a field from a map the script built"); + + calogContextClose(ctx); +} + + static void testCrossThreadCallback(void) { CalogContextT *ctx; CalogFnT *callback; @@ -251,6 +321,22 @@ static void testCrossThreadCallback(void) { } +static void testForeignFunction(void) { + CalogContextT *ctx; + + ctx = calogContextOpen(calog, &calogS7Engine); + atomic_store(&scriptDone, false); + atomic_store(&reportedValue, 0); + // The script receives a host-owned function value and applies it. + calogContextEval(ctx, "(begin (report ((getAdder) 2 3)) (done))"); + pumpUntilDone(); + + CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host"); + + calogContextClose(ctx); +} + + static void testScriptError(void) { CalogContextT *ctx; int32_t before; @@ -302,11 +388,15 @@ int main(void) { calogRegister(calog, "bump", nativeBump, NULL); calogRegister(calog, "makeUser", nativeMakeUser, NULL); calogRegister(calog, "reportName", nativeReportName, NULL); + calogRegister(calog, "getAdder", nativeGetAdder, NULL); + calogRegister(calog, "mapAge", nativeMapAge, NULL); calogRegisterInline(calog, "reportInline", nativeReportInline, NULL); testHostAndInlineNatives(); testMaterializedRecord(); + testMapIngress(); testCrossThreadCallback(); + testForeignFunction(); testScriptError(); testConcurrentContexts(); diff --git a/tests/testEngineSquirrel.c b/tests/testEngineSquirrel.c index 8a7c902..5645261 100644 --- a/tests/testEngineSquirrel.c +++ b/tests/testEngineSquirrel.c @@ -30,8 +30,10 @@ static int32_t testsRun = 0; static int32_t testsFailed = 0; static void checkImpl(bool condition, const char *message, const char *file, int32_t line); +static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); +static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); @@ -39,6 +41,7 @@ static void onError(uint64_t contextId, const char *message, void *userData); static void pumpUntilDone(void); static void testConcurrentContexts(void); static void testCrossThreadCallback(void); +static void testForeignFunction(void); static void testHostAndInlineNatives(void); static void testScriptError(void); @@ -52,6 +55,17 @@ static void checkImpl(bool condition, const char *message, const char *file, int } +static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + (void)userData; + calogValueNil(result); + 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; +} + + static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; @@ -72,6 +86,24 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu } +static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + CalogFnT *callable; + int32_t status; + + (void)args; + (void)argCount; + (void)userData; + calogValueNil(result); + // A host-owned function value handed to the script; calling it routes back here. + status = calogFnFromNative(&callable, calog, nativeAdd, NULL); + if (status != calogOkE) { + return calogFail(result, status, "getAdder could not allocate"); + } + calogValueFn(result, callable); + return calogOkE; +} + + static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); @@ -176,6 +208,22 @@ static void testCrossThreadCallback(void) { } +static void testForeignFunction(void) { + CalogContextT *ctx; + + ctx = calogContextOpen(calog, &calogSquirrelEngine); + atomic_store(&scriptDone, false); + atomic_store(&reportedValue, 0); + // The script receives a host-owned function value and calls it. + calogContextEval(ctx, "report(getAdder()(2, 3))\ndone()"); + pumpUntilDone(); + + CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host"); + + calogContextClose(ctx); +} + + static void testScriptError(void) { CalogContextT *ctx; int32_t before; @@ -225,10 +273,12 @@ int main(void) { calogRegister(calog, "setCb", nativeSetCb, NULL); calogRegister(calog, "done", nativeDone, NULL); calogRegister(calog, "bump", nativeBump, NULL); + calogRegister(calog, "getAdder", nativeGetAdder, NULL); calogRegisterInline(calog, "reportInline", nativeReportInline, NULL); testHostAndInlineNatives(); testCrossThreadCallback(); + testForeignFunction(); testScriptError(); testConcurrentContexts(); diff --git a/tests/testEngineWren.c b/tests/testEngineWren.c index 8a8ac09..a8ffd40 100644 --- a/tests/testEngineWren.c +++ b/tests/testEngineWren.c @@ -32,9 +32,12 @@ static int32_t testsRun = 0; static int32_t testsFailed = 0; static void checkImpl(bool condition, const char *message, const char *file, int32_t line); +static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); +static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); +static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); @@ -43,7 +46,9 @@ static void onError(uint64_t contextId, const char *message, void *userData); static void pumpUntilDone(void); static void testConcurrentContexts(void); static void testCrossThreadCallback(void); +static void testForeignFunction(void); static void testHostAndInlineNatives(void); +static void testMapIngress(void); static void testMaterializedRecord(void); static void testScriptError(void); @@ -57,6 +62,17 @@ static void checkImpl(bool condition, const char *message, const char *file, int } +static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + (void)userData; + calogValueNil(result); + 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; +} + + static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; @@ -77,6 +93,24 @@ static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *resu } +static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + CalogFnT *callable; + int32_t status; + + (void)args; + (void)argCount; + (void)userData; + calogValueNil(result); + // A host-owned function value handed to the script; calling it routes back here. + status = calogFnFromNative(&callable, calog, nativeAdd, NULL); + if (status != calogOkE) { + return calogFail(result, status, "getAdder could not allocate"); + } + calogValueFn(result, callable); + return calogOkE; +} + + static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogAggT *user; CalogValueT key; @@ -102,6 +136,26 @@ static int32_t nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT * } +static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { + CalogValueT key; + CalogValueT *found; + + (void)userData; + calogValueNil(result); + if (argCount != 1 || args[0].type != calogAggE) { + return calogFail(result, calogErrArgE, "mapAge expects a map"); + } + // Read a field out of a Wren map the script built and handed to C (map ingress). + calogValueString(&key, "age", 3); + found = calogAggGet(args[0].as.agg, &key); + calogValueFree(&key); + if (found != NULL && found->type == calogIntE) { + atomic_store(&reportedValue, found->as.i); + } + return calogOkE; +} + + static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); @@ -215,6 +269,39 @@ static void testMaterializedRecord(void) { } +static void testMapIngress(void) { + CalogContextT *ctx; + + ctx = calogContextOpen(calog, &calogWrenEngine); + atomic_store(&scriptDone, false); + atomic_store(&reportedValue, 0); + // The script builds a Wren Map and hands it to a native, which reads a field + // (exercises the calog wren.c patch that enumerates map keys from C). + calogContextEval(ctx, "Calog.call(\"mapAge\", [{\"age\": 9}])\nCalog.call(\"done\", [])"); + pumpUntilDone(); + + CHECK(atomic_load(&reportedValue) == 9, "native read a field from a Wren map the script built"); + + calogContextClose(ctx); +} + + +static void testForeignFunction(void) { + CalogContextT *ctx; + + ctx = calogContextOpen(calog, &calogWrenEngine); + atomic_store(&scriptDone, false); + atomic_store(&reportedValue, 0); + // The script receives a host-owned function value and calls it as f.call([...]). + calogContextEval(ctx, "var f = Calog.call(\"getAdder\", [])\nCalog.call(\"report\", [f.call([2, 3])])\nCalog.call(\"done\", [])"); + pumpUntilDone(); + + CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host"); + + calogContextClose(ctx); +} + + static void testCrossThreadCallback(void) { CalogContextT *ctx; CalogFnT *callback; @@ -293,10 +380,14 @@ int main(void) { calogRegister(calog, "bump", nativeBump, NULL); calogRegister(calog, "makeUser", nativeMakeUser, NULL); calogRegister(calog, "reportName", nativeReportName, NULL); + calogRegister(calog, "getAdder", nativeGetAdder, NULL); + calogRegister(calog, "mapAge", nativeMapAge, NULL); calogRegisterInline(calog, "reportInline", nativeReportInline, NULL); testHostAndInlineNatives(); testMaterializedRecord(); + testMapIngress(); + testForeignFunction(); testCrossThreadCallback(); testScriptError(); testConcurrentContexts(); diff --git a/vendor/berry/berry_conf.h b/vendor/berry/berry_conf.h index f032d77..fce7701 100644 --- a/vendor/berry/berry_conf.h +++ b/vendor/berry/berry_conf.h @@ -37,12 +37,13 @@ #define BE_USE_SINGLE_FLOAT 0 /* Macro: BE_BYTES_MAX_SIZE - * Maximum size in bytes of a `bytes()` object. - * Putting too much pressure on the memory allocator can do - * harm, so we limit the maximum size. - * Default: 32kb + * Maximum size in bytes of a `bytes()` object. This is a ceiling, not a + * preallocation -- a script only uses what it allocates -- so it is set + * generously here (calog raised it from Berry's 32 kb default). The hard + * ceiling is ~INT32_MAX because a bytes object's size/len fields are int32; + * a host can also raise vm->bytesmaxsize at runtime. **/ -#define BE_BYTES_MAX_SIZE (32*1024) /* 32 kb default value */ +#define BE_BYTES_MAX_SIZE (256*1024*1024) /* 256 MB (calog) */ /* Macro: BE_USE_PRECOMPILED_OBJECT * Use precompiled objects to avoid creating these objects at diff --git a/vendor/wren/wren.c b/vendor/wren/wren.c index 93d009a..31cf286 100644 --- a/vendor/wren/wren.c +++ b/vendor/wren/wren.c @@ -4652,6 +4652,34 @@ void wrenGetMapValue(WrenVM* vm, int mapSlot, int keySlot, int valueSlot) vm->apiStack[valueSlot] = value; } +// --- calog patch: map key enumeration (upstream Wren has no C map iterator) --- +int wrenGetMapCapacity(WrenVM* vm, int mapSlot) +{ + validateApiSlot(vm, mapSlot); + ASSERT(IS_MAP(vm->apiStack[mapSlot]), "Slot must hold a map."); + + return (int)AS_MAP(vm->apiStack[mapSlot])->capacity; +} + +bool wrenGetMapEntry(WrenVM* vm, int mapSlot, int index, int keySlot, int valueSlot) +{ + validateApiSlot(vm, mapSlot); + validateApiSlot(vm, keySlot); + validateApiSlot(vm, valueSlot); + ASSERT(IS_MAP(vm->apiStack[mapSlot]), "Slot must hold a map."); + + ObjMap* map = AS_MAP(vm->apiStack[mapSlot]); + if (index < 0 || (uint32_t)index >= map->capacity) return false; + + MapEntry* entry = &map->entries[index]; + if (IS_UNDEFINED(entry->key)) return false; + + vm->apiStack[keySlot] = entry->key; + vm->apiStack[valueSlot] = entry->value; + return true; +} +// --- end calog patch --- + void wrenSetMapValue(WrenVM* vm, int mapSlot, int keySlot, int valueSlot) { validateApiSlot(vm, mapSlot); diff --git a/vendor/wren/wren.h b/vendor/wren/wren.h index 7845911..94fae7c 100644 --- a/vendor/wren/wren.h +++ b/vendor/wren/wren.h @@ -528,6 +528,17 @@ WREN_API void wrenSetMapValue(WrenVM* vm, int mapSlot, int keySlot, int valueSlo WREN_API void wrenRemoveMapValue(WrenVM* vm, int mapSlot, int keySlot, int removedValueSlot); +// --- calog patch: map key enumeration (upstream Wren has no C map iterator) --- +// Returns the raw hash-table capacity of the map in [mapSlot] (>= wrenGetMapCount). +// Iterate indices 0..capacity-1 and call wrenGetMapEntry to visit each occupied slot. +WREN_API int wrenGetMapCapacity(WrenVM* vm, int mapSlot); + +// Stores the key and value of the map entry at raw table [index] into [keySlot] and +// [valueSlot] and returns true; returns false if that slot is empty (skip it). Mirrors +// Wren's own internal map_iterate/keyIteratorValue. +WREN_API bool wrenGetMapEntry(WrenVM* vm, int mapSlot, int index, int keySlot, int valueSlot); +// --- end calog patch --- + // Looks up the top level variable with [name] in resolved [module] and stores // it in [slot]. WREN_API void wrenGetVariable(WrenVM* vm, const char* module, const char* name,