macOS binds libnode's own calls to v8::base::Stack inside libnode, so the
bridge's definitions cannot give V8 Node's stack as they do on Linux; V8
took the main thread's stack start and its conservative scan faulted in
the gap above Node's stack. While Node starts on its stack, the main
thread's pthread record now names that stack, which V8 reads once and
keeps.
macOS's makecontext clears the whole stack it is given, so it gets only
the top slice of Node's stack (no 64 MiB pass per context).
CI: dev-pack.json pins mirrored darwin-arm64 and darwin-x64 libnode
builds of the pinned Node 24.22.0 from the libnode-packs workflow, the
dev-pack action serves either platform, and the macOS lanes provision it,
so island tests fail rather than skip on macOS CI. The arm64 LLVM job
runs the island harness; the Intel job runs it and the island corpus.
bridge-smoke loads the .dylib bridge on macOS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
structuredClone in an island of a value holding native records, class
instances or objects (interceptor objects V8's serializer cannot clone)
clones a plain copy of that data, as Node clones the objects themselves.
An island array of primitives or strings (or arrays of those) returned
directly or through a promise crosses as a copy, which the checked cast
then meets, instead of failing the cast as an object handle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain object a program passes into an island is an object-template
instance, which inherits from its implicit constructor's prototype object;
island code that checks for plain objects (jose's isObject:
Object.getPrototypeOf(o) === Object.prototype) rejected it. The instance now
inherits Object.prototype directly. Corpus 6826 checks it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scope-B milestone NI-3. When a program loads Node in-process, its pending
ticks, promise jobs, timers and immediates move onto Node's queues in
order (timers keep their deadlines relative to the moment Node began
loading; the work of a phase the switch interrupted finishes first) and
every later one is scheduled there, so the jobs of both sides interleave
as in Node. The runtime's I/O hooks are watched from Node's loop, and its
liveness is re-read before each poll phase, after each check phase and
after every microtask checkpoint. Node's loop runs on Node's stack, and
V8's limit follows whichever stack an entry comes from.
Island signal and exit listeners join the runtime's registries at their
position, so the runtime keeps owning every signal disposition; Node's own
'exit' emission is left to the runtime's exit sequence, and the exit code
island code leaves in process.exitCode becomes the program's. Bridge ABI 3.
Ordering programs (tests/corpus-node-islands-order, harness
node-island-order.test.ts), corpus programs 6804-6811 and the
node-island-process harness pin the behavior against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calls into Node now run on one dedicated 64 MB stack with a guard page,
reached by a context switch, and the bridge sets V8's stack limit on it
(bridge ABI 2). Before, a call from an async fiber (512 KB stacks without a
guard page) failed to bootstrap Node, which the dev-mode LLVM differential
showed. The runtime's stack guard follows the switch, and the switch
carries ASan's fiber annotations.
The Node island tests load Node from a libnode pack. Until the NI-13 packs
are published, .github/actions/libnode-dev-pack assembles one for the
Linux x64 test lanes (plain and sanitized): a pinned, checksum-verified
prebuilt libnode of the repository's Node release (24.22.0) and Node's
official headers (packages/libnode-common/dev-pack.json), with the bridge
built against them by build-pack.mjs. The pack is cached by its inputs and
the bridge source, so only a cache miss downloads and builds; the normal
build path never does.
tests/harness/libnode.ts makes a missing pack a failure in those lanes:
island tests skip only off Linux x64 CI without a configured pack. The
bridge smoke test (packages/libnode-common/test/bridge-smoke.c) loads the
pack the way scr_node_island.c does and makes synchronous, throwing and
asynchronous calls through the bridge with the runtime's bootstrap.
IR, LLVM and object outputs of programs that call into Node no longer need
a pack at build time; only executables record one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scope-B milestone NI-1, runtime side. Programs whose IR has a
`nodeIsland.*` operation link scr_node_island.c and the SCR_NODE_ISLANDS
build of scr_async.c; every other program links neither, byte for byte.
- scr_node_island.c dlopens the bridge of a libnode pack on the first call
(RTLD_NOW | RTLD_GLOBAL), checks its bridge ABI and that it links exactly
the Node version recorded at build time, starts one Node environment and
runs the embedded bootstrap (scr_node_island_host.cjs). A run that never
calls into Node never maps libnode. The pack directory is SCRIPTC_LIBNODE
at run time, else the recorded path; a `$ORIGIN/` path resolves against
the executable's directory (the beside packaging, and the hook for
shipping Node with the program).
- scr_node_bridge.h is the C ABI between the runtime and the bridge; the
runtime includes no Node or V8 header. The bridge
(packages/libnode-common/bridge/scr_node_bridge.cc) initializes Node once
with no default signal handling, no debug-signal handler or inspector,
no stdio reinitialization and one platform worker, creates the
environment from the built-in snapshot, routes Node's process exit to
the runtime's exit sequence, promotes libnode to the global scope for
native addons, and runs calls in a CallbackScope with a TryCatch.
build-pack.mjs assembles a pack (libnode, the bridge, manifest.json) from
a Node --shared build and the matching headers.
- The SCR_NODE_ISLANDS build of scr_async.c adds a loop slot through which
the runtime's loop drives Node's until NI-3 hosts the runtime's queues on
Node's libuv: it keeps the program alive while Node has ref'd work,
sleeps on Node's backend fd up to its next timer, and runs one
non-blocking turn per loop turn.
- The `nodeIsland` runtime feature (rule, link features, unit table, pack
matrix row and scr_async.c variant), the nodeIsland.call/callSync
libCalls, the install call in main, and the build-time record of the
pack (node-islands/libnode.ts: --libnode=<pack>|beside or
SCRIPTC_LIBNODE, SC8005 when none is usable, Node >=24.13.1).
- The zero-cost gate and the bench nm test require that no good-code
executable defines a Node island symbol.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>