mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Pass native closures, promises and objects into Node islands by reference (NI-5) (#1900)
This commit is contained in:
@@ -1056,6 +1056,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"nodeIsland.applyTo": "scr_node_island_apply_to",
|
||||
"nodeIsland.construct": "scr_node_island_construct",
|
||||
"nodeIsland.instanceof": "scr_node_island_instanceof",
|
||||
"nodeIsland.classMethods": "scr_node_island_class_methods",
|
||||
"nodeIsland.copyArrays": "scr_node_island_copy_arrays",
|
||||
"nodeIsland.awaitCopy": "scr_node_island_await_copy",
|
||||
"dyn.keySet": "scr_dyn_key_set",
|
||||
"dyn.keySetComputed": "scr_dyn_key_set_computed",
|
||||
"dyn.keyDelete": "scr_dyn_key_delete",
|
||||
@@ -2035,6 +2038,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"nodeIsland.callSync",
|
||||
"nodeIsland.require",
|
||||
"nodeIsland.import",
|
||||
"nodeIsland.awaitCopy",
|
||||
"nodeIsland.get",
|
||||
"nodeIsland.getKey",
|
||||
"nodeIsland.set",
|
||||
|
||||
@@ -371,7 +371,12 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unitArgs((u) => (u.tlsCa && targetPlatform(u.driver) === "win32" ? ["-lcrypt32"] : [])),
|
||||
// Fetch uses the native network, TLS, and HTTP runtime.
|
||||
unit((u) => u.nativeFetch, "scr_fetch.c"),
|
||||
unit((u) => u.opts.nodeIsland === true, "scr_node_island.c", "scr_node_island_handle.c"),
|
||||
unit(
|
||||
(u) => u.opts.nodeIsland === true,
|
||||
"scr_node_island.c",
|
||||
"scr_node_island_handle.c",
|
||||
"scr_node_island_export.c",
|
||||
),
|
||||
// Readable.fromWeb reads a web ReadableStream through its reader.
|
||||
unit((u) => u.nativeFetch && u.opts.stream, "scr_stream_from_web.c"),
|
||||
];
|
||||
|
||||
@@ -115,6 +115,7 @@ export const DIAGNOSTIC_CODES = {
|
||||
SC8005: "no usable libnode pack for a program that calls into Node.js",
|
||||
SC8006: "Node island modules the build cannot collect",
|
||||
SC8010: "value crossing a Node island boundary in a form without an exact copy",
|
||||
SC8011: "native class extending a Node island class outside the supported forms",
|
||||
SC8030: "type patch: no range covers the installed copy",
|
||||
SC8031: "type patch: selector matches nothing",
|
||||
SC8032: "type patch: declaration already has the type",
|
||||
@@ -152,7 +153,6 @@ export const RETIRED_DIAGNOSTIC_CODES = {
|
||||
* that work lands it moves its codes into DIAGNOSTIC_CODES; when it is
|
||||
* abandoned, delete them here. */
|
||||
export const RESERVED_DIAGNOSTIC_CODES = {
|
||||
SC8011: "Node islands (ni/* branches)",
|
||||
SC8012: "Node islands (ni/* branches)",
|
||||
SC8020: "Node islands (ni/* branches)",
|
||||
SC8021: "Node islands (ni/* branches)",
|
||||
|
||||
@@ -336,6 +336,18 @@ export function projectConfigDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8011 — a native class extends a class owned by a Node island. Native
|
||||
* subclasses of island classes are scope-B milestone NI-5b. */
|
||||
export function nativeSubclassOfIslandDiag(loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC8011",
|
||||
message:
|
||||
"a native class cannot extend a class owned by a Node island yet (native subclasses of island classes)",
|
||||
loc,
|
||||
hint: "keep the subclass in Node: move it into the island module, or mark its module as a Node island; a registry shim of the package also lifts this",
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8002 — Node islands are off (or limited to marked islands) and an
|
||||
* npm package is outside the native subset. */
|
||||
export function nodeIslandsOffDiag(pkg: string, why: string, loc: SrcLoc): ScrDiagnostic {
|
||||
|
||||
@@ -125,6 +125,8 @@ import {
|
||||
requireSpecOf,
|
||||
} from "../program.js";
|
||||
import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js";
|
||||
import { islandValueOrigin } from "../node-api-import.js";
|
||||
import { nativeSubclassOfIslandDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { declarationGroupKey } from "./type-failure.js";
|
||||
import { isAssertClassNew } from "./assert-instances.js";
|
||||
import { isCjsRequireGlobal } from "./lower-node-module.js";
|
||||
@@ -2212,6 +2214,10 @@ export function collectClassShapeInner(
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (t && islandValueOrigin(lowerer.program, t.expression)) {
|
||||
lowerer.pushDiag(nativeSubclassOfIslandDiag(locOf(t)));
|
||||
throw new PoisonError();
|
||||
}
|
||||
if (!t || !ts.isIdentifier(t.expression)) {
|
||||
if (t && adoptCallableBase(t.expression)) continue;
|
||||
lowerer.unsupported("SC1090", clause, "extending computed expressions");
|
||||
|
||||
@@ -35,6 +35,8 @@ import {
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { classMethodValue } from "./class-method-values.js";
|
||||
import { funcTypeFromParamShapes } from "./call-signatures.js";
|
||||
import { resolveBareModule } from "../resolve.js";
|
||||
import { nodeIslandBoundaryDiag, nodeIslandModuleDiag } from "../../diagnostics/diagnostic.js";
|
||||
import {
|
||||
@@ -528,6 +530,35 @@ function fromIsland(lowerer: Lowerer, raw: IrExpr, node: ts.Expression): IrExpr
|
||||
};
|
||||
return mapped.inner.kind === "dyn" ? promise : lowerer.coerceToExpected(promise, mapped);
|
||||
}
|
||||
// Arrays of copied values (or arrays of those) cross as copies of the
|
||||
// island array, directly or as an awaited promise's value; the checked
|
||||
// cast then meets the copy. A native array that crossed in comes back as
|
||||
// itself.
|
||||
if (mapped.kind === "promise" && copiedArrayType(lowerer, mapped.inner)) {
|
||||
const promise: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.awaitCopy",
|
||||
args: [raw],
|
||||
type: { kind: "promise", inner: DYN },
|
||||
loc: raw.loc,
|
||||
};
|
||||
return lowerer.coerceToExpected(promise, mapped);
|
||||
}
|
||||
if (copiedArrayType(lowerer, mapped)) {
|
||||
const copy: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.copyArrays",
|
||||
args: [raw],
|
||||
type: DYN,
|
||||
loc: raw.loc,
|
||||
};
|
||||
return lowerer.coerceToExpected(copy, mapped);
|
||||
}
|
||||
// NI-5: a native value the program passed in comes back as itself (an
|
||||
// export); the checked cast unboxes it, and anything else fails the cast
|
||||
// (types don't lie).
|
||||
if (!copiedType(lowerer, mapped) && exportedByReference(lowerer, tsType))
|
||||
return lowerer.coerceToExpected(raw, mapped);
|
||||
if (!copiedType(lowerer, mapped))
|
||||
boundaryFail(
|
||||
lowerer,
|
||||
@@ -537,6 +568,12 @@ function fromIsland(lowerer: Lowerer, raw: IrExpr, node: ts.Expression): IrExpr
|
||||
return lowerer.coerceToExpected(raw, mapped);
|
||||
}
|
||||
|
||||
/** An array whose elements are copied values or such arrays. */
|
||||
function copiedArrayType(lowerer: Lowerer, t: IrType): boolean {
|
||||
if (t.kind !== "array") return false;
|
||||
return copiedType(lowerer, t.elem) || copiedArrayType(lowerer, t.elem);
|
||||
}
|
||||
|
||||
/** Whether a value of IR type `t` is copied at the boundary: primitives,
|
||||
* strings, null/undefined, and unions of those. */
|
||||
function copiedType(lowerer: Lowerer, t: IrType): boolean {
|
||||
@@ -589,7 +626,20 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
case "handle":
|
||||
return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
|
||||
case "dyn":
|
||||
// NI-5: the runtime exports dyn objects by reference and copies only
|
||||
// primitives, strings and (for now) arrays.
|
||||
return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
|
||||
case "refused":
|
||||
// NI-5: closures, promises, records and class instances cross by
|
||||
// reference (scr_node_island_export.c): their dyn boxes keep the
|
||||
// native referent, and the closure's thunk checks its arguments.
|
||||
if (exportedByReference(lowerer, lowerer.typeOf(node))) {
|
||||
const lowered = lowerer.lowerExpr(node);
|
||||
const value = lowerer.coerceToExpected(lowered, DYN);
|
||||
return lowered.type.kind === "object"
|
||||
? withClassMethods(lowerer, value, lowered.type.className, node)
|
||||
: value;
|
||||
}
|
||||
return boundaryFail(
|
||||
lowerer,
|
||||
`a '${lowerer.checker.typeToString(lowerer.typeOf(node))}' value (a native object would cross as a copy and lose its identity)`,
|
||||
@@ -598,6 +648,95 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether an island reads the lowered method `name` by that name: a
|
||||
* public, string-keyed prototype method. The constructor, #private methods,
|
||||
* accessor halves (`get:x`, `set:x`) and symbol-keyed slots (`sym:iterator`)
|
||||
* are not properties an island can name. */
|
||||
function islandMethodName(name: string): boolean {
|
||||
return name !== "constructor" && !name.startsWith("#") && !/^(?:get|set|sym):/.test(name);
|
||||
}
|
||||
|
||||
/** A class instance crossing into an island: its capsule answers island
|
||||
* reads of its fields, and its prototype methods come from a table the
|
||||
* runtime builds once per class (nodeIsland.classMethods): the unbound
|
||||
* method values, whose thunks check the receiver Node calls them on. */
|
||||
function withClassMethods(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
className: string,
|
||||
blame: ts.Node,
|
||||
): IrExpr {
|
||||
const info = lowerer.classes.get(className);
|
||||
if (info === undefined || info.def.runtime) return value;
|
||||
const names: string[] = [];
|
||||
for (
|
||||
let c: typeof info | undefined = info;
|
||||
c !== undefined;
|
||||
c = c.def.base ? lowerer.classes.get(c.def.base) : undefined
|
||||
) {
|
||||
for (const m of c.def.methods ?? [])
|
||||
if (!names.includes(m) && islandMethodName(m)) names.push(m);
|
||||
}
|
||||
if (names.length === 0) return value;
|
||||
const loc = value.loc;
|
||||
const builder = `%island.methods:${className}`;
|
||||
if (!lowerer.classMethodValueHelpers.has(builder)) {
|
||||
lowerer.classMethodValueHelpers.add(builder);
|
||||
const fields: { key: IrExpr; value: IrExpr }[] = [];
|
||||
for (const m of names) {
|
||||
const method = classMethodValue(lowerer, blame, info, m, loc);
|
||||
if (method === null) continue;
|
||||
fields.push({
|
||||
key: { kind: "strLit", value: m, type: STRING, loc },
|
||||
value: lowerer.coerceToExpected(method, DYN),
|
||||
});
|
||||
}
|
||||
lowerer.liftedFns.push({
|
||||
name: builder,
|
||||
params: [],
|
||||
returnType: DYN,
|
||||
locals: [],
|
||||
body: [{ kind: "return", value: { kind: "dynObjLit", fields, type: DYN, loc }, loc }],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const closure: IrExpr = {
|
||||
kind: "closure",
|
||||
fnName: builder,
|
||||
captures: [],
|
||||
type: funcTypeFromParamShapes([], DYN),
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.classMethods",
|
||||
args: [value, lowerer.coerceToExpected(closure, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Native values NI-5 exports into the island by reference: functions,
|
||||
* promises, and object types other than arrays, tuples, Maps and Sets
|
||||
* (records, interfaces, class instances). A union qualifies when every
|
||||
* arm does or is copied. */
|
||||
function exportedByReference(lowerer: Lowerer, type: ts.Type): boolean {
|
||||
const checker = lowerer.checker;
|
||||
if (type.isUnionType())
|
||||
return ts
|
||||
.constituentTypes(type)
|
||||
.every(
|
||||
(t) =>
|
||||
(t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0 ||
|
||||
exportedByReference(lowerer, t),
|
||||
);
|
||||
if ((type.flags & ts.TypeFlags.Object) === 0) return false;
|
||||
if (checker.isArrayType(type) || checker.isTupleType(type)) return false;
|
||||
const name = type.getSymbol()?.escapedName;
|
||||
if (name === "Map" || name === "Set" || name === "WeakMap" || name === "WeakSet") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** A fresh object or array literal crosses as a copy (a plain dyn object
|
||||
* or array the runtime copies into V8): nothing native can alias it. Its
|
||||
* members cross by the same rules (toIsland). */
|
||||
|
||||
@@ -62,3 +62,28 @@ export function nodeApiCalleeOf(program: ts.Program, callee: ts.Expression): Nod
|
||||
if (!isNodeApiMember(member)) return null;
|
||||
return isNodeApiNamespace(program, callee.expression) ? member : null;
|
||||
}
|
||||
|
||||
/** Whether an expression evaluates to a value owned by a Node island: a
|
||||
* requireModule(...) result or anything reached from one through member
|
||||
* reads, element reads, destructuring or a const alias (`const { Point } =
|
||||
* requireModule("./lib.cjs")`, `lib.Point`, `const P = lib.Point`). Used
|
||||
* to refuse native subclasses of island classes (SC8011). */
|
||||
export function islandValueOrigin(program: ts.Program, expr: ts.Expression, depth = 0): boolean {
|
||||
if (depth > 8) return false;
|
||||
let e: ts.Node = expr;
|
||||
for (;;) {
|
||||
if (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
else if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) e = e.expression;
|
||||
else if (ts.isAsExpression(e) || ts.isNonNullExpression(e)) e = e.expression;
|
||||
else break;
|
||||
}
|
||||
if (ts.isCallExpression(e)) return nodeApiCalleeOf(program, e.expression) === "requireModule";
|
||||
if (!ts.isIdentifier(e)) return false;
|
||||
const symbol = program.getTypeChecker().getSymbolAtLocation(e);
|
||||
let decl: ts.Node | undefined = symbol ? symbol.declarations[0] : undefined;
|
||||
// A destructuring binding: climb to the declaration that initializes it.
|
||||
while (decl !== undefined && ts.isBindingElement(decl)) decl = decl.parent?.parent;
|
||||
if (decl === undefined || !ts.isVariableDeclaration(decl) || decl.initializer === undefined)
|
||||
return false;
|
||||
return islandValueOrigin(program, decl.initializer, depth + 1);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"nodeIsland.applyTo",
|
||||
"nodeIsland.construct",
|
||||
"nodeIsland.instanceof",
|
||||
"nodeIsland.classMethods",
|
||||
"nodeIsland.copyArrays",
|
||||
// Node's argument, cipher-name, IV, tag-length and key-length errors.
|
||||
"crypto.cipherNew",
|
||||
// Node's curve, key, point, encoding and format errors.
|
||||
|
||||
@@ -188,9 +188,9 @@ function buildLibFnSignatures() {
|
||||
/** Node islands (scope-B NI-4, scr_node_island_handle.c): island values
|
||||
* are dyn values (`foreign` lowers as dyn), objects are handle boxes
|
||||
* whose identity is the V8 object's. require/import load an island
|
||||
* module by its absolute (or `$ORIGIN/`) path (also @scriptc/node
|
||||
* requireModule); the rest are member reads,
|
||||
* writes and calls on island values, argument lists as one dyn array. */
|
||||
* module by its absolute (or `$ORIGIN/`) path; the rest are member reads,
|
||||
* writes and calls on island values, argument lists as one dyn array.
|
||||
* requireModule(specifier) of @scriptc/node lowers to require too. */
|
||||
"nodeIsland.require": { argTypes: [STRING], result: DYN },
|
||||
"nodeIsland.import": { argTypes: [STRING], result: { kind: "promise", inner: DYN } },
|
||||
"nodeIsland.get": { argTypes: [DYN, STRING], result: DYN },
|
||||
@@ -202,6 +202,13 @@ function buildLibFnSignatures() {
|
||||
"nodeIsland.applyTo": { argTypes: [DYN, DYN, DYN], result: DYN },
|
||||
"nodeIsland.construct": { argTypes: [DYN, DYN], result: DYN },
|
||||
"nodeIsland.instanceof": { argTypes: [DYN, DYN], result: BOOL },
|
||||
/** NI-5: a class instance crossing into an island, with the builder of
|
||||
* its class's method table (run once per class). Answers the value. */
|
||||
"nodeIsland.classMethods": { argTypes: [DYN, DYN], result: DYN },
|
||||
/** Island arrays of copied values as copies (a result, or an awaited
|
||||
* promise's value). */
|
||||
"nodeIsland.copyArrays": { argTypes: [DYN], result: DYN },
|
||||
"nodeIsland.awaitCopy": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
|
||||
/** Keyed WRITE on a dyn value — `h.onDone = cb` / `h["k"] = v` on a
|
||||
* checked-dynamic object (args: receiver, key string, value — all
|
||||
* borrowed; the runtime copies the key and retains the value in). An
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
export const NODE_ISLAND_MIN_NODE = "24.13.1";
|
||||
|
||||
/** The bridge ABI this compiler's runtime speaks (scr_node_bridge.h). */
|
||||
export const NODE_BRIDGE_ABI = 5;
|
||||
export const NODE_BRIDGE_ABI = 6;
|
||||
|
||||
/** The prefix of a recorded path relative to the executable's directory. */
|
||||
export const ORIGIN = "$ORIGIN/";
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6830-node-island-callbacks/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6831-node-island-promises/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6832-node-island-objects/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6833-node-island-typed-callbacks/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6834-node-island-package-callbacks/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6835-node-island-package-promises/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6836-node-island-package-objects/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6837-node-island-clone-and-arrays/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6838-node-island-private-methods/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/node-island-subclass/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -58,6 +58,8 @@ using v8::Value;
|
||||
|
||||
const ScrNodeBridgeHost *g_host = nullptr;
|
||||
void DropAllHandles();
|
||||
void DropAllExports();
|
||||
void FlushReleasedExports();
|
||||
std::unique_ptr<node::MultiIsolatePlatform> g_platform;
|
||||
node::EmbedderSnapshotData::Pointer g_snapshot;
|
||||
std::unique_ptr<node::CommonEnvironmentSetup> g_setup;
|
||||
@@ -188,6 +190,16 @@ bool TakeFunction(Isolate *isolate, Local<Context> context, Local<Object> from,
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t ExportIdOf(Isolate *isolate, Local<Context> context, Local<Value> v);
|
||||
|
||||
// isExport(value): whether a value is the representative of a native value
|
||||
// (NI-5), for the bootstrap's structuredClone of native data.
|
||||
void IsExport(const FunctionCallbackInfo<Value> &info) {
|
||||
Isolate *isolate = info.GetIsolate();
|
||||
info.GetReturnValue().Set(info.Length() > 0 &&
|
||||
ExportIdOf(isolate, isolate->GetCurrentContext(), info[0]) != 0);
|
||||
}
|
||||
|
||||
char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *argv0,
|
||||
uintptr_t stack_limit) {
|
||||
if (g_setup) return nullptr;
|
||||
@@ -255,6 +267,7 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar
|
||||
SetMethod(context, native, "settle", Settle);
|
||||
SetMethod(context, native, "run", Run);
|
||||
SetMethod(context, native, "listen", Listen);
|
||||
SetMethod(context, native, "isExport", IsExport);
|
||||
global->Set(context, Utf8(isolate, "__scriptcNodeIslandHost"), native).Check();
|
||||
TryCatch try_catch(isolate);
|
||||
if (node::LoadEnvironment(g_setup->env(), bootstrap).IsEmpty()) {
|
||||
@@ -487,6 +500,7 @@ void SyncIo(int phase) {
|
||||
// its liveness, which the callback may have changed after the turn's check.
|
||||
void OnMicrotasksDone(Isolate *, void *) {
|
||||
if (!g_hosted) return;
|
||||
FlushReleasedExports();
|
||||
g_host->microtasks_done();
|
||||
SyncIo(SCR_NODE_IO_JOBS);
|
||||
}
|
||||
@@ -621,6 +635,7 @@ void Stop() {
|
||||
g_call.Reset();
|
||||
g_load.Reset();
|
||||
DropAllHandles();
|
||||
DropAllExports();
|
||||
for (Global<Function> *fn :
|
||||
{&g_set_timer, &g_clear_timer, &g_ref_timer, &g_refresh_timer, &g_set_immediate,
|
||||
&g_clear_immediate, &g_ref_immediate, &g_next_tick, &g_emit})
|
||||
@@ -808,6 +823,326 @@ size_t ElemSize(uint32_t elem) {
|
||||
return elem < 10 ? sizes[elem] : 1;
|
||||
}
|
||||
|
||||
Local<Value> ToV8(Isolate *isolate, Local<Context> context, const ScrNodeValue *in);
|
||||
void FromV8(Isolate *isolate, Local<Context> context, Local<Value> v, ScrNodeValue *out);
|
||||
|
||||
// ── exports: native values in V8 (scope-B NI-5) ─────────────────────────
|
||||
// A native value that crosses into Node by reference has an export id in
|
||||
// the runtime's table and one V8 representative here, held weakly: a
|
||||
// function whose calls run the native closure (export_call), a promise the
|
||||
// runtime settles inside its own promise's settlement (export_settle), or
|
||||
// an object whose property operations all go to the native storage
|
||||
// (interceptors). The representative carries its id in a private symbol, so
|
||||
// it crosses back as the native value itself. When V8 collects one, the
|
||||
// runtime hears export_release at the next microtask checkpoint.
|
||||
|
||||
Global<v8::Private> g_export_key;
|
||||
Global<v8::ObjectTemplate> g_export_template;
|
||||
std::unordered_map<uint32_t, Global<Object>> g_exports;
|
||||
std::unordered_map<uint32_t, Global<v8::Promise::Resolver>> g_resolvers;
|
||||
std::vector<uint32_t> g_released_exports;
|
||||
|
||||
Local<v8::Private> ExportKey(Isolate *isolate) {
|
||||
if (g_export_key.IsEmpty())
|
||||
g_export_key.Reset(isolate, v8::Private::New(isolate, Utf8(isolate, "scriptc:export")));
|
||||
return g_export_key.Get(isolate);
|
||||
}
|
||||
|
||||
uint32_t ExportIdOf(Isolate *isolate, Local<Context> context, Local<Value> v) {
|
||||
if (!v->IsObject() || v->IsProxy() || g_exports.empty()) return 0;
|
||||
Local<Value> id;
|
||||
if (!v.As<Object>()->GetPrivate(context, ExportKey(isolate)).ToLocal(&id) || !id->IsUint32())
|
||||
return 0;
|
||||
return id.As<v8::Uint32>()->Value();
|
||||
}
|
||||
|
||||
void OnExportGone(const v8::WeakCallbackInfo<void> &info) {
|
||||
uint32_t id = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(info.GetParameter()));
|
||||
auto it = g_exports.find(id);
|
||||
if (it != g_exports.end()) {
|
||||
it->second.Reset();
|
||||
g_exports.erase(it);
|
||||
}
|
||||
g_released_exports.push_back(id);
|
||||
}
|
||||
|
||||
// Delivers the releases V8's collector queued (never from inside a GC).
|
||||
void FlushReleasedExports() {
|
||||
while (!g_released_exports.empty()) {
|
||||
uint32_t id = g_released_exports.back();
|
||||
g_released_exports.pop_back();
|
||||
if (g_exports.count(id) != 0) continue; // crossed again since
|
||||
g_host->export_release(id);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ExportIdArg(Local<Value> data) {
|
||||
return data->IsUint32() ? data.As<v8::Uint32>()->Value() : 0;
|
||||
}
|
||||
|
||||
// Out-value scratch of host calls made from outside any runtime entry (a
|
||||
// listener Node's loop dispatches) is reset when the outermost call
|
||||
// starts. A call that puts its own inputs in the scratch (a property
|
||||
// name) opens its scope before converting them.
|
||||
struct HostCall {
|
||||
HostCall() {
|
||||
if (g_entry_depth++ == 0) g_scratch.clear();
|
||||
}
|
||||
~HostCall() { g_entry_depth--; }
|
||||
};
|
||||
|
||||
void ThrowOrReturn(Isolate *isolate, Local<Context> context, int status, const ScrNodeValue *out,
|
||||
Local<Value> *result) {
|
||||
Local<Value> v = ToV8(isolate, context, out);
|
||||
if (status == 1) isolate->ThrowException(v);
|
||||
else *result = v;
|
||||
}
|
||||
|
||||
void ExportCall(const FunctionCallbackInfo<Value> &info) {
|
||||
Isolate *isolate = info.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
HostCall scope;
|
||||
uint32_t id = ExportIdArg(info.Data());
|
||||
std::vector<ScrNodeValue> args(static_cast<size_t>(info.Length()));
|
||||
for (int i = 0; i < info.Length(); i++) FromV8(isolate, context, info[i], &args[i]);
|
||||
ScrNodeValue out;
|
||||
std::memset(&out, 0, sizeof out);
|
||||
int status;
|
||||
if (info.IsConstructCall()) {
|
||||
status = g_host->export_construct(id, args.data(), args.size(), &out);
|
||||
} else {
|
||||
ScrNodeValue self;
|
||||
FromV8(isolate, context, info.This(), &self);
|
||||
status = g_host->export_call(id, &self, args.data(), args.size(), &out);
|
||||
}
|
||||
Local<Value> result;
|
||||
ThrowOrReturn(isolate, context, status, &out, &result);
|
||||
if (!result.IsEmpty()) info.GetReturnValue().Set(result);
|
||||
}
|
||||
|
||||
uint32_t HolderId(Local<Object> holder) {
|
||||
if (holder->InternalFieldCount() < 1) return 0;
|
||||
return ExportIdArg(holder->GetInternalField(0).As<Value>());
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
uint32_t InfoId(const v8::PropertyCallbackInfo<T> &info) {
|
||||
return HolderId(info.HolderV2());
|
||||
}
|
||||
|
||||
void NameKey(Isolate *isolate, Local<Context> context, Local<v8::Name> name, ScrNodeValue *key) {
|
||||
FromV8(isolate, context, name, key);
|
||||
}
|
||||
|
||||
void IndexKey(uint32_t index, ScrNodeValue *key) {
|
||||
std::memset(key, 0, sizeof *key);
|
||||
key->tag = SCR_NV_NUMBER;
|
||||
key->u.num = index;
|
||||
}
|
||||
|
||||
v8::Intercepted GetKey(const v8::PropertyCallbackInfo<Value> &info, const ScrNodeValue *key) {
|
||||
Isolate *isolate = info.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
HostCall scope;
|
||||
ScrNodeValue out;
|
||||
std::memset(&out, 0, sizeof out);
|
||||
int status = g_host->export_get(InfoId(info), key, &out);
|
||||
if (status == 2) return v8::Intercepted::kNo;
|
||||
Local<Value> result;
|
||||
ThrowOrReturn(isolate, context, status, &out, &result);
|
||||
if (!result.IsEmpty()) info.GetReturnValue().Set(result);
|
||||
return v8::Intercepted::kYes;
|
||||
}
|
||||
|
||||
v8::Intercepted SetKey(const v8::PropertyCallbackInfo<void> &info, const ScrNodeValue *key,
|
||||
Local<Value> value) {
|
||||
Isolate *isolate = info.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
HostCall scope;
|
||||
ScrNodeValue v, out;
|
||||
FromV8(isolate, context, value, &v);
|
||||
std::memset(&out, 0, sizeof out);
|
||||
int status = g_host->export_set(InfoId(info), key, &v, &out);
|
||||
if (status == 2) return v8::Intercepted::kNo;
|
||||
if (status == 1) isolate->ThrowException(ToV8(isolate, context, &out));
|
||||
return v8::Intercepted::kYes;
|
||||
}
|
||||
|
||||
v8::Intercepted QueryKey(const v8::PropertyCallbackInfo<v8::Integer> &info,
|
||||
const ScrNodeValue *key) {
|
||||
HostCall scope;
|
||||
if (!g_host->export_has(InfoId(info), key)) return v8::Intercepted::kNo;
|
||||
info.GetReturnValue().Set(static_cast<int32_t>(v8::None));
|
||||
return v8::Intercepted::kYes;
|
||||
}
|
||||
|
||||
v8::Intercepted DeleteKey(const v8::PropertyCallbackInfo<v8::Boolean> &info,
|
||||
const ScrNodeValue *key) {
|
||||
Isolate *isolate = info.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
HostCall scope;
|
||||
ScrNodeValue out;
|
||||
std::memset(&out, 0, sizeof out);
|
||||
int status = g_host->export_delete(InfoId(info), key, &out);
|
||||
if (status == 2) return v8::Intercepted::kNo;
|
||||
if (status == 1) isolate->ThrowException(ToV8(isolate, context, &out));
|
||||
else info.GetReturnValue().Set(true);
|
||||
return v8::Intercepted::kYes;
|
||||
}
|
||||
|
||||
v8::Intercepted NamedGet(Local<v8::Name> name, const v8::PropertyCallbackInfo<Value> &info) {
|
||||
HostCall scope; // the key's text lives in the scratch this call keeps
|
||||
ScrNodeValue key;
|
||||
NameKey(info.GetIsolate(), info.GetIsolate()->GetCurrentContext(), name, &key);
|
||||
return GetKey(info, &key);
|
||||
}
|
||||
v8::Intercepted NamedSet(Local<v8::Name> name, Local<Value> value,
|
||||
const v8::PropertyCallbackInfo<void> &info) {
|
||||
HostCall scope; // the key's text lives in the scratch this call keeps
|
||||
ScrNodeValue key;
|
||||
NameKey(info.GetIsolate(), info.GetIsolate()->GetCurrentContext(), name, &key);
|
||||
return SetKey(info, &key, value);
|
||||
}
|
||||
v8::Intercepted NamedQuery(Local<v8::Name> name, const v8::PropertyCallbackInfo<v8::Integer> &info) {
|
||||
HostCall scope; // the key's text lives in the scratch this call keeps
|
||||
ScrNodeValue key;
|
||||
NameKey(info.GetIsolate(), info.GetIsolate()->GetCurrentContext(), name, &key);
|
||||
return QueryKey(info, &key);
|
||||
}
|
||||
v8::Intercepted NamedDelete(Local<v8::Name> name, const v8::PropertyCallbackInfo<v8::Boolean> &info) {
|
||||
HostCall scope; // the key's text lives in the scratch this call keeps
|
||||
ScrNodeValue key;
|
||||
NameKey(info.GetIsolate(), info.GetIsolate()->GetCurrentContext(), name, &key);
|
||||
return DeleteKey(info, &key);
|
||||
}
|
||||
v8::Intercepted IndexedGet(uint32_t index, const v8::PropertyCallbackInfo<Value> &info) {
|
||||
ScrNodeValue key;
|
||||
IndexKey(index, &key);
|
||||
return GetKey(info, &key);
|
||||
}
|
||||
v8::Intercepted IndexedSet(uint32_t index, Local<Value> value,
|
||||
const v8::PropertyCallbackInfo<void> &info) {
|
||||
ScrNodeValue key;
|
||||
IndexKey(index, &key);
|
||||
return SetKey(info, &key, value);
|
||||
}
|
||||
v8::Intercepted IndexedQuery(uint32_t index, const v8::PropertyCallbackInfo<v8::Integer> &info) {
|
||||
ScrNodeValue key;
|
||||
IndexKey(index, &key);
|
||||
return QueryKey(info, &key);
|
||||
}
|
||||
v8::Intercepted IndexedDelete(uint32_t index, const v8::PropertyCallbackInfo<v8::Boolean> &info) {
|
||||
ScrNodeValue key;
|
||||
IndexKey(index, &key);
|
||||
return DeleteKey(info, &key);
|
||||
}
|
||||
|
||||
// Own enumerable keys, split for V8: names for the named enumerator, array
|
||||
// indices for the indexed one.
|
||||
Local<v8::Array> ExportKeys(const v8::PropertyCallbackInfo<v8::Array> &info, bool indices) {
|
||||
Isolate *isolate = info.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
HostCall scope;
|
||||
ScrNodeValue out;
|
||||
std::memset(&out, 0, sizeof out);
|
||||
std::vector<Local<Value>> keep;
|
||||
if (g_host->export_keys(InfoId(info), &out) == 0) {
|
||||
Local<Value> all = ToV8(isolate, context, &out);
|
||||
if (all->IsArray()) {
|
||||
Local<v8::Array> list = all.As<v8::Array>();
|
||||
for (uint32_t i = 0; i < list->Length(); i++) {
|
||||
Local<Value> key = list->Get(context, i).ToLocalChecked();
|
||||
uint32_t index = 0;
|
||||
bool is_index = false;
|
||||
if (key->IsString()) {
|
||||
Local<v8::Uint32> as_index;
|
||||
if (key->ToArrayIndex(context).ToLocal(&as_index)) {
|
||||
is_index = true;
|
||||
index = as_index->Value();
|
||||
}
|
||||
}
|
||||
if (is_index != indices) continue;
|
||||
keep.push_back(indices ? Local<Value>(v8::Integer::NewFromUnsigned(isolate, index)) : key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return v8::Array::New(isolate, keep.data(), keep.size());
|
||||
}
|
||||
|
||||
void NamedEnum(const v8::PropertyCallbackInfo<v8::Array> &info) {
|
||||
info.GetReturnValue().Set(ExportKeys(info, false));
|
||||
}
|
||||
void IndexedEnum(const v8::PropertyCallbackInfo<v8::Array> &info) {
|
||||
info.GetReturnValue().Set(ExportKeys(info, true));
|
||||
}
|
||||
|
||||
Local<v8::ObjectTemplate> ExportTemplate(Isolate *isolate) {
|
||||
if (g_export_template.IsEmpty()) {
|
||||
Local<v8::ObjectTemplate> t = v8::ObjectTemplate::New(isolate);
|
||||
t->SetInternalFieldCount(1);
|
||||
t->SetHandler(v8::NamedPropertyHandlerConfiguration(
|
||||
NamedGet, NamedSet, NamedQuery, NamedDelete, NamedEnum, Local<Value>(),
|
||||
v8::PropertyHandlerFlags::kOnlyInterceptStrings));
|
||||
t->SetHandler(v8::IndexedPropertyHandlerConfiguration(IndexedGet, IndexedSet, IndexedQuery,
|
||||
IndexedDelete, IndexedEnum));
|
||||
g_export_template.Reset(isolate, t);
|
||||
}
|
||||
return g_export_template.Get(isolate);
|
||||
}
|
||||
|
||||
Local<Value> ExportValue(Isolate *isolate, Local<Context> context, const ScrNodeValue *in) {
|
||||
uint32_t id = in->u.exp.id;
|
||||
auto found = g_exports.find(id);
|
||||
if (found != g_exports.end()) return found->second.Get(isolate);
|
||||
Local<Object> obj;
|
||||
Local<Value> data = v8::Integer::NewFromUnsigned(isolate, id);
|
||||
if (in->flags & SCR_NVF_FUNCTION) {
|
||||
Local<Function> fn =
|
||||
Function::New(context, ExportCall, data, static_cast<int>(in->u.exp.length))
|
||||
.ToLocalChecked();
|
||||
fn->SetName(in->u.exp.name != nullptr
|
||||
? FromWtf8(isolate, in->u.exp.name, in->u.exp.name_len)
|
||||
: String::Empty(isolate));
|
||||
obj = fn;
|
||||
} else if (in->flags & SCR_NVF_PROMISE) {
|
||||
Local<v8::Promise::Resolver> resolver = v8::Promise::Resolver::New(context).ToLocalChecked();
|
||||
g_resolvers[id].Reset(isolate, resolver);
|
||||
obj = resolver->GetPromise();
|
||||
} else {
|
||||
obj = ExportTemplate(isolate)->NewInstance(context).ToLocalChecked();
|
||||
obj->SetInternalField(0, data);
|
||||
}
|
||||
obj->SetPrivate(context, ExportKey(isolate), data).Check();
|
||||
Global<Object> &slot = g_exports[id];
|
||||
slot.Reset(isolate, obj);
|
||||
slot.SetWeak(reinterpret_cast<void *>(static_cast<uintptr_t>(id)), OnExportGone,
|
||||
v8::WeakCallbackType::kParameter);
|
||||
if (in->flags & SCR_NVF_PROMISE) g_host->export_watch(id);
|
||||
return obj;
|
||||
}
|
||||
|
||||
void ExportSettle(uint32_t id, int rejected, const ScrNodeValue *value) {
|
||||
Enter e;
|
||||
auto it = g_resolvers.find(id);
|
||||
if (it == g_resolvers.end()) return;
|
||||
Local<v8::Promise::Resolver> resolver = it->second.Get(e.isolate);
|
||||
it->second.Reset();
|
||||
g_resolvers.erase(it);
|
||||
Local<Value> v = ToV8(e.isolate, e.context, value);
|
||||
if (rejected) resolver->Reject(e.context, v).Check();
|
||||
else resolver->Resolve(e.context, v).Check();
|
||||
}
|
||||
|
||||
void DropAllExports() {
|
||||
for (auto &it : g_exports) it.second.Reset();
|
||||
g_exports.clear();
|
||||
for (auto &it : g_resolvers) it.second.Reset();
|
||||
g_resolvers.clear();
|
||||
g_released_exports.clear();
|
||||
g_export_template.Reset();
|
||||
g_export_key.Reset();
|
||||
}
|
||||
|
||||
void FromV8(Isolate *isolate, Local<Context> context, Local<Value> v, ScrNodeValue *out) {
|
||||
std::memset(out, 0, sizeof *out);
|
||||
if (v.IsEmpty() || v->IsUndefined()) { out->tag = SCR_NV_UNDEFINED; return; }
|
||||
@@ -843,6 +1178,11 @@ void FromV8(Isolate *isolate, Local<Context> context, Local<Value> v, ScrNodeVal
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (uint32_t export_id = ExportIdOf(isolate, context, v)) {
|
||||
out->tag = SCR_NV_EXPORT;
|
||||
out->u.exp.id = export_id;
|
||||
return;
|
||||
}
|
||||
out->tag = SCR_NV_HANDLE;
|
||||
uint32_t flags = 0;
|
||||
if (v->IsSymbol()) flags |= SCR_NVF_SYMBOL;
|
||||
@@ -925,6 +1265,7 @@ Local<Value> ToV8(Isolate *isolate, Local<Context> context, const ScrNodeValue *
|
||||
FromWtf8(isolate, in->u.error.code, in->u.error.code_len)).Check();
|
||||
return error;
|
||||
}
|
||||
case SCR_NV_EXPORT: return ExportValue(isolate, context, in);
|
||||
case SCR_NV_JSON: {
|
||||
Local<Value> parsed;
|
||||
if (v8::JSON::Parse(context, FromWtf8(isolate, in->u.str.data, in->u.str.len)).ToLocal(&parsed))
|
||||
@@ -1257,6 +1598,7 @@ const ScrNodeBridge kBridge = {
|
||||
ErrorInfo,
|
||||
DropHandle,
|
||||
LiveHandles,
|
||||
ExportSettle,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -195,6 +195,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [
|
||||
// over the SCR_NODE_ISLANDS build of scr_async.c.
|
||||
["scr_node_island.c", "nodeIsland"],
|
||||
["scr_node_island_handle.c", "nodeIsland"],
|
||||
["scr_node_island_export.c", "nodeIsland"],
|
||||
["scr_stream_from_web.c", all("stream", "nativeFetch")],
|
||||
];
|
||||
|
||||
|
||||
@@ -172,6 +172,9 @@ static void scr_fiber_stack_free(void *stack) {
|
||||
enum { SCR_PROM_PENDING = 0, SCR_PROM_FULFILLED = 1, SCR_PROM_REJECTED = 2 };
|
||||
|
||||
#define SCR_CB_SYNC SIZE_MAX
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
#define SCR_CB_ANY SIZE_MAX /* all_idx of an any-settlement callback */
|
||||
#endif
|
||||
#define SCR_CB_BATCH (SIZE_MAX - 1)
|
||||
|
||||
typedef struct ScrFiber ScrFiber;
|
||||
@@ -388,6 +391,26 @@ void scr_promise_share_identity(ScrPromise *dst, ScrPromise *src) {
|
||||
dst->identity->rc++;
|
||||
}
|
||||
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
/* Node islands (NI-5) key their export table by a promise's identity
|
||||
* (scr_node_island_export.c); other programs link none of this. */
|
||||
const void *scr_promise_identity_key(ScrPromise *p) {
|
||||
if (!p->identity) {
|
||||
p->identity = malloc(sizeof *p->identity);
|
||||
if (!p->identity) scr_oom();
|
||||
p->identity->rc = 1;
|
||||
}
|
||||
return p->identity;
|
||||
}
|
||||
|
||||
void scr_promise_identity_hold(const void *key) { ((ScrPromiseIdentity *)key)->rc++; }
|
||||
|
||||
void scr_promise_identity_drop(const void *key) {
|
||||
ScrPromiseIdentity *identity = (ScrPromiseIdentity *)key;
|
||||
if (--identity->rc == 0) free(identity);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool scr_promise_identity_equal(ScrPromise *a, ScrPromise *b) {
|
||||
return a == b || (a->identity != NULL && a->identity == b->identity);
|
||||
}
|
||||
@@ -1831,6 +1854,10 @@ static void scr_promise_cb_run(struct ScrPromiseCbWaiter *cb, ScrPromise *src) {
|
||||
scr_promise_all_state_release(cb->all);
|
||||
} else if (src->state == SCR_PROM_FULFILLED) {
|
||||
cb->adapt(cb->dst, src);
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
} else if (cb->all_idx == SCR_CB_ANY) {
|
||||
cb->adapt(cb->dst, src); /* scr_promise_on_settle_sync: rejections too */
|
||||
#endif
|
||||
} else {
|
||||
scr_promise_settle_from(cb->dst, src);
|
||||
}
|
||||
@@ -1944,6 +1971,17 @@ void scr_promise_race_join(ScrPromise *race, ScrPromise *in,
|
||||
in, (struct ScrPromiseCbWaiter){ .adapt = adapt, .dst = scr_promise_retain(race) });
|
||||
}
|
||||
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
/* Node islands (NI-5): runs `cb(in, in)` inside `in`'s settlement, for
|
||||
* fulfillments and rejections alike (a native promise's V8 representative
|
||||
* settles at the same moment, so its reactions queue where a JavaScript
|
||||
* promise's would). `in` must be pending. */
|
||||
void scr_promise_on_settle_sync(ScrPromise *in, void (*cb)(ScrPromise *dst, ScrPromise *src)) {
|
||||
*scr_promise_cb_push(in) =
|
||||
(struct ScrPromiseCbWaiter){ cb, scr_promise_retain(in), NULL, SCR_CB_ANY, SCR_CB_SYNC };
|
||||
}
|
||||
#endif
|
||||
|
||||
void scr_promise_race_add(ScrPromise *race, ScrPromise *in,
|
||||
void (*adapt)(ScrPromise *dst, ScrPromise *src)) {
|
||||
if (in->state != SCR_PROM_PENDING) {
|
||||
|
||||
@@ -23,6 +23,13 @@ ScrPromise *scr_tp_set_immediate(void);
|
||||
ScrPromise *scr_promise_new(void);
|
||||
void scr_promise_share_identity(ScrPromise *dst, ScrPromise *src);
|
||||
bool scr_promise_identity_equal(ScrPromise *a, ScrPromise *b);
|
||||
/* A key every promise sharing `p`'s identity answers (each adapter box of
|
||||
* a typed promise), creating the shared cell when `p` has none. */
|
||||
const void *scr_promise_identity_key(ScrPromise *p);
|
||||
/* A reference on a key from scr_promise_identity_key, for a table that
|
||||
* keeps the key after the promises that share it are gone. */
|
||||
void scr_promise_identity_hold(const void *key);
|
||||
void scr_promise_identity_drop(const void *key);
|
||||
ScrPromise *scr_promise_retain(ScrPromise *p);
|
||||
void scr_promise_release(ScrPromise *p);
|
||||
void *scr_promise_retain_v(void *p);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SCR_NODE_BRIDGE_ABI 5u
|
||||
#define SCR_NODE_BRIDGE_ABI 6u
|
||||
/* The symbol a bridge library exports: `const ScrNodeBridge *(void)`. */
|
||||
#define SCR_NODE_BRIDGE_ENTRY "scr_node_bridge_v1"
|
||||
|
||||
@@ -82,7 +82,8 @@ enum {
|
||||
SCR_NV_BYTES = 7, /* bytes: a typed array / Buffer / DataView over shared memory */
|
||||
SCR_NV_ERROR = 8, /* into V8 only: a new Error of an intrinsic class */
|
||||
SCR_NV_JSON = 9, /* into V8 only: str is JSON text, parsed there (a copy) */
|
||||
SCR_NV_EXPORT = 10, /* reserved for NI-5: a runtime object exported by reference */
|
||||
SCR_NV_EXPORT = 10, /* exp: a runtime value exported by reference (NI-5); flags: SCR_NVF_FUNCTION,
|
||||
SCR_NVF_PROMISE, SCR_NVF_ARRAY or 0 for an object */
|
||||
};
|
||||
/* What a handle is, reported with every handle the bridge answers. */
|
||||
enum {
|
||||
@@ -119,6 +120,12 @@ typedef struct ScrNodeValue {
|
||||
const char *name, *message, *stack, *code; /* code NULL: none */
|
||||
size_t name_len, message_len, stack_len, code_len;
|
||||
} error;
|
||||
struct {
|
||||
uint32_t id; /* the runtime's export-table id */
|
||||
uint32_t length; /* functions: their `length` */
|
||||
const char *name; /* functions: their `name` (NULL: anonymous) */
|
||||
size_t name_len;
|
||||
} exp;
|
||||
} u;
|
||||
} ScrNodeValue;
|
||||
|
||||
@@ -172,6 +179,27 @@ typedef struct ScrNodeBridgeHost {
|
||||
* value that went into V8). May run on any thread: the runtime queues
|
||||
* the release for its own thread. */
|
||||
void (*bytes_free)(void *owner);
|
||||
/* Native values in Node (NI-5): the V8 representative of export `id`
|
||||
* was called, constructed, or had a property read, written, tested,
|
||||
* deleted or enumerated. 0: returned (*out), 1: threw (*out is the thrown
|
||||
* value), 2: absent (get/set/delete: the ordinary object answers). Out
|
||||
* strings are runtime-owned until the next host call. Keys are strings
|
||||
* or array indices (numbers). export_keys answers SCR_NV_JSON: the own
|
||||
* enumerable string keys. */
|
||||
int (*export_call)(uint32_t id, const ScrNodeValue *this_, const ScrNodeValue *args,
|
||||
size_t argc, ScrNodeValue *out);
|
||||
int (*export_construct)(uint32_t id, const ScrNodeValue *args, size_t argc, ScrNodeValue *out);
|
||||
int (*export_get)(uint32_t id, const ScrNodeValue *key, ScrNodeValue *out);
|
||||
int (*export_set)(uint32_t id, const ScrNodeValue *key, const ScrNodeValue *value,
|
||||
ScrNodeValue *out);
|
||||
int (*export_has)(uint32_t id, const ScrNodeValue *key);
|
||||
int (*export_delete)(uint32_t id, const ScrNodeValue *key, ScrNodeValue *out);
|
||||
int (*export_keys)(uint32_t id, ScrNodeValue *out);
|
||||
/* The V8 promise representing native promise `id` exists: settle it
|
||||
* (bridge->export_settle) when the native promise settles. */
|
||||
void (*export_watch)(uint32_t id);
|
||||
/* V8 collected the representative of `id`. */
|
||||
void (*export_release)(uint32_t id);
|
||||
} ScrNodeBridgeHost;
|
||||
|
||||
typedef struct ScrNodeBridge {
|
||||
@@ -260,6 +288,8 @@ typedef struct ScrNodeBridge {
|
||||
void (*drop)(uint32_t h);
|
||||
/* The number of live import-table entries (leak tests). */
|
||||
uint32_t (*live_handles)(void);
|
||||
/* Settles the V8 promise representing native promise `id` (NI-5). */
|
||||
void (*export_settle)(uint32_t id, int rejected, const ScrNodeValue *value);
|
||||
} ScrNodeBridge;
|
||||
|
||||
#endif /* SCR_NODE_BRIDGE_H */
|
||||
|
||||
@@ -1165,6 +1165,15 @@ static const ScrNodeBridgeHost ni_host = {
|
||||
&ni_stack_start,
|
||||
&scr_ni_settle_value,
|
||||
&scr_ni_bytes_free,
|
||||
&scr_ni_host_export_call,
|
||||
&scr_ni_host_export_construct,
|
||||
&scr_ni_host_export_get,
|
||||
&scr_ni_host_export_set,
|
||||
&scr_ni_host_export_has,
|
||||
&scr_ni_host_export_delete,
|
||||
&scr_ni_host_export_keys,
|
||||
&scr_ni_host_export_watch,
|
||||
&scr_ni_host_export_release,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
@@ -1210,6 +1219,8 @@ static void ni_stop_atexit(void) {
|
||||
fprintf(stderr, "[node-islands] live handles %u\n", scr_ni_live_handles());
|
||||
}
|
||||
ni_on_node_stack(ni_stop_job, NULL);
|
||||
/* Node is gone: no representative can call in again. */
|
||||
scr_ni_exports_release_all();
|
||||
}
|
||||
|
||||
/* The hosted loop's run_loop on Node's stack. */
|
||||
|
||||
@@ -95,6 +95,9 @@ bool scr_ni_is_function(const ScrDyn *value); /* an island handle that is callab
|
||||
ScrStr *scr_ni_inspect(const ScrDyn *value, double recurse, double depth);
|
||||
ScrDyn *scr_ni_own_keys(ScrDyn *obj, int mode); /* 0 Object.keys, 1 Reflect.ownKeys */
|
||||
ScrPromise *scr_ni_await(ScrDyn *value);
|
||||
/* Island arrays as copies for results typed as arrays of copied values. */
|
||||
ScrDyn *scr_node_island_copy_arrays(ScrDyn *value);
|
||||
ScrPromise *scr_node_island_await_copy(ScrDyn *value);
|
||||
uint32_t scr_ni_live_handles(void); /* live import-table entries (leak tests) */
|
||||
|
||||
/* Between the two island units. */
|
||||
@@ -111,6 +114,36 @@ void scr_ni_bytes_free(void *owner);
|
||||
void scr_ni_handles_install(void);
|
||||
void scr_ni_stop(void);
|
||||
|
||||
/* ── native values in Node (scope-B NI-5, scr_node_island_export.c) ───────
|
||||
* Closures, promises, records, class instances and dyn objects cross into
|
||||
* Node by reference: one export id and one V8 representative per native
|
||||
* referent. The conversions of scr_node_island_handle.c call these. */
|
||||
bool scr_ni_value_out(ScrDyn *v, struct ScrNodeValue *out); /* NI-4's into-V8 conversion */
|
||||
ScrDyn *scr_ni_value_in(const struct ScrNodeValue *v); /* +1 */
|
||||
ScrDyn *scr_ni_arg_in(const struct ScrNodeValue *v); /* +1; island Errors as their ScrError */
|
||||
bool scr_ni_exportable(const ScrDyn *v);
|
||||
void scr_ni_export_out(ScrDyn *v, struct ScrNodeValue *out);
|
||||
ScrDyn *scr_ni_export_value(uint32_t id); /* borrowed, or NULL */
|
||||
uint32_t scr_ni_live_exports(void);
|
||||
/* nodeIsland.classMethods: registers the method table of a class capsule. */
|
||||
ScrDyn *scr_node_island_class_methods(ScrDyn *value, ScrDyn *builder);
|
||||
void scr_ni_bridge_export_settle(uint32_t id, int rejected, const struct ScrNodeValue *value);
|
||||
int scr_ni_host_export_call(uint32_t id, const struct ScrNodeValue *this_,
|
||||
const struct ScrNodeValue *args, size_t argc, struct ScrNodeValue *out);
|
||||
int scr_ni_host_export_construct(uint32_t id, const struct ScrNodeValue *args, size_t argc,
|
||||
struct ScrNodeValue *out);
|
||||
int scr_ni_host_export_get(uint32_t id, const struct ScrNodeValue *key, struct ScrNodeValue *out);
|
||||
int scr_ni_host_export_set(uint32_t id, const struct ScrNodeValue *key,
|
||||
const struct ScrNodeValue *value, struct ScrNodeValue *out);
|
||||
int scr_ni_host_export_has(uint32_t id, const struct ScrNodeValue *key);
|
||||
int scr_ni_host_export_delete(uint32_t id, const struct ScrNodeValue *key, struct ScrNodeValue *out);
|
||||
int scr_ni_host_export_keys(uint32_t id, struct ScrNodeValue *out);
|
||||
void scr_ni_host_export_watch(uint32_t id);
|
||||
void scr_ni_host_export_release(uint32_t id);
|
||||
void scr_ni_exports_release_all(void); /* at exit, after Node stopped */
|
||||
/* Promise settlement callback (SCR_NODE_ISLANDS build of scr_async.c). */
|
||||
void scr_promise_on_settle_sync(ScrPromise *in, void (*cb)(ScrPromise *dst, ScrPromise *src));
|
||||
|
||||
/* Island operations the compiler emits (lib-shared.ts adapters). */
|
||||
ScrDyn *scr_node_island_apply(ScrDyn *fn, ScrDyn *args);
|
||||
/* fn called with `this` (a method read before its arguments ran). */
|
||||
|
||||
@@ -0,0 +1,602 @@
|
||||
/* Node islands: native values in Node (scope-B milestone NI-5). Links with
|
||||
* scr_node_island.c (the `nodeIsland` runtime feature) and nothing else.
|
||||
*
|
||||
* The export table. A native value that crosses into Node by reference (a
|
||||
* closure, a promise, a record or class instance, a checked-dynamic object
|
||||
* or array) gets an export id: the table holds it strongly (+1) and the
|
||||
* bridge keeps one V8 representative per id, weakly. The same native value
|
||||
* always gets the same id, so it always meets the same V8 object, and a
|
||||
* representative crossing back is answered with the original value
|
||||
* (SCR_NV_EXPORT), so identity holds both ways. Identity is the referent,
|
||||
* not the dyn box: a record boxed twice is two capsules over one record.
|
||||
*
|
||||
* What the representative does (scr_node_bridge.cc):
|
||||
* - closures are V8 functions whose calls run export_call: the arguments
|
||||
* cross into the runtime per the boundary rules and meet the closure's
|
||||
* declared parameter types through its dyn thunk (a mismatch throws the
|
||||
* runtime's TypeError into JavaScript); the result crosses back; a
|
||||
* runtime exception becomes a JavaScript throw and a JavaScript throw
|
||||
* inside a nested island call propagates out as a runtime exception;
|
||||
* - promises are V8 promises resolved inside the runtime promise's own
|
||||
* settlement (export_watch), so their reactions queue exactly when a
|
||||
* JavaScript promise's would;
|
||||
* - objects use interceptors: every property read, write, `in`, delete
|
||||
* and key enumeration goes to the native storage (Reflect.get/set over
|
||||
* the dyn value: a typed record capsule reads and commits its fields).
|
||||
*
|
||||
* When V8 collects a representative, the bridge reports export_release at
|
||||
* its next entry (never inside a GC) and the table drops its reference.
|
||||
* Cycles through both heaps leak until NI-8. */
|
||||
#define _XOPEN_SOURCE 700
|
||||
#define _DARWIN_C_SOURCE 1
|
||||
#include "scr_runtime.h"
|
||||
#include "scr_node_bridge.h"
|
||||
#include "scr_node_island.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#if !defined(_WIN32) && !defined(__wasi__)
|
||||
|
||||
typedef struct {
|
||||
ScrDyn *value; /* +1; NULL for a free slot */
|
||||
const void *identity;
|
||||
uint32_t flags;
|
||||
} NiExport;
|
||||
|
||||
static NiExport *ni_exports = NULL; /* index = id; id 0 unused */
|
||||
static uint32_t ni_exports_len = 1, ni_exports_cap = 0;
|
||||
static uint32_t *ni_free = NULL;
|
||||
static uint32_t ni_nfree = 0, ni_free_cap = 0;
|
||||
|
||||
/* identity -> id, open addressing over a power-of-two table. */
|
||||
typedef struct {
|
||||
const void *key;
|
||||
uint32_t id;
|
||||
} NiIdEntry;
|
||||
static NiIdEntry *ni_ids = NULL;
|
||||
static size_t ni_ids_cap = 0, ni_ids_len = 0, ni_ids_dead = 0;
|
||||
static const char ni_tomb;
|
||||
|
||||
SCR_COLD static void ni_export_oom(void) { scr_trap("scriptc: out of memory\n"); }
|
||||
|
||||
static size_t ni_hash(const void *p) {
|
||||
uintptr_t x = (uintptr_t)p;
|
||||
x ^= x >> 33;
|
||||
x *= 0xff51afd7ed558ccdULL;
|
||||
x ^= x >> 33;
|
||||
return (size_t)x;
|
||||
}
|
||||
|
||||
static void ni_ids_put(const void *key, uint32_t id);
|
||||
|
||||
static void ni_ids_grow(void) {
|
||||
NiIdEntry *old = ni_ids;
|
||||
size_t old_cap = ni_ids_cap;
|
||||
ni_ids_cap = old_cap ? old_cap * 2 : 64;
|
||||
while (ni_ids_len * 2 >= ni_ids_cap) ni_ids_cap *= 2;
|
||||
ni_ids = calloc(ni_ids_cap, sizeof *ni_ids);
|
||||
if (ni_ids == NULL) ni_export_oom();
|
||||
ni_ids_len = 0;
|
||||
ni_ids_dead = 0;
|
||||
for (size_t i = 0; i < old_cap; i++)
|
||||
if (old[i].key != NULL && old[i].key != &ni_tomb) ni_ids_put(old[i].key, old[i].id);
|
||||
free(old);
|
||||
}
|
||||
|
||||
static void ni_ids_put(const void *key, uint32_t id) {
|
||||
if ((ni_ids_len + ni_ids_dead + 1) * 2 > ni_ids_cap) ni_ids_grow();
|
||||
size_t mask = ni_ids_cap - 1;
|
||||
for (size_t i = ni_hash(key) & mask;; i = (i + 1) & mask) {
|
||||
if (ni_ids[i].key == NULL || ni_ids[i].key == &ni_tomb) {
|
||||
if (ni_ids[i].key == &ni_tomb) ni_ids_dead--;
|
||||
ni_ids[i].key = key;
|
||||
ni_ids[i].id = id;
|
||||
ni_ids_len++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t ni_ids_get(const void *key) {
|
||||
if (ni_ids_cap == 0) return 0;
|
||||
size_t mask = ni_ids_cap - 1;
|
||||
for (size_t i = ni_hash(key) & mask;; i = (i + 1) & mask) {
|
||||
if (ni_ids[i].key == NULL) return 0;
|
||||
if (ni_ids[i].key == key) return ni_ids[i].id;
|
||||
}
|
||||
}
|
||||
|
||||
static void ni_ids_del(const void *key) {
|
||||
if (ni_ids_cap == 0) return;
|
||||
size_t mask = ni_ids_cap - 1;
|
||||
for (size_t i = ni_hash(key) & mask;; i = (i + 1) & mask) {
|
||||
if (ni_ids[i].key == NULL) return;
|
||||
if (ni_ids[i].key == key) {
|
||||
ni_ids[i].key = &ni_tomb;
|
||||
ni_ids_len--;
|
||||
ni_ids_dead++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The identity of a value that crosses by reference: the referent for
|
||||
* boxes that wrap one (closures, typed capsules), a promise's shared
|
||||
* identity (a typed promise boxes a fresh adapter each time it crosses),
|
||||
* else the box. */
|
||||
static const void *ni_identity(const ScrDyn *v) {
|
||||
switch (v->kind) {
|
||||
case SCR_DYN_FUNC:
|
||||
return v->v.fn.clo != NULL ? (const void *)v->v.fn.clo : (const void *)v;
|
||||
case SCR_DYN_PROMISE:
|
||||
return scr_promise_identity_key(v->v.promise);
|
||||
case SCR_DYN_TYPED_REF:
|
||||
return v->v.typed_ref.ptr;
|
||||
default:
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t scr_ni_export_flags(const ScrDyn *v) {
|
||||
switch (v->kind) {
|
||||
case SCR_DYN_FUNC:
|
||||
return SCR_NVF_FUNCTION;
|
||||
case SCR_DYN_PROMISE:
|
||||
return SCR_NVF_PROMISE;
|
||||
case SCR_DYN_ARR:
|
||||
return SCR_NVF_ARRAY;
|
||||
case SCR_DYN_TYPED_REF:
|
||||
return scr_dyn_typed_ref_array_ops(v) != NULL ? SCR_NVF_ARRAY : 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Whether `v` crosses into Node as an export. Arrays keep NI-4's JSON
|
||||
* copy for now: an interceptor object is not an Array in V8 (NOTES.md). */
|
||||
bool scr_ni_exportable(const ScrDyn *v) {
|
||||
switch (v->kind) {
|
||||
case SCR_DYN_FUNC:
|
||||
case SCR_DYN_PROMISE:
|
||||
case SCR_DYN_OBJ:
|
||||
return true;
|
||||
case SCR_DYN_TYPED_REF:
|
||||
return scr_dyn_typed_ref_array_ops(v) == NULL;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* The export id of `v` (borrowed), minting one on first crossing. */
|
||||
uint32_t scr_ni_export_id(ScrDyn *v) {
|
||||
const void *identity = ni_identity(v);
|
||||
uint32_t id = ni_ids_get(identity);
|
||||
if (id != 0) return id;
|
||||
if (ni_nfree > 0) {
|
||||
id = ni_free[--ni_nfree];
|
||||
} else {
|
||||
if (ni_exports_len >= ni_exports_cap) {
|
||||
uint32_t cap = ni_exports_cap ? ni_exports_cap * 2 : 64;
|
||||
NiExport *grown = realloc(ni_exports, cap * sizeof *grown);
|
||||
if (grown == NULL) ni_export_oom();
|
||||
ni_exports = grown;
|
||||
ni_exports_cap = cap;
|
||||
if (ni_exports_len == 1) memset(&ni_exports[0], 0, sizeof ni_exports[0]);
|
||||
}
|
||||
id = ni_exports_len++;
|
||||
}
|
||||
ni_exports[id].value = scr_dyn_retain(v);
|
||||
ni_exports[id].identity = identity;
|
||||
/* The table keeps a promise's identity cell while the export lives: a
|
||||
* freed cell's address could come back as another promise's. */
|
||||
if (v->kind == SCR_DYN_PROMISE) scr_promise_identity_hold(identity);
|
||||
ni_exports[id].flags = scr_ni_export_flags(v);
|
||||
ni_ids_put(identity, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
/* The value of export `id` (borrowed), or NULL. */
|
||||
ScrDyn *scr_ni_export_value(uint32_t id) {
|
||||
if (id == 0 || id >= ni_exports_len) return NULL;
|
||||
return ni_exports[id].value;
|
||||
}
|
||||
|
||||
/* Fills `out` with the export of `v` (borrowed). */
|
||||
void scr_ni_export_out(ScrDyn *v, ScrNodeValue *out) {
|
||||
uint32_t id = scr_ni_export_id(v);
|
||||
memset(out, 0, sizeof *out);
|
||||
out->tag = SCR_NV_EXPORT;
|
||||
out->flags = ni_exports[id].flags;
|
||||
out->u.exp.id = id;
|
||||
if (v->kind == SCR_DYN_FUNC) {
|
||||
out->u.exp.length = v->v.fn.arity;
|
||||
if (v->v.fn.name != NULL) {
|
||||
out->u.exp.name = v->v.fn.name;
|
||||
out->u.exp.name_len = strlen(v->v.fn.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t scr_ni_live_exports(void) {
|
||||
uint32_t n = 0;
|
||||
for (uint32_t i = 1; i < ni_exports_len; i++) n += ni_exports[i].value != NULL;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* ── host callbacks ───────────────────────────────────────────────────── */
|
||||
|
||||
/* Values the runtime hands the bridge stay alive until the next host call
|
||||
* (their string data points into them). */
|
||||
static ScrDyn *ni_keep = NULL;
|
||||
|
||||
static void ni_keep_set(ScrDyn *v) {
|
||||
ScrDyn *old = ni_keep;
|
||||
ni_keep = v;
|
||||
scr_dyn_release(old);
|
||||
}
|
||||
|
||||
/* The pending runtime exception as an out value: 1. */
|
||||
static int ni_threw(ScrNodeValue *out) {
|
||||
ScrCaught *caught = scr_exc_take();
|
||||
ScrDyn *thrown = scr_caught_to_dyn(caught);
|
||||
scr_caught_release(caught);
|
||||
if (thrown == NULL) thrown = scr_dyn_retain(scr_dyn_undefined());
|
||||
if (!scr_ni_value_out(thrown, out)) {
|
||||
/* The thrown value itself cannot cross: throw what that raised. */
|
||||
scr_dyn_release(thrown);
|
||||
caught = scr_exc_take();
|
||||
thrown = scr_caught_to_dyn(caught);
|
||||
scr_caught_release(caught);
|
||||
if (thrown == NULL || !scr_ni_value_out(thrown, out)) {
|
||||
scr_exc_clear();
|
||||
memset(out, 0, sizeof *out);
|
||||
out->tag = SCR_NV_UNDEFINED;
|
||||
}
|
||||
}
|
||||
ni_keep_set(thrown);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* A runtime result as an out value: 0, or 1 when it cannot cross. */
|
||||
static int ni_returned(ScrDyn *result, ScrNodeValue *out) {
|
||||
if (!scr_ni_value_out(result, out)) {
|
||||
scr_dyn_release(result);
|
||||
return ni_threw(out);
|
||||
}
|
||||
ni_keep_set(result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Arguments as runtime values (+1 each). */
|
||||
static ScrDyn **ni_args_in(const ScrNodeValue *args, size_t argc, ScrDyn **small, size_t nsmall) {
|
||||
ScrDyn **out = argc <= nsmall ? small : malloc(argc * sizeof *out);
|
||||
if (out == NULL) ni_export_oom();
|
||||
for (size_t i = 0; i < argc; i++) out[i] = scr_ni_arg_in(&args[i]);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void ni_args_free(ScrDyn **args, size_t argc, ScrDyn **small) {
|
||||
for (size_t i = 0; i < argc; i++) scr_dyn_release(args[i]);
|
||||
if (args != small) free(args);
|
||||
}
|
||||
|
||||
int scr_ni_host_export_call(uint32_t id, const ScrNodeValue *this_, const ScrNodeValue *args,
|
||||
size_t argc, ScrNodeValue *out) {
|
||||
ni_keep_set(NULL);
|
||||
ScrDyn *fn = scr_ni_export_value(id);
|
||||
if (fn == NULL) {
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, "native function is gone", 23);
|
||||
return ni_threw(out);
|
||||
}
|
||||
scr_dyn_retain(fn);
|
||||
ScrDyn *small[8];
|
||||
ScrDyn **in = ni_args_in(args, argc, small, 8);
|
||||
ScrDyn *self = this_ != NULL ? scr_ni_value_in(this_) : scr_dyn_retain(scr_dyn_undefined());
|
||||
scr_dyn_this_push_dyn(self);
|
||||
ScrDyn *result = scr_dyn_call(fn, in, argc, "callback");
|
||||
scr_dyn_this_pop();
|
||||
scr_dyn_release(self);
|
||||
ni_args_free(in, argc, small);
|
||||
scr_dyn_release(fn);
|
||||
if (result == NULL || scr_exc_pending()) {
|
||||
scr_dyn_release(result);
|
||||
return ni_threw(out);
|
||||
}
|
||||
return ni_returned(result, out);
|
||||
}
|
||||
|
||||
int scr_ni_host_export_construct(uint32_t id, const ScrNodeValue *args, size_t argc,
|
||||
ScrNodeValue *out) {
|
||||
ni_keep_set(NULL);
|
||||
ScrDyn *ctor = scr_ni_export_value(id);
|
||||
if (ctor == NULL) {
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, "native function is gone", 23);
|
||||
return ni_threw(out);
|
||||
}
|
||||
scr_dyn_retain(ctor);
|
||||
ScrDyn *list = scr_dyn_new_arr();
|
||||
for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(list, scr_ni_value_in(&args[i]));
|
||||
ScrStr *what = scr_str_new("constructor", 11);
|
||||
ScrDyn *result = scr_dyn_construct(ctor, list, what);
|
||||
scr_str_release(what);
|
||||
scr_dyn_release(list);
|
||||
scr_dyn_release(ctor);
|
||||
if (result == NULL || scr_exc_pending()) {
|
||||
scr_dyn_release(result);
|
||||
return ni_threw(out);
|
||||
}
|
||||
return ni_returned(result, out);
|
||||
}
|
||||
|
||||
/* ── class method tables ──────────────────────────────────────────────
|
||||
* A class instance's capsule holds its fields; its prototype methods come
|
||||
* from a table the compiler's builder makes once per class (name -> the
|
||||
* unbound method value, a dyn function whose thunk checks the receiver),
|
||||
* keyed by the capsule's type key. */
|
||||
typedef struct {
|
||||
const char *key;
|
||||
size_t key_len;
|
||||
ScrDyn *table; /* +1 dyn object */
|
||||
} NiMethods;
|
||||
static NiMethods *ni_methods = NULL;
|
||||
static size_t ni_nmethods = 0, ni_methods_cap = 0;
|
||||
|
||||
static ScrDyn *ni_methods_of(const ScrDyn *v) {
|
||||
if (v->kind != SCR_DYN_TYPED_REF || v->v.typed_ref.type_key == NULL) return NULL;
|
||||
const char *key = v->v.typed_ref.type_key;
|
||||
size_t len = v->v.typed_ref.type_key_len;
|
||||
for (size_t i = 0; i < ni_nmethods; i++)
|
||||
if (ni_methods[i].key_len == len && memcmp(ni_methods[i].key, key, len) == 0)
|
||||
return ni_methods[i].table;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ScrDyn *scr_node_island_class_methods(ScrDyn *value, ScrDyn *builder) {
|
||||
if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key != NULL &&
|
||||
ni_methods_of(value) == NULL) {
|
||||
ScrDyn *table = scr_dyn_call(builder, NULL, 0, "class methods");
|
||||
if (table == NULL) return NULL;
|
||||
if (ni_nmethods == ni_methods_cap) {
|
||||
ni_methods_cap = ni_methods_cap ? ni_methods_cap * 2 : 8;
|
||||
NiMethods *grown = realloc(ni_methods, ni_methods_cap * sizeof *grown);
|
||||
if (grown == NULL) ni_export_oom();
|
||||
ni_methods = grown;
|
||||
}
|
||||
ni_methods[ni_nmethods].key = value->v.typed_ref.type_key; /* static */
|
||||
ni_methods[ni_nmethods].key_len = value->v.typed_ref.type_key_len;
|
||||
ni_methods[ni_nmethods].table = table;
|
||||
ni_nmethods++;
|
||||
}
|
||||
return scr_dyn_retain(value);
|
||||
}
|
||||
|
||||
/* A prototype method of a class capsule (+1), or NULL. An own property the
|
||||
* island wrote under the method's name (kept in the capsule's materialized
|
||||
* view) hides the method, as an own property hides a prototype's in Node. */
|
||||
static ScrDyn *ni_class_method(const ScrDyn *target, const ScrDyn *key) {
|
||||
if (key->kind != SCR_DYN_STR) return NULL;
|
||||
ScrDyn *table = ni_methods_of(target);
|
||||
if (table == NULL) return NULL;
|
||||
const ScrDyn *view = target->v.typed_ref.materialized;
|
||||
if (view != NULL && view->kind == SCR_DYN_OBJ &&
|
||||
scr_dyn_obj_get(view, key->v.str->data, key->v.str->len) != NULL)
|
||||
return NULL;
|
||||
ScrDyn *m = scr_dyn_obj_get(table, key->v.str->data, key->v.str->len);
|
||||
return m != NULL ? scr_dyn_retain(m) : NULL;
|
||||
}
|
||||
|
||||
int scr_ni_host_export_get(uint32_t id, const ScrNodeValue *key, ScrNodeValue *out) {
|
||||
ni_keep_set(NULL);
|
||||
ScrDyn *target = scr_ni_export_value(id);
|
||||
if (target == NULL) return 2;
|
||||
ScrDyn *k = scr_ni_value_in(key);
|
||||
ScrDyn *pk = scr_dyn_property_key_value(k);
|
||||
if (pk == NULL) {
|
||||
scr_dyn_release(k);
|
||||
return ni_threw(out);
|
||||
}
|
||||
int present = 0;
|
||||
if (pk->kind == SCR_DYN_STR) present = scr_dyn_in(target, pk->v.str);
|
||||
if (!present && !scr_exc_pending()) {
|
||||
scr_dyn_release(pk);
|
||||
scr_dyn_release(k);
|
||||
return 2;
|
||||
}
|
||||
ScrDyn *result = scr_exc_pending() ? NULL : scr_dyn_reflect_get(target, pk, target);
|
||||
if (result != NULL && result->kind == SCR_DYN_UNDEF) {
|
||||
ScrDyn *method = ni_class_method(target, pk);
|
||||
if (method != NULL) {
|
||||
scr_dyn_release(result);
|
||||
result = method;
|
||||
}
|
||||
}
|
||||
scr_dyn_release(pk);
|
||||
scr_dyn_release(k);
|
||||
if (result == NULL || scr_exc_pending()) {
|
||||
scr_dyn_release(result);
|
||||
return ni_threw(out);
|
||||
}
|
||||
return ni_returned(result, out);
|
||||
}
|
||||
|
||||
int scr_ni_host_export_set(uint32_t id, const ScrNodeValue *key, const ScrNodeValue *value,
|
||||
ScrNodeValue *out) {
|
||||
ni_keep_set(NULL);
|
||||
ScrDyn *target = scr_ni_export_value(id);
|
||||
if (target == NULL) return 2;
|
||||
ScrDyn *k = scr_ni_value_in(key);
|
||||
ScrDyn *v = scr_ni_value_in(value);
|
||||
bool ok = scr_dyn_reflect_set(target, k, v, target);
|
||||
scr_dyn_release(v);
|
||||
scr_dyn_release(k);
|
||||
if (scr_exc_pending()) return ni_threw(out);
|
||||
(void)ok; /* a refused write is silent outside strict mode; Node's setter
|
||||
* interceptor reports the same */
|
||||
memset(out, 0, sizeof *out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int scr_ni_host_export_has(uint32_t id, const ScrNodeValue *key) {
|
||||
ScrDyn *target = scr_ni_export_value(id);
|
||||
if (target == NULL) return 0;
|
||||
ScrDyn *k = scr_ni_value_in(key);
|
||||
ScrDyn *pk = scr_dyn_property_key_value(k);
|
||||
int present = 0;
|
||||
if (pk != NULL && pk->kind == SCR_DYN_STR) present = scr_dyn_in(target, pk->v.str);
|
||||
if (scr_exc_pending()) scr_exc_clear();
|
||||
scr_dyn_release(pk);
|
||||
scr_dyn_release(k);
|
||||
return present;
|
||||
}
|
||||
|
||||
int scr_ni_host_export_delete(uint32_t id, const ScrNodeValue *key, ScrNodeValue *out) {
|
||||
ni_keep_set(NULL);
|
||||
ScrDyn *target = scr_ni_export_value(id);
|
||||
if (target == NULL) return 2;
|
||||
ScrDyn *k = scr_ni_value_in(key);
|
||||
scr_dyn_key_delete_computed(target, k, false);
|
||||
scr_dyn_release(k);
|
||||
if (scr_exc_pending()) return ni_threw(out);
|
||||
memset(out, 0, sizeof *out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Own enumerable string keys, in JS order, as an array value. */
|
||||
int scr_ni_host_export_keys(uint32_t id, ScrNodeValue *out) {
|
||||
ni_keep_set(NULL);
|
||||
ScrDyn *target = scr_ni_export_value(id);
|
||||
ScrDyn *keys = target != NULL ? scr_dyn_obj_keys(target) : scr_dyn_new_arr();
|
||||
if (keys == NULL || scr_exc_pending()) {
|
||||
scr_dyn_release(keys);
|
||||
return ni_threw(out);
|
||||
}
|
||||
/* Keys cross as a JSON copy: an array of strings. */
|
||||
ScrStr *gap = scr_str_new("", 0);
|
||||
ScrDyn *json = scr_json_stringify_replacer(keys, scr_dyn_undefined(), gap);
|
||||
scr_str_release(gap);
|
||||
scr_dyn_release(keys);
|
||||
if (json == NULL || json->kind != SCR_DYN_STR) {
|
||||
scr_dyn_release(json);
|
||||
if (scr_exc_pending()) return ni_threw(out);
|
||||
json = scr_dyn_new_str(scr_str_new("[]", 2));
|
||||
}
|
||||
memset(out, 0, sizeof *out);
|
||||
out->tag = SCR_NV_JSON;
|
||||
out->u.str.data = json->v.str->data;
|
||||
out->u.str.len = json->v.str->len;
|
||||
ni_keep_set(json);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void scr_ni_host_export_release(uint32_t id) {
|
||||
if (id == 0 || id >= ni_exports_len || ni_exports[id].value == NULL) return;
|
||||
ScrDyn *v = ni_exports[id].value;
|
||||
ni_ids_del(ni_exports[id].identity);
|
||||
if (v->kind == SCR_DYN_PROMISE) scr_promise_identity_drop(ni_exports[id].identity);
|
||||
ni_exports[id].value = NULL;
|
||||
ni_exports[id].identity = NULL;
|
||||
if (ni_nfree == ni_free_cap) {
|
||||
uint32_t cap = ni_free_cap ? ni_free_cap * 2 : 64;
|
||||
uint32_t *grown = realloc(ni_free, cap * sizeof *grown);
|
||||
if (grown == NULL) ni_export_oom();
|
||||
ni_free = grown;
|
||||
ni_free_cap = cap;
|
||||
}
|
||||
ni_free[ni_nfree++] = id;
|
||||
scr_dyn_release(v);
|
||||
}
|
||||
|
||||
/* ── promises into Node ───────────────────────────────────────────────── */
|
||||
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
int rejected;
|
||||
const ScrNodeValue *value;
|
||||
} NiSettle;
|
||||
|
||||
static void ni_settle_job(void *p) {
|
||||
NiSettle *a = p;
|
||||
scr_ni_bridge_loaded()->export_settle(a->id, a->rejected, a->value);
|
||||
}
|
||||
|
||||
void scr_ni_bridge_export_settle(uint32_t id, int rejected, const ScrNodeValue *value) {
|
||||
if (scr_ni_bridge_loaded() == NULL) return;
|
||||
NiSettle a = {id, rejected, value};
|
||||
scr_ni_on_node_stack(ni_settle_job, &a);
|
||||
}
|
||||
|
||||
/* Settles the V8 promise of export `id` from the settled runtime promise. */
|
||||
static void ni_settle_export(uint32_t id, ScrPromise *p) {
|
||||
ScrNodeValue value;
|
||||
ScrDyn *payload;
|
||||
int rejected = scr_promise_is_rejected(p);
|
||||
if (rejected) {
|
||||
payload = scr_promise_reason_dyn(p);
|
||||
} else if (scr_promise_payload_is_dyn(p)) {
|
||||
payload = (ScrDyn *)scr_promise_payload_ref(p);
|
||||
} else {
|
||||
payload = NULL; /* a void fulfillment */
|
||||
}
|
||||
if (payload == NULL) payload = scr_dyn_retain(scr_dyn_undefined());
|
||||
if (!scr_ni_value_out(payload, &value)) {
|
||||
/* The payload cannot cross: the promise rejects with that error. */
|
||||
scr_dyn_release(payload);
|
||||
ScrCaught *caught = scr_exc_take();
|
||||
payload = scr_caught_to_dyn(caught);
|
||||
scr_caught_release(caught);
|
||||
rejected = 1;
|
||||
if (payload == NULL || !scr_ni_value_out(payload, &value)) {
|
||||
scr_exc_clear();
|
||||
memset(&value, 0, sizeof value);
|
||||
}
|
||||
}
|
||||
scr_ni_bridge_export_settle(id, rejected, &value);
|
||||
scr_dyn_release(payload);
|
||||
}
|
||||
|
||||
static void ni_promise_settled(ScrPromise *dst, ScrPromise *src) {
|
||||
(void)dst;
|
||||
uint32_t id = ni_ids_get(scr_promise_identity_key(src));
|
||||
if (id != 0) ni_settle_export(id, src);
|
||||
}
|
||||
|
||||
/* The bridge created the V8 promise of export `id`: it settles inside the
|
||||
* runtime promise's settlement, or now when that already happened. The
|
||||
* runtime promise counts as handled from here on; the V8 promise carries
|
||||
* Node's own unhandled-rejection tracking. */
|
||||
void scr_ni_host_export_watch(uint32_t id) {
|
||||
ScrDyn *v = scr_ni_export_value(id);
|
||||
if (v == NULL || v->kind != SCR_DYN_PROMISE) return;
|
||||
ScrPromise *p = v->v.promise;
|
||||
scr_promise_mark_handled(p);
|
||||
if (!scr_promise_is_pending(p)) {
|
||||
ni_settle_export(id, p);
|
||||
return;
|
||||
}
|
||||
scr_promise_on_settle_sync(p, ni_promise_settled);
|
||||
}
|
||||
|
||||
/* At exit, once Node stopped: everything the export table and the class
|
||||
* tables hold (the RC audit counts them). */
|
||||
void scr_ni_exports_release_all(void) {
|
||||
ni_keep_set(NULL);
|
||||
for (uint32_t i = 1; i < ni_exports_len; i++) {
|
||||
ScrDyn *v = ni_exports[i].value;
|
||||
if (v == NULL) continue;
|
||||
if (v->kind == SCR_DYN_PROMISE) scr_promise_identity_drop(ni_exports[i].identity);
|
||||
ni_exports[i].value = NULL;
|
||||
scr_dyn_release(v);
|
||||
}
|
||||
ni_exports_len = 1;
|
||||
ni_nfree = 0;
|
||||
free(ni_ids);
|
||||
ni_ids = NULL;
|
||||
ni_ids_cap = ni_ids_len = ni_ids_dead = 0;
|
||||
for (size_t i = 0; i < ni_nmethods; i++) scr_dyn_release(ni_methods[i].table);
|
||||
ni_nmethods = 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -330,56 +330,78 @@ static ScrDyn *nh_from_node(const ScrNodeValue *v) {
|
||||
return d;
|
||||
}
|
||||
case SCR_NV_BYTES: return nh_bytes_box(v);
|
||||
case SCR_NV_EXPORT: {
|
||||
/* A native value's representative came back: the value itself. */
|
||||
ScrDyn *d = scr_ni_export_value(v->u.exp.id);
|
||||
return d != NULL ? scr_dyn_retain(d) : scr_dyn_undefined();
|
||||
}
|
||||
default: return scr_dyn_undefined();
|
||||
}
|
||||
}
|
||||
|
||||
/* The ScrError of an island Error handle (+1): the one already pinned to
|
||||
* it, else a new one of the nearest intrinsic class, pinned. */
|
||||
static ScrError *nh_error_of(const ScrNodeValue *v) {
|
||||
ScrNodeRef *ref = nh_ref(v->u.handle, v->flags);
|
||||
ScrError *pinned = nh_pinned_obj(ref, NH_PIN_ERROR);
|
||||
if (pinned != NULL) {
|
||||
nh_ref_release(ref);
|
||||
return scr_error_retain(pinned);
|
||||
}
|
||||
ScrNodeErrorInfo info;
|
||||
nh_bridge()->error_info(ref->id, &info);
|
||||
ScrStr *message = nh_str(info.message ? info.message : "", info.message_len);
|
||||
ScrError *e = scr_error_new((int)info.kind, message);
|
||||
scr_str_release(message);
|
||||
if (info.name != NULL) {
|
||||
ScrStr *name = nh_str(info.name, info.name_len);
|
||||
ScrStr *deflt = scr_error_default_name(e);
|
||||
bool differs = deflt == NULL || deflt->len != name->len ||
|
||||
memcmp(deflt->data, name->data, name->len) != 0;
|
||||
if (deflt != NULL) scr_str_release(deflt);
|
||||
if (differs) {
|
||||
scr_str_release(e->name);
|
||||
e->name = name;
|
||||
e->name_present = true;
|
||||
} else {
|
||||
scr_str_release(name);
|
||||
}
|
||||
}
|
||||
if (info.stack != NULL) {
|
||||
ScrStr *stack = nh_str(info.stack, info.stack_len);
|
||||
scr_error_set_stack(e, stack);
|
||||
scr_str_release(stack);
|
||||
}
|
||||
if (info.code != NULL) {
|
||||
ScrStr *code = nh_str(info.code, info.code_len);
|
||||
scr_error_set_code_value(e, code);
|
||||
scr_str_release(code);
|
||||
}
|
||||
nh_pin(e, NH_PIN_ERROR, ref);
|
||||
nh_ref_release(ref);
|
||||
return e;
|
||||
}
|
||||
|
||||
/* An argument of a native callback (scr_node_island_export.c): island
|
||||
* Errors arrive as their pinned ScrError, as thrown ones do, so typed code
|
||||
* sees `instanceof RangeError`, `message` and `code`. +1. */
|
||||
ScrDyn *scr_ni_arg_in(const ScrNodeValue *v) {
|
||||
if (v->tag == SCR_NV_HANDLE && (v->flags & SCR_NVF_ERROR)) {
|
||||
ScrError *e = nh_error_of(v);
|
||||
ScrDyn *d = scr_dyn_from_error(e);
|
||||
scr_error_release(e);
|
||||
return d;
|
||||
}
|
||||
return scr_ni_value_in(v);
|
||||
}
|
||||
|
||||
/* Leaves a thrown island value pending as a scriptc exception. An Error
|
||||
* becomes a ScrError of the nearest intrinsic class with V8's name,
|
||||
* message, stack and string code, pinned to the V8 error so that it is
|
||||
* that error again if it crosses back. */
|
||||
static void nh_throw_value(const ScrNodeValue *v) {
|
||||
if (v->tag == SCR_NV_HANDLE && (v->flags & SCR_NVF_ERROR)) {
|
||||
ScrNodeRef *ref = nh_ref(v->u.handle, v->flags);
|
||||
/* An island error the program already holds is that ScrError again. */
|
||||
ScrError *known = nh_pinned_obj(ref, NH_PIN_ERROR);
|
||||
if (known != NULL) {
|
||||
nh_ref_release(ref);
|
||||
scr_throw_obj(scr_error_retain(known), &scr_error_retain_v, &scr_error_release_v,
|
||||
scr_error_trace_arg());
|
||||
return;
|
||||
}
|
||||
ScrNodeErrorInfo info;
|
||||
nh_bridge()->error_info(ref->id, &info);
|
||||
ScrStr *message = nh_str(info.message ? info.message : "", info.message_len);
|
||||
ScrError *e = scr_error_new((int)info.kind, message);
|
||||
scr_str_release(message);
|
||||
if (info.name != NULL) {
|
||||
ScrStr *name = nh_str(info.name, info.name_len);
|
||||
ScrStr *deflt = scr_error_default_name(e);
|
||||
bool differs = deflt == NULL || deflt->len != name->len ||
|
||||
memcmp(deflt->data, name->data, name->len) != 0;
|
||||
if (deflt != NULL) scr_str_release(deflt);
|
||||
if (differs) {
|
||||
scr_str_release(e->name);
|
||||
e->name = name;
|
||||
e->name_present = true;
|
||||
} else {
|
||||
scr_str_release(name);
|
||||
}
|
||||
}
|
||||
if (info.stack != NULL) {
|
||||
ScrStr *stack = nh_str(info.stack, info.stack_len);
|
||||
scr_error_set_stack(e, stack);
|
||||
scr_str_release(stack);
|
||||
}
|
||||
if (info.code != NULL) {
|
||||
ScrStr *code = nh_str(info.code, info.code_len);
|
||||
scr_error_set_code_value(e, code);
|
||||
scr_str_release(code);
|
||||
}
|
||||
nh_pin(e, NH_PIN_ERROR, ref);
|
||||
nh_ref_release(ref);
|
||||
ScrError *e = nh_error_of(v);
|
||||
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
return;
|
||||
}
|
||||
@@ -462,6 +484,12 @@ SCR_COLD static bool nh_refuse(const char *what) {
|
||||
* native objects and arrays). Until then: plain dyn objects and arrays
|
||||
* cross as a JSON copy (the NI-4 shortcut), anything else is refused. */
|
||||
static bool nh_export_value(ScrDyn *v, ScrNodeValue *out, NhKeep *keep) {
|
||||
/* NI-5: closures, promises, records, class instances and dyn objects
|
||||
* cross by reference (scr_node_island_export.c). */
|
||||
if (scr_ni_exportable(v)) {
|
||||
scr_ni_export_out(v, out);
|
||||
return true;
|
||||
}
|
||||
if (v->kind == SCR_DYN_OBJ || v->kind == SCR_DYN_ARR) {
|
||||
ScrStr *gap = scr_str_new("", 0);
|
||||
ScrDyn *json = scr_json_stringify_replacer(v, scr_dyn_undefined(), gap);
|
||||
@@ -989,6 +1017,74 @@ ScrStr *scr_ni_inspect(const ScrDyn *value, double recurse, double depth) {
|
||||
return s;
|
||||
}
|
||||
|
||||
/* The conversions for scr_node_island_export.c: what a value_out keeps
|
||||
* alive lives until the next value_out (the bridge reads it at once). */
|
||||
static NhKeep nh_out_keep;
|
||||
|
||||
bool scr_ni_value_out(ScrDyn *v, ScrNodeValue *out) {
|
||||
NhKeep old = nh_out_keep;
|
||||
memset(&nh_out_keep, 0, sizeof nh_out_keep);
|
||||
nh_keep_done(&old);
|
||||
return nh_to_node(v, out, &nh_out_keep);
|
||||
}
|
||||
|
||||
ScrDyn *scr_ni_value_in(const ScrNodeValue *v) {
|
||||
ScrDyn *d = nh_from_node(v);
|
||||
return d != NULL ? d : scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
|
||||
/* ── island arrays as copies (results typed as arrays of copied values) ─
|
||||
* An island array whose declared native type is an array of primitives or
|
||||
* strings (or arrays of those) crosses as a copy of its elements, the
|
||||
* values the checked cast then meets; nested island arrays copy too, any
|
||||
* other value (a native export coming back, an island object) is itself. */
|
||||
ScrDyn *scr_node_island_copy_arrays(ScrDyn *value) {
|
||||
ScrNodeRef *ref = nh_ref_of(value);
|
||||
if (ref == NULL || (ref->flags & SCR_NVF_ARRAY) == 0) return scr_dyn_retain(value);
|
||||
ScrDyn *len = scr_ni_get_str(value, "length", 6);
|
||||
if (len == NULL) return NULL;
|
||||
double n = len->kind == SCR_DYN_NUM ? len->v.num : 0;
|
||||
scr_dyn_release(len);
|
||||
ScrDyn *out = scr_dyn_new_arr();
|
||||
for (double i = 0; i < n; i++) {
|
||||
ScrDyn *key = scr_dyn_new_num(i);
|
||||
ScrDyn *element = scr_ni_get(value, key);
|
||||
scr_dyn_release(key);
|
||||
ScrDyn *copy = element != NULL ? scr_node_island_copy_arrays(element) : NULL;
|
||||
scr_dyn_release(element);
|
||||
if (copy == NULL) {
|
||||
scr_dyn_release(out);
|
||||
return NULL;
|
||||
}
|
||||
scr_dyn_arr_push(out, copy);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static void nh_copy_fulfilled(ScrPromise *dst, ScrPromise *src) {
|
||||
ScrDyn *payload = scr_promise_payload_is_dyn(src) ? (ScrDyn *)scr_promise_payload_ref(src)
|
||||
: scr_dyn_retain(scr_dyn_undefined());
|
||||
ScrDyn *copy = scr_node_island_copy_arrays(payload);
|
||||
scr_dyn_release(payload);
|
||||
if (copy == NULL) {
|
||||
scr_promise_reject_pending(dst);
|
||||
return;
|
||||
}
|
||||
scr_promise_fulfill_ref(dst, copy, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
|
||||
}
|
||||
|
||||
/* `await` of an island promise whose declared result holds arrays: the
|
||||
* fulfillment value's island arrays copied (inside the settlement, so no
|
||||
* reaction turn is added), rejections as they are. +1 promise<dyn>. */
|
||||
ScrPromise *scr_node_island_await_copy(ScrDyn *value) {
|
||||
ScrPromise *src = scr_ni_await(value);
|
||||
ScrPromise *dst = scr_promise_new();
|
||||
scr_promise_mark_handled(src);
|
||||
scr_promise_race_add(dst, src, nh_copy_fulfilled);
|
||||
scr_promise_release(src);
|
||||
return dst;
|
||||
}
|
||||
|
||||
uint32_t scr_ni_live_handles(void) {
|
||||
const ScrNodeBridge *b = scr_ni_bridge_loaded();
|
||||
if (b == NULL) return 0;
|
||||
|
||||
@@ -56,6 +56,65 @@ const ERROR_KINDS = [
|
||||
[URIError, 7],
|
||||
];
|
||||
|
||||
// structuredClone of native values (NI-5). A native record, class
|
||||
// instance or object is an object with interceptors in V8, which V8's
|
||||
// serializer cannot clone (DataCloneError). Node clones such values as the
|
||||
// plain data they are, so when a clone fails on a value holding native
|
||||
// values, it clones a plain copy instead: native values and the plain
|
||||
// objects and arrays around them are copied (own enumerable string keys,
|
||||
// shared references and cycles kept), everything else is cloned as is.
|
||||
const nodeStructuredClone = globalThis.structuredClone;
|
||||
function plainCopy(value, memo) {
|
||||
if (value === null || typeof value !== "object") return value;
|
||||
if (memo.has(value)) return memo.get(value);
|
||||
const native = nativeHost.isExport(value);
|
||||
if (Array.isArray(value)) {
|
||||
// Own enumerable keys, as structuredClone copies them: holes stay holes
|
||||
// and named properties come along.
|
||||
const out = new Array(value.length);
|
||||
memo.set(value, out);
|
||||
for (const key of Object.keys(value)) out[key] = plainCopy(value[key], memo);
|
||||
return out;
|
||||
}
|
||||
const proto = Object.getPrototypeOf(value);
|
||||
if (!native && proto !== Object.prototype && proto !== null) return value;
|
||||
const out = {};
|
||||
memo.set(value, out);
|
||||
for (const key of Object.keys(value)) out[key] = plainCopy(value[key], memo);
|
||||
return out;
|
||||
}
|
||||
function holdsNative(value, seen) {
|
||||
if (value === null || typeof value !== "object" || seen.has(value)) return false;
|
||||
seen.add(value);
|
||||
if (nativeHost.isExport(value)) return true;
|
||||
const proto = Object.getPrototypeOf(value);
|
||||
if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return false;
|
||||
for (const key of Object.keys(value)) if (holdsNative(value[key], seen)) return true;
|
||||
return false;
|
||||
}
|
||||
if (typeof nodeStructuredClone === "function") {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "structuredClone");
|
||||
const structuredClone = {
|
||||
structuredClone(value, options = undefined) {
|
||||
try {
|
||||
return Reflect.apply(nodeStructuredClone, globalThis, arguments);
|
||||
} catch (error) {
|
||||
if (!holdsNative(value, new Set())) throw error;
|
||||
return Reflect.apply(nodeStructuredClone, globalThis, [
|
||||
plainCopy(value, new Map()),
|
||||
options,
|
||||
]);
|
||||
}
|
||||
},
|
||||
}.structuredClone;
|
||||
Object.defineProperty(globalThis, "structuredClone", {
|
||||
configurable: descriptor === undefined || descriptor.configurable,
|
||||
enumerable: descriptor !== undefined && descriptor.enumerable,
|
||||
writable: true,
|
||||
value: structuredClone,
|
||||
});
|
||||
}
|
||||
|
||||
const requires = new Map();
|
||||
|
||||
function exportedFunction(file, specifier, name) {
|
||||
|
||||
@@ -62,6 +62,65 @@ static const char scr_node_island_host_js[] =
|
||||
" [URIError, 7],\012"
|
||||
"];\012"
|
||||
"\012"
|
||||
"// structuredClone of native values (NI-5). A native record, class\012"
|
||||
"// instance or object is an object with interceptors in V8, which V8's\012"
|
||||
"// serializer cannot clone (DataCloneError). Node clones such values as the\012"
|
||||
"// plain data they are, so when a clone fails on a value holding native\012"
|
||||
"// values, it clones a plain copy instead: native values and the plain\012"
|
||||
"// objects and arrays around them are copied (own enumerable string keys,\012"
|
||||
"// shared references and cycles kept), everything else is cloned as is.\012"
|
||||
"const nodeStructuredClone = globalThis.structuredClone;\012"
|
||||
"function plainCopy(value, memo) {\012"
|
||||
" if (value === null || typeof value !== \"object\") return value;\012"
|
||||
" if (memo.has(value)) return memo.get(value);\012"
|
||||
" const native = nativeHost.isExport(value);\012"
|
||||
" if (Array.isArray(value)) {\012"
|
||||
" // Own enumerable keys, as structuredClone copies them: holes stay holes\012"
|
||||
" // and named properties come along.\012"
|
||||
" const out = new Array(value.length);\012"
|
||||
" memo.set(value, out);\012"
|
||||
" for (const key of Object.keys(value)) out[key] = plainCopy(value[key], memo);\012"
|
||||
" return out;\012"
|
||||
" }\012"
|
||||
" const proto = Object.getPrototypeOf(value);\012"
|
||||
" if (!native && proto !== Object.prototype && proto !== null) return value;\012"
|
||||
" const out = {};\012"
|
||||
" memo.set(value, out);\012"
|
||||
" for (const key of Object.keys(value)) out[key] = plainCopy(value[key], memo);\012"
|
||||
" return out;\012"
|
||||
"}\012"
|
||||
"function holdsNative(value, seen) {\012"
|
||||
" if (value === null || typeof value !== \"object\" || seen.has(value)) return false;\012"
|
||||
" seen.add(value);\012"
|
||||
" if (nativeHost.isExport(value)) return true;\012"
|
||||
" const proto = Object.getPrototypeOf(value);\012"
|
||||
" if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return false;\012"
|
||||
" for (const key of Object.keys(value)) if (holdsNative(value[key], seen)) return true;\012"
|
||||
" return false;\012"
|
||||
"}\012"
|
||||
"if (typeof nodeStructuredClone === \"function\") {\012"
|
||||
" const descriptor = Object.getOwnPropertyDescriptor(globalThis, \"structuredClone\");\012"
|
||||
" const structuredClone = {\012"
|
||||
" structuredClone(value, options = undefined) {\012"
|
||||
" try {\012"
|
||||
" return Reflect.apply(nodeStructuredClone, globalThis, arguments);\012"
|
||||
" } catch (error) {\012"
|
||||
" if (!holdsNative(value, new Set())) throw error;\012"
|
||||
" return Reflect.apply(nodeStructuredClone, globalThis, [\012"
|
||||
" plainCopy(value, new Map()),\012"
|
||||
" options,\012"
|
||||
" ]);\012"
|
||||
" }\012"
|
||||
" },\012"
|
||||
" }.structuredClone;\012"
|
||||
" Object.defineProperty(globalThis, \"structuredClone\", {\012"
|
||||
" configurable: descriptor === undefined || descriptor.configurable,\012"
|
||||
" enumerable: descriptor !== undefined && descriptor.enumerable,\012"
|
||||
" writable: true,\012"
|
||||
" value: structuredClone,\012"
|
||||
" });\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"const requires = new Map();\012"
|
||||
"\012"
|
||||
"function exportedFunction(file, specifier, name) {\012"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// @requires-libnode
|
||||
// @order: race
|
||||
// X { T 0 { Z { L i-zlib } } } T 2 { B 80 L h-sync }
|
||||
import { callSync } from "@scriptc/node";
|
||||
import { readFile, writeSync } from "node:fs";
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
const { EventEmitter } = require("node:events");
|
||||
|
||||
class Bus extends EventEmitter {
|
||||
fire(n) {
|
||||
const delivered = this.emit("data", n, `item-${n}`);
|
||||
return delivered;
|
||||
}
|
||||
}
|
||||
|
||||
function each(items, fn) {
|
||||
const out = [];
|
||||
for (let i = 0; i < items.length; i++) out.push(fn(items[i], i));
|
||||
return out;
|
||||
}
|
||||
|
||||
function compute(n, callback) {
|
||||
setTimeout(() => {
|
||||
if (n < 0) callback(new RangeError(`negative: ${n}`));
|
||||
else callback(null, n * n);
|
||||
}, 5);
|
||||
}
|
||||
|
||||
function guarded(fn, value) {
|
||||
try {
|
||||
return `ok ${fn(value)}`;
|
||||
} catch (e) {
|
||||
return `caught ${e.name}: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function twice(fn) {
|
||||
return [fn(1), fn(2)];
|
||||
}
|
||||
|
||||
function same(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
module.exports = { Bus, each, compute, guarded, twice, same };
|
||||
@@ -0,0 +1,53 @@
|
||||
// @requires-libnode
|
||||
// Native closures passed into a Node island run as V8 functions: event
|
||||
// listeners, synchronous callbacks, Node-style async callbacks, and
|
||||
// exceptions crossing both ways.
|
||||
import { requireModule } from "@scriptc/node";
|
||||
|
||||
const { Bus, compute, each, guarded, same, twice } = requireModule("./lib.cjs");
|
||||
|
||||
const bus = new Bus();
|
||||
let total = 0;
|
||||
const listener = (n, label) => {
|
||||
total += n;
|
||||
console.log("listener", n, label);
|
||||
};
|
||||
bus.on("data", listener);
|
||||
bus.once("data", (n) => console.log("once", n));
|
||||
console.log("delivered", bus.fire(3));
|
||||
console.log("delivered", bus.fire(4));
|
||||
console.log("count", bus.listenerCount("data"), "total", total);
|
||||
bus.off("data", listener);
|
||||
console.log("delivered", bus.fire(5));
|
||||
|
||||
console.log(each([1, 2, 3], (x, i) => x * 10 + i).join(","));
|
||||
console.log(twice((n) => `n=${n}`).join(" "));
|
||||
|
||||
// A native throw propagates through island frames, where JS can catch it.
|
||||
console.log(guarded((v) => {
|
||||
if (v > 1) throw new RangeError(`too big: ${v}`);
|
||||
return v;
|
||||
}, 2));
|
||||
console.log(guarded((v) => v + 1, 1));
|
||||
|
||||
// An exception thrown natively and not caught by the island reaches the caller.
|
||||
bus.on("boom", () => {
|
||||
throw new TypeError("native boom");
|
||||
});
|
||||
try {
|
||||
bus.emit("boom");
|
||||
} catch (e) {
|
||||
console.log("caught", e instanceof TypeError, e.message);
|
||||
}
|
||||
|
||||
// The same closure crosses as the same function.
|
||||
const f = (n) => `${n}`;
|
||||
console.log("same", same(f, f));
|
||||
|
||||
compute(6, (err, value) => {
|
||||
console.log("compute", err, value);
|
||||
compute(-1, (err2, value2) => {
|
||||
console.log("compute", err2 instanceof RangeError, err2 && err2.message, value2);
|
||||
});
|
||||
});
|
||||
console.log("end of main");
|
||||
@@ -0,0 +1,35 @@
|
||||
"use strict";
|
||||
function log(s) { console.log(`island: ${s}`); }
|
||||
|
||||
async function later(label) {
|
||||
log(`later ${label} start`);
|
||||
await null;
|
||||
log(`later ${label} resumed`);
|
||||
return `${label}!`;
|
||||
}
|
||||
|
||||
function resolved(v) { return Promise.resolve(v); }
|
||||
|
||||
function rejectsWith(message) { return Promise.reject(new TypeError(message)); }
|
||||
|
||||
// Awaits a promise the caller passes in, interleaving its own microtasks.
|
||||
async function consume(p) {
|
||||
Promise.resolve().then(() => log("tick A"));
|
||||
const v = await p;
|
||||
log(`consumed ${v}`);
|
||||
return v * 2;
|
||||
}
|
||||
|
||||
function chain(p) {
|
||||
return p.then((v) => { log(`then ${v}`); return v + 1; });
|
||||
}
|
||||
|
||||
function isPromise(p) { return p instanceof Promise && typeof p.then === "function"; }
|
||||
|
||||
function timer(ms, v) { return new Promise((r) => setTimeout(() => r(v), ms)); }
|
||||
|
||||
async function catches(p) {
|
||||
try { await p; return "no error"; } catch (e) { return `caught ${e.name}: ${e.message}`; }
|
||||
}
|
||||
|
||||
module.exports = { later, resolved, rejectsWith, consume, chain, isPromise, timer, catches };
|
||||
@@ -0,0 +1,45 @@
|
||||
// @requires-libnode
|
||||
// Promises both ways on one microtask queue (scope-B NI-5): island
|
||||
// promises awaited natively and native promises awaited in the island
|
||||
// interleave with both sides' reactions exactly as in Node.
|
||||
import { requireModule } from "@scriptc/node";
|
||||
|
||||
const { catches, chain, consume, isPromise, later, rejectsWith, resolved, timer } =
|
||||
requireModule("./lib.cjs");
|
||||
|
||||
async function nativeValue(n) {
|
||||
await null;
|
||||
console.log("native resolving", n);
|
||||
return n;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("start");
|
||||
Promise.resolve().then(() => console.log("native tick 1"));
|
||||
const p = later("a");
|
||||
Promise.resolve().then(() => console.log("native tick 2"));
|
||||
console.log("awaited", await p);
|
||||
|
||||
console.log("resolved", await resolved(41));
|
||||
try {
|
||||
await rejectsWith("bad input");
|
||||
} catch (e) {
|
||||
console.log("rejected", e instanceof TypeError, e.message);
|
||||
}
|
||||
|
||||
// A native promise passed into the island is a real Promise there.
|
||||
const mine = nativeValue(21);
|
||||
console.log("isPromise", isPromise(mine));
|
||||
Promise.resolve().then(() => console.log("native tick 3"));
|
||||
console.log("consume", await consume(mine));
|
||||
console.log("chain", await chain(nativeValue(1)));
|
||||
console.log("catches", await catches(Promise.reject(new RangeError("native rejection"))));
|
||||
console.log("catches", await catches(nativeValue(2)));
|
||||
|
||||
const [x, y] = await Promise.all([timer(10, "slow"), timer(1, "fast")]);
|
||||
console.log("all", x, y);
|
||||
console.log("timer", await timer(2, "early"));
|
||||
console.log("end");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,26 @@
|
||||
"use strict";
|
||||
const kept = [];
|
||||
|
||||
function describe(o) {
|
||||
return Object.keys(o).map((k) => `${k}=${o[k]}`).join(" ");
|
||||
}
|
||||
|
||||
function bump(o, by) {
|
||||
o.count += by;
|
||||
o.label = `${o.label}+`;
|
||||
return o.count;
|
||||
}
|
||||
|
||||
function keep(o) { kept.push(o); return kept.length; }
|
||||
function keptAt(i) { return kept[i]; }
|
||||
function same(a, b) { return a === b; }
|
||||
function has(o, k) { return k in o; }
|
||||
function readThrough(o) { return `${o.greeting}, island (${Object.keys(o).join(",")})`; }
|
||||
function store(o) { return { inner: o }; }
|
||||
|
||||
// Reads a property from a Node timer: outside any call from the program.
|
||||
function readLater(o) {
|
||||
return new Promise((resolve) => setTimeout(() => resolve(`${o.label}/${o.count}`), 0));
|
||||
}
|
||||
|
||||
module.exports = { describe, bump, keep, keptAt, same, has, readThrough, store, readLater };
|
||||
@@ -0,0 +1,40 @@
|
||||
// @requires-libnode
|
||||
// Native objects and class instances passed into a Node island are live
|
||||
// objects there (scope-B NI-5): island reads and writes reach the native
|
||||
// storage, and the same native object comes back as itself.
|
||||
import { requireModule } from "@scriptc/node";
|
||||
|
||||
const { bump, describe, has, keep, keptAt, readLater, readThrough, same, store } =
|
||||
requireModule("./lib.cjs");
|
||||
|
||||
class Greeter {
|
||||
constructor(greeting) {
|
||||
this.greeting = greeting;
|
||||
}
|
||||
greet(who) {
|
||||
return `${this.greeting}, ${who}`;
|
||||
}
|
||||
}
|
||||
|
||||
const counter = { count: 1, label: "c" };
|
||||
console.log(describe(counter));
|
||||
console.log("bump", bump(counter, 2), bump(counter, 3));
|
||||
console.log("native sees", counter.count, counter.label);
|
||||
counter.count = 100;
|
||||
console.log(describe(counter));
|
||||
|
||||
console.log("kept", keep(counter));
|
||||
const back = keptAt(0);
|
||||
console.log("identity", back === counter, same(counter, counter), same(counter, { ...counter }));
|
||||
back.label = "via island";
|
||||
console.log("label", counter.label);
|
||||
|
||||
console.log("has", has(counter, "count"), has(counter, "missing"));
|
||||
|
||||
const g = new Greeter("Hello");
|
||||
console.log(readThrough(g));
|
||||
g.greeting = "Hi";
|
||||
console.log(readThrough(g));
|
||||
const boxed = store(g);
|
||||
console.log("boxed", boxed.inner === g, boxed.inner.greet("native"));
|
||||
console.log("later", await readLater(counter));
|
||||
@@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
const { EventEmitter } = require("node:events");
|
||||
|
||||
class Bus extends EventEmitter {
|
||||
fire(n) {
|
||||
const delivered = this.emit("data", n, `item-${n}`);
|
||||
return delivered;
|
||||
}
|
||||
}
|
||||
|
||||
function each(items, fn) {
|
||||
const out = [];
|
||||
for (let i = 0; i < items.length; i++) out.push(fn(items[i], i));
|
||||
return out;
|
||||
}
|
||||
|
||||
function compute(n, callback) {
|
||||
setTimeout(() => {
|
||||
if (n < 0) callback(new RangeError(`negative: ${n}`));
|
||||
else callback(null, n * n);
|
||||
}, 5);
|
||||
}
|
||||
|
||||
function guarded(fn, value) {
|
||||
try {
|
||||
return `ok ${fn(value)}`;
|
||||
} catch (e) {
|
||||
return `caught ${e.name}: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function twice(fn) {
|
||||
return [fn(1), fn(2)];
|
||||
}
|
||||
|
||||
function same(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
module.exports = { Bus, each, compute, guarded, twice, same };
|
||||
@@ -0,0 +1,54 @@
|
||||
// @requires-libnode
|
||||
// Typed native closures passed into a Node island run as V8 functions
|
||||
// whose arguments meet the declared parameter types (scope-B NI-5): event
|
||||
// listeners, synchronous callbacks, Node-style async callbacks, and
|
||||
// exceptions crossing both ways.
|
||||
import { requireModule } from "@scriptc/node";
|
||||
|
||||
const { Bus, compute, each, guarded, same, twice } = requireModule("./lib.cjs");
|
||||
|
||||
const bus = new Bus();
|
||||
let total = 0;
|
||||
const listener = (n: number, label: string): void => {
|
||||
total += n;
|
||||
console.log("listener", n, label);
|
||||
};
|
||||
bus.on("data", listener);
|
||||
bus.once("data", (n: number) => console.log("once", n));
|
||||
console.log("delivered", bus.fire(3));
|
||||
console.log("delivered", bus.fire(4));
|
||||
console.log("count", bus.listenerCount("data"), "total", total);
|
||||
bus.off("data", listener);
|
||||
console.log("delivered", bus.fire(5));
|
||||
|
||||
console.log(each([1, 2, 3], (x: number, i: number) => x * 10 + i).join(","));
|
||||
console.log(twice((n: number) => `n=${n}`).join(" "));
|
||||
|
||||
// A native throw propagates through island frames, where JS can catch it.
|
||||
console.log(guarded((v: number) => {
|
||||
if (v > 1) throw new RangeError(`too big: ${v}`);
|
||||
return v;
|
||||
}, 2));
|
||||
console.log(guarded((v: number) => v + 1, 1));
|
||||
|
||||
// An exception thrown natively and not caught by the island reaches the caller.
|
||||
bus.on("boom", () => {
|
||||
throw new TypeError("native boom");
|
||||
});
|
||||
try {
|
||||
bus.emit("boom");
|
||||
} catch (e) {
|
||||
console.log("caught", e instanceof TypeError, (e as Error).message);
|
||||
}
|
||||
|
||||
// The same closure crosses as the same function.
|
||||
const f = (n: number): string => `${n}`;
|
||||
console.log("same", same(f, f));
|
||||
|
||||
compute(6, (err: Error | null, value?: number) => {
|
||||
console.log("compute", err, value);
|
||||
compute(-1, (err2: Error | null, value2?: number) => {
|
||||
console.log("compute", err2 instanceof RangeError, err2?.message, value2);
|
||||
});
|
||||
});
|
||||
console.log("end of main");
|
||||
@@ -0,0 +1,51 @@
|
||||
// @requires-libnode
|
||||
// Native closures passed into a Node island run as V8 functions: event
|
||||
// listeners, synchronous callbacks, Node-style async callbacks, and
|
||||
// exceptions crossing both ways.
|
||||
import { Bus, compute, each, guarded, same, twice } from "emitter-pkg";
|
||||
|
||||
const bus = new Bus();
|
||||
let total = 0;
|
||||
const listener = (n: number, label: string): void => {
|
||||
total += n;
|
||||
console.log("listener", n, label);
|
||||
};
|
||||
bus.on("data", listener);
|
||||
bus.once("data", (n: number) => console.log("once", n));
|
||||
console.log("delivered", bus.fire(3));
|
||||
console.log("delivered", bus.fire(4));
|
||||
console.log("count", bus.listenerCount("data"), "total", total);
|
||||
bus.off("data", listener);
|
||||
console.log("delivered", bus.fire(5));
|
||||
|
||||
console.log(each(3, (x: number, i: number) => x * 10 + i));
|
||||
console.log(twice((n: number) => `n=${n}`));
|
||||
|
||||
// A native throw propagates through island frames, where JS can catch it.
|
||||
console.log(guarded((v: number) => {
|
||||
if (v > 1) throw new RangeError(`too big: ${v}`);
|
||||
return v;
|
||||
}, 2));
|
||||
console.log(guarded((v: number) => v + 1, 1));
|
||||
|
||||
// An exception thrown natively and not caught by the island reaches the caller.
|
||||
bus.on("boom", () => {
|
||||
throw new TypeError("native boom");
|
||||
});
|
||||
try {
|
||||
bus.emit("boom");
|
||||
} catch (e) {
|
||||
console.log("caught", e instanceof TypeError, (e as Error).message);
|
||||
}
|
||||
|
||||
// The same closure crosses as the same function.
|
||||
const f = (n: number): string => `${n}`;
|
||||
console.log("same", same(f, f));
|
||||
|
||||
compute(6, (err: Error | null, value?: number) => {
|
||||
console.log("compute", err, value);
|
||||
compute(-1, (err2: Error | null, value2?: number) => {
|
||||
console.log("compute", err2 instanceof RangeError, err2?.message, value2);
|
||||
});
|
||||
});
|
||||
console.log("end of main");
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
export declare class Bus extends EventEmitter {
|
||||
fire(n: number): boolean;
|
||||
}
|
||||
export declare function each(count: number, fn: (item: number, index: number) => number): string;
|
||||
export declare function compute(n: number, callback: (err: Error | null, value?: number) => void): void;
|
||||
export declare function guarded(fn: (value: number) => number, value: unknown): string;
|
||||
export declare function twice(fn: (n: number) => string): string;
|
||||
export declare function same(a: unknown, b: unknown): boolean;
|
||||
Generated
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
const { EventEmitter } = require("node:events");
|
||||
|
||||
class Bus extends EventEmitter {
|
||||
fire(n) {
|
||||
const delivered = this.emit("data", n, `item-${n}`);
|
||||
return delivered;
|
||||
}
|
||||
}
|
||||
|
||||
function each(count, fn) {
|
||||
const out = [];
|
||||
for (let i = 0; i < count; i++) out.push(fn(i + 1, i));
|
||||
return out.join(",");
|
||||
}
|
||||
|
||||
function compute(n, callback) {
|
||||
setTimeout(() => {
|
||||
if (n < 0) callback(new RangeError(`negative: ${n}`));
|
||||
else callback(null, n * n);
|
||||
}, 5);
|
||||
}
|
||||
|
||||
function guarded(fn, value) {
|
||||
try {
|
||||
return `ok ${fn(value)}`;
|
||||
} catch (e) {
|
||||
return `caught ${e.name}: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function twice(fn) {
|
||||
return [fn(1), fn(2)].join(" ");
|
||||
}
|
||||
|
||||
function same(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
module.exports = { Bus, each, compute, guarded, twice, same };
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "emitter-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "island-callbacks", "private": true, "type": "module", "scriptc": { "packages": { "emitter-pkg": "node" } } }
|
||||
@@ -0,0 +1,55 @@
|
||||
// @requires-libnode
|
||||
// Promises both ways on one microtask queue: island promises awaited
|
||||
// natively and native promises awaited in the island interleave with
|
||||
// both sides' reactions exactly as in Node.
|
||||
import {
|
||||
catches,
|
||||
chain,
|
||||
consume,
|
||||
isPromise,
|
||||
later,
|
||||
rejectsWith,
|
||||
resolved,
|
||||
samePromise,
|
||||
timer,
|
||||
} from "async-pkg";
|
||||
|
||||
async function nativeValue(n: number): Promise<number> {
|
||||
await null;
|
||||
console.log("native resolving", n);
|
||||
return n;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log("start");
|
||||
Promise.resolve().then(() => console.log("native tick 1"));
|
||||
const p = later("a");
|
||||
Promise.resolve().then(() => console.log("native tick 2"));
|
||||
console.log("awaited", await p);
|
||||
|
||||
console.log("resolved", await resolved(41));
|
||||
try {
|
||||
await rejectsWith("bad input");
|
||||
} catch (e) {
|
||||
console.log("rejected", e instanceof TypeError, (e as Error).message);
|
||||
}
|
||||
|
||||
// A native promise passed into the island is a real Promise there.
|
||||
const mine = nativeValue(21);
|
||||
console.log("isPromise", isPromise(mine));
|
||||
Promise.resolve().then(() => console.log("native tick 3"));
|
||||
console.log("consume", await consume(mine));
|
||||
console.log("chain", await chain(nativeValue(1)));
|
||||
console.log("catches", await catches(Promise.reject(new RangeError("native rejection"))));
|
||||
console.log("catches", await catches(nativeValue(2)));
|
||||
const twice = nativeValue(3);
|
||||
console.log("same promise", samePromise(twice, twice));
|
||||
await twice;
|
||||
|
||||
const [x, y] = await Promise.all([timer(10, "slow"), timer(1, "fast")]);
|
||||
console.log("all", x, y);
|
||||
console.log("race", await Promise.race([timer(20, "late"), timer(2, "early")]));
|
||||
console.log("end");
|
||||
}
|
||||
|
||||
main();
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export declare function later(label: string): Promise<string>;
|
||||
export declare function resolved<T>(v: T): Promise<T>;
|
||||
export declare function rejectsWith(message: string): Promise<void>;
|
||||
export declare function consume(p: Promise<number>): Promise<number>;
|
||||
export declare function chain(p: Promise<number>): Promise<number>;
|
||||
export declare function isPromise(p: unknown): boolean;
|
||||
export declare function timer<T>(ms: number, v: T): Promise<T>;
|
||||
export declare function catches(p: Promise<unknown>): Promise<string>;
|
||||
export declare function samePromise(a: Promise<number>, b: Promise<number>): boolean;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
function log(s) { console.log(`island: ${s}`); }
|
||||
|
||||
async function later(label) {
|
||||
log(`later ${label} start`);
|
||||
await null;
|
||||
log(`later ${label} resumed`);
|
||||
return `${label}!`;
|
||||
}
|
||||
|
||||
function resolved(v) { return Promise.resolve(v); }
|
||||
|
||||
function rejectsWith(message) { return Promise.reject(new TypeError(message)); }
|
||||
|
||||
// Awaits a promise the caller passes in, interleaving its own microtasks.
|
||||
async function consume(p) {
|
||||
Promise.resolve().then(() => log("tick A"));
|
||||
const v = await p;
|
||||
log(`consumed ${v}`);
|
||||
return v * 2;
|
||||
}
|
||||
|
||||
function chain(p) {
|
||||
return p.then((v) => { log(`then ${v}`); return v + 1; });
|
||||
}
|
||||
|
||||
function isPromise(p) { return p instanceof Promise && typeof p.then === "function"; }
|
||||
|
||||
function timer(ms, v) { return new Promise((r) => setTimeout(() => r(v), ms)); }
|
||||
|
||||
async function catches(p) {
|
||||
try { await p; return "no error"; } catch (e) { return `caught ${e.name}: ${e.message}`; }
|
||||
}
|
||||
|
||||
function samePromise(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
module.exports = { later, resolved, rejectsWith, consume, chain, isPromise, timer, catches, samePromise };
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "async-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "island-promises", "private": true, "type": "module", "scriptc": { "packages": { "async-pkg": "node" } } }
|
||||
@@ -0,0 +1,42 @@
|
||||
// @requires-libnode
|
||||
// Native records and class instances passed into a Node island are live
|
||||
// objects there: island reads and writes reach the native storage, and the
|
||||
// same native object comes back as itself.
|
||||
import { bump, describe, has, keep, keptAt, readThrough, same, store } from "obj-pkg";
|
||||
|
||||
interface Counter {
|
||||
count: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
class Greeter {
|
||||
greeting: string;
|
||||
constructor(greeting: string) {
|
||||
this.greeting = greeting;
|
||||
}
|
||||
greet(who: string): string {
|
||||
return `${this.greeting}, ${who}`;
|
||||
}
|
||||
}
|
||||
|
||||
const counter: Counter = { count: 1, label: "c" };
|
||||
console.log(describe(counter));
|
||||
console.log("bump", bump(counter, 2), bump(counter, 3));
|
||||
console.log("native sees", counter.count, counter.label);
|
||||
counter.count = 100;
|
||||
console.log(describe(counter));
|
||||
|
||||
console.log("kept", keep(counter));
|
||||
const back = keptAt<Counter>(0);
|
||||
console.log("identity", back === counter, same(counter, counter), same(counter, { count: counter.count, label: counter.label }));
|
||||
back.label = "via island";
|
||||
console.log("label", counter.label);
|
||||
|
||||
console.log("has", has(counter, "count"), has(counter, "missing"));
|
||||
|
||||
const g = new Greeter("Hello");
|
||||
console.log(readThrough(g));
|
||||
g.greeting = "Hi";
|
||||
console.log(readThrough(g));
|
||||
const boxed = store(g);
|
||||
console.log("boxed", boxed.inner === g, boxed.inner.greet("native"));
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
export declare function describe(o: object): string;
|
||||
export declare function bump(o: { count: number; label: string }, by: number): number;
|
||||
export declare function keep(o: object): number;
|
||||
export declare function keptAt<T>(i: number): T;
|
||||
export declare function same(a: unknown, b: unknown): boolean;
|
||||
export declare function has(o: object, k: string): boolean;
|
||||
export declare function readThrough(o: { greet(who: string): string }): string;
|
||||
export declare function store<T>(o: T): { inner: T };
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
const kept = [];
|
||||
|
||||
function describe(o) {
|
||||
return Object.keys(o).map((k) => `${k}=${o[k]}`).join(" ");
|
||||
}
|
||||
|
||||
function bump(o, by) {
|
||||
o.count += by;
|
||||
o.label = `${o.label}+`;
|
||||
return o.count;
|
||||
}
|
||||
|
||||
function keep(o) { kept.push(o); return kept.length; }
|
||||
function keptAt(i) { return kept[i]; }
|
||||
function same(a, b) { return a === b; }
|
||||
function has(o, k) { return k in o; }
|
||||
function readThrough(o) { return o.greet("island"); }
|
||||
function store(o) { return { inner: o }; }
|
||||
|
||||
module.exports = { describe, bump, keep, keptAt, same, has, readThrough, store };
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "obj-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "island-objects", "private": true, "type": "module", "scriptc": { "packages": { "obj-pkg": "node" } } }
|
||||
@@ -0,0 +1,42 @@
|
||||
// @requires-libnode
|
||||
// structuredClone in a Node island of native objects passed in clones
|
||||
// their data, as under Node; arrays of plain values the island returns,
|
||||
// directly or through a promise, arrive as arrays.
|
||||
import {
|
||||
cloneClaims,
|
||||
cloneFails,
|
||||
cloneNested,
|
||||
cloneSparse,
|
||||
listFiles,
|
||||
matrix,
|
||||
names,
|
||||
} from "data-pkg";
|
||||
|
||||
interface Claims {
|
||||
sub: string;
|
||||
aud: string;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
class Session {
|
||||
user = "ada";
|
||||
level = 2;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const claims: Claims = { sub: "user-1", aud: "api", exp: 1700000000 };
|
||||
console.log(cloneClaims(claims));
|
||||
console.log("native unchanged", claims.sub);
|
||||
console.log(cloneNested({ list: [1, 2], session: new Session() }));
|
||||
console.log(cloneNested([claims, claims]));
|
||||
console.log(cloneFails());
|
||||
console.log(cloneSparse({ kind: "native" }));
|
||||
const files = await listFiles("/tmp");
|
||||
for (const f of files) console.log("file", f);
|
||||
console.log("count", files.length, files.join(","));
|
||||
console.log("names", names().join("|"), names().length);
|
||||
const m = await matrix();
|
||||
console.log("matrix", m.length, m[0].join("+"), m[1][0]);
|
||||
}
|
||||
|
||||
main();
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export declare function cloneClaims(payload: object): string;
|
||||
export declare function cloneNested(value: unknown): string;
|
||||
export declare function cloneFails(): string;
|
||||
export declare function cloneSparse(native: object): string;
|
||||
export declare function listFiles(dir: string): Promise<string[]>;
|
||||
export declare function names(): string[];
|
||||
export declare function matrix(): Promise<number[][]>;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
"use strict";
|
||||
function cloneClaims(payload) {
|
||||
const copy = structuredClone(payload);
|
||||
copy.sub = "changed";
|
||||
return `${JSON.stringify(copy)} same=${copy === payload} proto=${Object.getPrototypeOf(copy) === Object.prototype}`;
|
||||
}
|
||||
function cloneNested(value) {
|
||||
const copy = structuredClone({ value, when: new Date(0) });
|
||||
return `${JSON.stringify(copy.value)} date=${copy.when instanceof Date}`;
|
||||
}
|
||||
function cloneFails() {
|
||||
try {
|
||||
structuredClone({ f() {} });
|
||||
return "cloned";
|
||||
} catch (e) {
|
||||
return `${e.name}`;
|
||||
}
|
||||
}
|
||||
async function listFiles(dir) {
|
||||
await null;
|
||||
return [`${dir}/a.txt`, `${dir}/b.txt`];
|
||||
}
|
||||
function names() {
|
||||
return ["x", "y", "z"];
|
||||
}
|
||||
async function matrix() {
|
||||
return [[1, 2], [3]];
|
||||
}
|
||||
function cloneSparse(native) {
|
||||
const list = [1, , 3];
|
||||
list.extra = "x";
|
||||
const copy = structuredClone({ native, list });
|
||||
return `sparse ${copy.list.length} ${1 in copy.list} ${copy.list.extra} ${copy.native.kind}`;
|
||||
}
|
||||
|
||||
module.exports = { cloneClaims, cloneNested, cloneFails, cloneSparse, listFiles, names, matrix };
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "data-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "island-clone-and-arrays", "private": true, "type": "module", "scriptc": { "packages": { "data-pkg": "node" } } }
|
||||
@@ -0,0 +1,30 @@
|
||||
// @requires-libnode
|
||||
// A native class instance passed into a Node island shows the island only
|
||||
// its public members: its #private methods, and the names the compiler
|
||||
// gives accessors and symbol-keyed methods, read as undefined, as in Node.
|
||||
import { probe, shadow } from "private-pkg";
|
||||
|
||||
class Account {
|
||||
#balance = 10;
|
||||
owner: string;
|
||||
constructor(owner: string) {
|
||||
this.owner = owner;
|
||||
}
|
||||
#secret(): string {
|
||||
return `secret of ${this.owner}: ${this.#balance}`;
|
||||
}
|
||||
get label(): string {
|
||||
return `account ${this.owner}`;
|
||||
}
|
||||
describe(): string {
|
||||
return `${this.owner} (${this.#secret().length})`;
|
||||
}
|
||||
*[Symbol.iterator](): Generator<string> {
|
||||
yield this.owner;
|
||||
}
|
||||
}
|
||||
|
||||
const a = new Account("ada");
|
||||
console.log(probe(a));
|
||||
console.log(a.describe(), [...a].join(","));
|
||||
console.log(shadow(new Account("bob")));
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export declare function probe(o: object): string;
|
||||
export declare function shadow(o: object): string;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
|
||||
// Reads a native instance's #private method and internal member names by
|
||||
// string: an island sees only its public members.
|
||||
function probe(o) {
|
||||
const out = [typeof o["#secret"], "#secret" in o];
|
||||
// A property with the private method's spelling, holding undefined.
|
||||
o["#secret"] = undefined;
|
||||
const m = o["#secret"];
|
||||
out.push(typeof m, "#secret" in o);
|
||||
if (typeof m === "function") out.push(m.call(o));
|
||||
out.push(typeof o["get:label"], typeof o["sym:iterator"], typeof o.describe);
|
||||
return out.join(" ");
|
||||
}
|
||||
|
||||
// An own property holding undefined hides the prototype method.
|
||||
function shadow(o) {
|
||||
o.describe = undefined;
|
||||
return `${typeof o.describe} ${Object.keys(o).includes("describe")}`;
|
||||
}
|
||||
|
||||
module.exports = { probe, shadow };
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "private-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "island-private-methods", "private": true, "type": "module", "scriptc": { "packages": { "private-pkg": "node" } } }
|
||||
@@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
const shared = { count: 0, items: [] };
|
||||
|
||||
class Point {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
norm() {
|
||||
return Math.hypot(this.x, this.y);
|
||||
}
|
||||
}
|
||||
|
||||
exports.shared = shared;
|
||||
exports.Point = Point;
|
||||
exports.getShared = () => shared;
|
||||
exports.bump = () => ++shared.count;
|
||||
exports.read = (o, k) => o[k];
|
||||
exports.same = (a, b) => a === b;
|
||||
exports.makeCounter = () => {
|
||||
let n = 0;
|
||||
return { inc: () => ++n, get value() { return n; } };
|
||||
};
|
||||
exports.add = (a, b) => a + b;
|
||||
exports.numbers = () => [-0, NaN, Infinity, 2 ** 53 + 2, 10n ** 30n, -7n];
|
||||
exports.isNegZero = (x) => Object.is(x, -0);
|
||||
exports.strings = () => ["plain", "wörld ☃ 🌍", "lone \ud800 high", "\udc00 lone low"];
|
||||
exports.codeUnits = (s) => Array.from(s, (c) => c.charCodeAt(0).toString(16)).join(" ");
|
||||
exports.describe = (v) => `${typeof v} ${Object.prototype.toString.call(v)}`;
|
||||
exports.keysOf = (o) => Object.keys(o).join(",");
|
||||
@@ -0,0 +1,13 @@
|
||||
// A native class extending a class owned by a Node island is SC8011:
|
||||
// native subclasses of island classes are not supported yet (NI-5b).
|
||||
import { requireModule } from "@scriptc/node";
|
||||
|
||||
interface Lib {
|
||||
Point: new (x: number, y: number) => { x: number; y: number; norm(): number };
|
||||
}
|
||||
const { Point } = requireModule<Lib>("./lib.cjs");
|
||||
|
||||
class Point3 extends Point {
|
||||
z = 0;
|
||||
}
|
||||
console.log(new Point3(1, 2).norm());
|
||||
@@ -0,0 +1,15 @@
|
||||
node-island-subclass/main.ts:10:22 - error SC8011: a native class cannot extend a class owned by a Node island yet (native subclasses of island classes)
|
||||
|
||||
9 |
|
||||
10 | class Point3 extends Point {
|
||||
| ^~~~~
|
||||
11 | z = 0;
|
||||
|
||||
hint: keep the subclass in Node: move it into the island module, or mark its module as a Node island; a registry shim of the package also lifts this
|
||||
|
||||
node-island-subclass/main.ts:13:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet
|
||||
|
||||
12 | }
|
||||
13 | console.log(new Point3(1, 2).norm());
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
14 |
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2000, "san": 30000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2500, "san": 3300 }
|
||||
@@ -13,7 +13,9 @@
|
||||
* A program whose head says `// @order: race` races a timer against I/O
|
||||
* whose duration differs between the sides (scriptc's own fs callbacks
|
||||
* complete in one turn where Node's readFile takes several; module loading
|
||||
* on the first call into Node takes longer than in a Node process), or two
|
||||
* on the first call into Node takes longer than in a Node process; a job on
|
||||
* libuv's thread pool, such as zlib's, can finish before or after a 2 ms
|
||||
* timer depending on the host), or two
|
||||
* equal timers armed on either side of an island call, which a millisecond
|
||||
* boundary between them (likely in a sanitized build) puts in different
|
||||
* loop turns, so Node permits orders it rarely produces in M runs. Its
|
||||
|
||||
@@ -308,7 +308,7 @@ describe.skipIf(skip)("Node islands in-process", () => {
|
||||
});
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toBe(
|
||||
`scriptc: the libnode pack at ${dir} has an incompatible bridge (ABI 999; this program needs 5)\n`,
|
||||
`scriptc: the libnode pack at ${dir} has an incompatible bridge (ABI 999; this program needs 6)\n`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user