Files
scriptc/packages/runtime/src/scr_node_bridge.h
T
2026-10-24 16:49:25 -05:00

340 lines
17 KiB
C

/* scriptc runtime — the C ABI between the runtime and the libnode bridge
* (packages/libnode-common/bridge/scr_node_bridge.cc). The runtime never
* includes a Node or V8 header: it loads the bridge library of a libnode
* pack with dlopen on first use and talks to it only through this table.
* The bridge links libnode and pins the Node ABI; the runtime pins only
* SCR_NODE_BRIDGE_ABI, which changes with any change to these structs.
*
* Conventions: strings are UTF-8 and borrowed unless noted. Call outcomes
* are "R<json>" (returned; "R" alone is undefined) or "T<json>" (threw;
* the JSON is {"error":{kind,message,code?}}, {"value":...} or
* {"undefined":true}, see scr_node_island_host.cjs). Every entry point
* runs on the thread that called start, and every host function is called
* on it. */
#ifndef SCR_NODE_BRIDGE_H
#define SCR_NODE_BRIDGE_H
#include <stddef.h>
#include <stdint.h>
#define SCR_NODE_BRIDGE_ABI 9u
/* The symbol a bridge library exports: `const ScrNodeBridge *(void)`. */
#define SCR_NODE_BRIDGE_ENTRY "scr_node_bridge_v1"
/* Operations on Map and Set exports (host->export_op, NI-5b). */
enum {
SCR_NODE_COLLECTION_GET = 0,
SCR_NODE_COLLECTION_SET = 1, /* Map#set, Set#add */
SCR_NODE_COLLECTION_HAS = 2,
SCR_NODE_COLLECTION_DELETE = 3,
SCR_NODE_COLLECTION_CLEAR = 4,
SCR_NODE_COLLECTION_SIZE = 5,
SCR_NODE_COLLECTION_ENTRIES = 6,
};
/* Kinds of runtime jobs the bridge schedules on Node's queues (`run`). */
enum {
SCR_NODE_JOB_MICROTASK = 1, /* V8's microtask queue */
SCR_NODE_JOB_TICK = 2, /* Node's process.nextTick queue */
SCR_NODE_JOB_TIMER = 3, /* a Node timer (setTimeout / setInterval) */
SCR_NODE_JOB_IMMEDIATE = 4, /* Node's check phase (setImmediate) */
};
/* Process events that island code listens to (`listen`): the listener
* joins the runtime's own registry at its registration position, so one
* registry orders the listeners of both sides and libuv never owns a
* signal. */
enum {
SCR_NODE_EVENT_SIGNAL = 1,
SCR_NODE_EVENT_EXIT = 2,
};
/* The runtime's I/O as Node's loop sees it (io_state): the fds to watch
* for readability, whether runtime work keeps the loop alive, whether
* runtime work is ready without any fd (the loop must not block), and a
* bound in milliseconds on the sleep for work no fd announces (-1: none).
* The bridge sets `phase` (input): when it asks, so that liveness is
* current wherever Node's loop next decides it. */
enum {
SCR_NODE_IO_PREPARE = 0, /* before a poll phase */
SCR_NODE_IO_CHECK = 1, /* after a check phase: the end of a turn */
SCR_NODE_IO_JOBS = 2, /* after a microtask checkpoint (any callback) */
};
#define SCR_NODE_IO_MAX_FDS 16
typedef struct ScrNodeIoState {
int phase;
int fds[SCR_NODE_IO_MAX_FDS];
int nfds;
int alive;
int ready;
int cap_ms;
} ScrNodeIoState;
/* ── object handles (scope-B NI-4) ────────────────────────────────────────
* A value crossing between the runtime and V8. Primitives, strings and
* BigInt are copies; objects are handles: ids into the bridge's import
* table of v8::Global references, one id per V8 object while the runtime
* holds it (the bridge answers the same id for the same object), released
* with `drop`. Typed arrays and Buffers coming out of V8 also carry their
* backing store's memory (BYTES), which the runtime aliases.
*
* Out-values the bridge writes hold bridge-owned string data valid until
* the next bridge entry; the runtime copies it at once. In-values are
* borrowed for the call. */
enum {
SCR_NV_UNDEFINED = 0,
SCR_NV_NULL = 1,
SCR_NV_BOOL = 2,
SCR_NV_NUMBER = 3,
SCR_NV_STRING = 4, /* WTF-8: lone surrogates as their 3-byte forms */
SCR_NV_BIGINT = 5, /* str: decimal text with an optional leading '-' */
SCR_NV_HANDLE = 6, /* handle: an import-table id; flags: SCR_NVF_* */
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, /* 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 {
SCR_NVF_FUNCTION = 1,
SCR_NVF_SYMBOL = 2,
SCR_NVF_ERROR = 4,
SCR_NVF_ARRAY = 8,
SCR_NVF_PROMISE = 16,
SCR_NVF_CONSTRUCTOR = 32,
SCR_NVF_BUFFER = 64, /* BYTES: a Buffer; DATAVIEW: a DataView */
SCR_NVF_DATAVIEW = 128,
SCR_NVF_MAP = 256, /* EXPORT: a native Map (NI-5b) */
SCR_NVF_SET = 512, /* EXPORT: a native Set (NI-5b) */
};
/* BYTES element kinds: ScrBytesElem's numbering (scr_bytes.h). */
typedef struct ScrNodeValue {
uint32_t tag;
uint32_t flags;
union {
int boolean;
double num;
struct {
const char *data;
size_t len;
} str;
uint32_t handle;
struct {
uint8_t *data; /* first element */
size_t len; /* elements */
uint32_t elem; /* ScrBytesElem */
uint32_t handle; /* out of V8: the view's handle (keeps the store alive) */
void *owner; /* into V8: the runtime ScrBytes root that owns data */
} bytes;
struct {
uint32_t kind; /* scr_error.h SCR_ERR_* (intrinsic class) */
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;
/* An island error's own properties, read by `error_info` (bridge-owned
* strings, valid until the next entry; code NULL when it has no string
* code). kind is the nearest intrinsic class (SCR_ERR_*). */
typedef struct ScrNodeErrorInfo {
uint32_t kind;
const char *name, *message, *stack, *code;
size_t name_len, message_len, stack_len, code_len;
/* Node's arrow header for the throw site ("<file>:<line>\n<source
* line>\n<caret>"), recorded when the error was thrown out of island
* code; NULL when none. */
const char *arrow;
size_t arrow_len;
} ScrNodeErrorInfo;
/* What the runtime provides to the bridge. */
typedef struct ScrNodeBridgeHost {
/* An asynchronous call settled with this outcome. */
void (*settle)(uint32_t id, const char *outcome, size_t len);
/* Node ends the process (process.exit, an uncaught exception): the
* runtime runs its exit sequence with this code and never returns. */
void (*exit)(int code);
/* A runtime job came due on Node's queues (a SCR_NODE_JOB_* kind and the
* token the runtime passed when it scheduled it). Runs on the stack of
* Node's own dispatch, so Node drains ticks and microtasks after it
* exactly as after any of its callbacks. */
void (*run)(uint32_t kind, uint32_t token);
/* The runtime's I/O hooks: its state before each poll phase and after
* each check phase, and one dispatch pass (when a watched fd is readable
* or `ready` was set). */
void (*io_state)(ScrNodeIoState *state);
void (*io_dispatch)(void);
/* V8 finished a microtask checkpoint: the runtime reports the unhandled
* rejections of its own promises, as Node does at the same point. */
void (*microtasks_done)(void);
/* Island code added (add=1, once=1 for once listeners) or removed
* (add=0) a listener for a process event (SCR_NODE_EVENT_*, `name` its
* event name). `listener` identifies it in later `emit` calls. */
void (*listen)(uint32_t event, const char *name, uint32_t listener, int add, int once);
/* The lowest stack address JavaScript may use on the stack the runtime
* is calling in from (its main thread's or an async fiber's), for V8's
* stack limit; 0 leaves V8's own. */
uintptr_t (*stack_limit)(void);
/* The start (highest address) of the stack the caller runs on when the
* runtime provides it (Node's own stack), 0 for a thread's own stack.
* V8 scans the stack conservatively from the current position up to the
* start it records for the thread, so it must not record the thread's
* when JavaScript runs on the runtime's stack. */
uintptr_t (*stack_start)(void);
/* NI-4: a thenable the runtime awaits (`then`) settled; `value` is the
* fulfillment or (rejected=1) the reason. Runs as a V8 reaction job. */
void (*settle_value)(uint32_t token, int rejected, const ScrNodeValue *value);
/* NI-4: V8 freed a backing store over runtime bytes (`owner` of a BYTES
* 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);
/* `value instanceof <export id>` for a native class representative
* (its Symbol.hasInstance): 1 or 0. */
int (*export_instance_of)(uint32_t id, const ScrNodeValue *value);
/* ── shared modules (NI-6): modules the program compiled that island
* code imports resolve, through the loader hooks of the bootstrap, to
* generated modules over the live native exports. ── */
/* The table, as SCR_NV_STRING JSON: [{"path","exports":[{"name","kind"}]}]
* (index = module id, export index = slot). */
void (*shared_table)(ScrNodeValue *out);
/* The current value of a shared export. mode 0: read; 1: read and bind
* (the generated module holds it: later native changes are pushed with
* bridge->shared_push); 2: bind late (the binding is in its temporal
* dead zone: it is pushed once the native declaration ran). 0: *out is
* the value; 1: threw (*out thrown); 3: in its temporal dead zone. */
int (*shared_get)(uint32_t mod, uint32_t slot, int mode, ScrNodeValue *out);
/* The exports in their temporal dead zone now, as SCR_NV_STRING JSON
* [[mod, slot], ...]. */
void (*shared_tdz)(ScrNodeValue *out);
/* A Map or Set export's operation (SCR_NODE_COLLECTION_*): 0 returned
* (*out), 1 threw. ENTRIES answers an array export: [key, value] pairs
* of a Map, the values of a Set. */
int (*export_op)(uint32_t id, uint32_t op, const ScrNodeValue *args, size_t argc,
ScrNodeValue *out);
} ScrNodeBridgeHost;
typedef struct ScrNodeBridge {
uint32_t abi; /* SCR_NODE_BRIDGE_ABI */
const char *node_version; /* the libnode it links: "24.22.0" */
uint32_t module_version; /* its NODE_MODULE_VERSION */
/* Initializes Node once per process and runs `bootstrap` as the main
* script of one environment. argv[0] names the executable. Every entry
* runs on one stack the runtime provides; `stack_limit` is the lowest
* address JavaScript may use on it (V8's stack limit), 0 for V8's
* default. Returns NULL, or a message the runtime frees with `release`. */
char *(*start)(const ScrNodeBridgeHost *host, const char *bootstrap, const char *argv0,
uintptr_t stack_limit);
/* Calls the bootstrap's dispatcher in a callback scope (ticks and
* microtasks drain when it closes, as after any libuv callback). A
* synchronous call answers its outcome (+1, freed with `release`, length
* in *len); an asynchronous one answers NULL and settles through the
* host, possibly before it returns. */
char *(*call)(uint32_t id, int sync, const char *file, const char *specifier,
const char *name, const char *args, size_t args_len, size_t *len);
void (*release)(void *memory);
/* The hosted loop (scope-B NI-3). Once Node is loaded, the runtime's
* queues live on Node's: `host_loop` starts the integration (the I/O
* handles that watch the runtime's fds, the microtask-checkpoint hook)
* and holds a callback scope open until the next `checkpoint`, so work
* already pending in the runtime and work the current runtime callback
* goes on to queue drain together, at the end of that callback. */
void (*host_loop)(void);
/* Schedules runtime jobs (SCR_NODE_JOB_*). Timers take Node's delay
* coercion; `repeat` > 0 re-arms after each run (setInterval), and
* `delay` may differ from `repeat` for intervals migrated mid-period.
* Handles are nonzero and name the Node Timeout or Immediate until the
* runtime clears them; a cleared or fired handle is never reused. */
void (*microtask)(uint32_t token);
void (*tick)(uint32_t token);
uint32_t (*timer)(double delay, double repeat, int ref, uint32_t token);
void (*timer_clear)(uint32_t handle);
void (*timer_ref)(uint32_t handle, int ref);
void (*timer_refresh)(uint32_t handle);
uint32_t (*immediate)(int ref, uint32_t token);
void (*immediate_clear)(uint32_t handle);
void (*immediate_ref)(uint32_t handle, int ref);
/* The checkpoint after a macrotask: closes the scope `host_loop` held
* (or opens and closes one), which runs Node's tick queue and V8's
* microtasks to exhaustion. `microtasks_first` runs the microtasks once
* before the ticks: the end of an ES module main body. */
void (*checkpoint)(int microtasks_first);
/* Runs Node's event loop until nothing keeps it alive (beforeExit
* included). Returns when the program ends normally, with the exit code
* island code assigned to process.exitCode (0: none). `continue_turn`:
* the switch interrupted a timers or poll phase, so the first iteration
* starts at the poll phase instead of the timers phase. */
int (*run_loop)(int continue_turn);
/* Calls island listener `listener` of a process event: a signal (`name`,
* signal number `arg`) or exit (code `arg`). */
void (*emit)(uint32_t event, uint32_t listener, const char *name, int arg);
/* Tears Node down at process exit. */
void (*stop)(void);
/* ── object handles (NI-4). Each operation answers 0 (returned: *out is
* the result) or 1 (threw: *out is the thrown value). ── */
/* Loads a module by absolute path: require(path) (esm=0, the exports),
* or import(path) (esm=1, a promise of the namespace). */
int (*load)(const char *path, int esm, ScrNodeValue *out);
/* Property access on handle h; key goes through ToPropertyKey. */
int (*get)(uint32_t h, const ScrNodeValue *key, ScrNodeValue *out);
int (*set)(uint32_t h, const ScrNodeValue *key, const ScrNodeValue *value, ScrNodeValue *out);
int (*has)(uint32_t h, const ScrNodeValue *key, ScrNodeValue *out); /* out: BOOL */
int (*del)(uint32_t h, const ScrNodeValue *key, ScrNodeValue *out); /* out: BOOL */
/* fn(...args) with receiver this_ (NULL: undefined). */
int (*invoke)(uint32_t fn, const ScrNodeValue *this_, const ScrNodeValue *args, size_t argc,
ScrNodeValue *out);
/* h[key](...args) with h as the receiver. */
int (*invoke_method)(uint32_t h, const ScrNodeValue *key, const ScrNodeValue *args, size_t argc,
ScrNodeValue *out);
int (*construct)(uint32_t h, const ScrNodeValue *args, size_t argc, ScrNodeValue *out);
int (*instance_of)(const ScrNodeValue *value, uint32_t ctor, ScrNodeValue *out); /* BOOL */
/* Own keys of h: mode 0 Object.keys, mode 1 Reflect.ownKeys. out: a
* handle to a fresh array, which the runtime reads and drops. */
int (*own_keys)(uint32_t h, int mode, ScrNodeValue *out);
/* Promise.resolve(value).then(...): settles through host->settle_value
* with `token`. Answers 1 with *out thrown if `then` could not attach. */
int (*then)(const ScrNodeValue *value, uint32_t token, ScrNodeValue *out);
/* The own name, message, stack and string code of an error handle. */
void (*error_info)(uint32_t h, ScrNodeErrorInfo *out);
/* Releases handle h's Global. */
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);
/* NI-6: the native value of shared export (mod, slot) changed: the
* generated module's binding takes `value`. */
void (*shared_push)(uint32_t mod, uint32_t slot, const ScrNodeValue *value);
} ScrNodeBridge;
#endif /* SCR_NODE_BRIDGE_H */