// The libnode bridge: the C ABI of packages/runtime/src/scr_node_bridge.h // over Node's C++ embedder API. It is built against one exact Node release // (build-pack.mjs) and ships in that release's libnode pack next to // libnode; the scriptc runtime dlopens it on the first call into Node and // never includes a Node or V8 header itself. // // One environment per process, created from Node's built-in startup // snapshot. Node keeps its hands off process-wide state that scriptc owns: // no default signal handling, no debug-signal handler or inspector, no // stdio reinitialization, one platform worker thread. Node's process exit // (process.exit, an uncaught exception) goes to the runtime's exit // sequence. // // One loop (scope-B NI-3). Once the runtime calls host_loop, its queues // live on Node's: its microtasks are V8 microtasks, its ticks, timers and // immediates are Node's (scheduled through the bootstrap's helpers), and // its I/O is watched from Node's libuv loop (uv_poll handles over the fds // the runtime reports, one prepare handle whose ref is the runtime's // liveness, an idle handle while runtime work is ready without any fd). // Calls into JavaScript therefore do not drain queues themselves: the // runtime's checkpoints do, through `checkpoint`, and Node's own after // every callback it dispatches. // // One heap, as far as cycles go (scope-B NI-8). The import table holds V8 // objects as v8::TracedReferences that Node's CppHeap traces, and every // export representative carries a CppHeap object that traces what its // native value reaches across the boundary; see "cross-heap cycle // collection" below. #include "scr_node_bridge.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { using v8::Context; using v8::Function; using v8::FunctionCallbackInfo; using v8::Global; using v8::HandleScope; using v8::Integer; using v8::Isolate; using v8::Local; using v8::Locker; using v8::NewStringType; using v8::Object; using v8::String; using v8::TryCatch; using v8::Value; const ScrNodeBridgeHost *g_host = nullptr; void DropAllHandles(); void DropAllExports(); void FlushReleasedExports(); bool StartCollector(v8::Isolate *isolate); void StopCollector(v8::Isolate *isolate); void Touch(uint32_t export_id); std::unique_ptr g_platform; node::EmbedderSnapshotData::Pointer g_snapshot; std::unique_ptr g_setup; Locker *g_locker = nullptr; Global g_call; // The bootstrap's hosted-loop helpers (scr_node_island_host.cjs). Global g_set_timer, g_clear_timer, g_ref_timer, g_refresh_timer; Global g_set_immediate, g_clear_immediate, g_ref_immediate; Global g_next_tick, g_emit; Global g_load; // the bootstrap's module loader (NI-4) Global g_shared_push; // the loader hooks' binding updates (NI-6) void SharedTable(const FunctionCallbackInfo &info); void SharedGet(const FunctionCallbackInfo &info); void SharedTdz(const FunctionCallbackInfo &info); char *Copy(const std::string &text, size_t *len) { char *out = static_cast(std::malloc(text.size() + 1)); if (out == nullptr) std::abort(); std::memcpy(out, text.data(), text.size()); out[text.size()] = '\0'; if (len != nullptr) *len = text.size(); return out; } // V8's stack limit follows the stack the runtime calls in from: its main // thread's, or an async fiber's (a few hundred KiB, mapped elsewhere), so // JavaScript entered from a fiber neither trips a false stack overflow nor // overruns the fiber. Restored when the entry returns. uintptr_t g_stack_limit = 0; struct StackLimit { uintptr_t previous; explicit StackLimit(Isolate *isolate) : previous(g_stack_limit) { uintptr_t want = g_host->stack_limit(); if (want != 0 && want != g_stack_limit) { isolate->SetStackLimit(want); g_stack_limit = want; } } ~StackLimit() { if (previous != 0 && previous != g_stack_limit) { g_setup->isolate()->SetStackLimit(previous); g_stack_limit = previous; } } }; // The start of the runtime's stack when the caller runs on it (see the // v8::base::Stack definitions at the end of this file), else 0. uintptr_t HostStackStart() { return g_host != nullptr && g_host->stack_start != nullptr ? g_host->stack_start() : 0; } Local Utf8(Isolate *isolate, const char *text, size_t len) { return String::NewFromUtf8(isolate, text, NewStringType::kNormal, static_cast(len)) .ToLocalChecked(); } // A string longer than V8 can hold is empty instead of a fatal error. v8::MaybeLocal MaybeUtf8(Isolate *isolate, const char *text, size_t len) { if (len > static_cast(String::kMaxLength)) return {}; return String::NewFromUtf8(isolate, text, NewStringType::kNormal, static_cast(len)); } Local Utf8(Isolate *isolate, const char *text) { return Utf8(isolate, text, std::strlen(text)); } std::string ToStdString(Isolate *isolate, Local value) { String::Utf8Value text(isolate, value); return *text != nullptr ? std::string(*text, text.length()) : std::string(); } // The outcome of an exception the dispatcher itself did not catch (a stack // overflow, for example): an Error copy with its message. // Reading the message runs user code (a getter, a toString), which may throw // again; the copy then has an empty message. std::string ThrownOutcome(Isolate *isolate, Local context, Local exception) { TryCatch inner(isolate); Local message = exception; if (exception->IsObject()) { Local own; if (exception.As()->Get(context, Utf8(isolate, "message")).ToLocal(&own)) message = own; } Local text; Local quoted; if (!message->ToString(context).ToLocal(&text) || !v8::JSON::Stringify(context, text).ToLocal("ed)) return "T{\"error\":{\"kind\":0,\"message\":\"\"}}"; return "T{\"error\":{\"kind\":0,\"message\":" + ToStdString(isolate, quoted) + "}}"; } // The bootstrap's settle(id, outcome) for asynchronous calls. void Settle(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); uint32_t id = info[0]->Uint32Value(isolate->GetCurrentContext()).FromMaybe(0); std::string outcome = ToStdString(isolate, info[1]); g_host->settle(id, outcome.data(), outcome.size()); } uint32_t Uint32Arg(const FunctionCallbackInfo &info, int i) { return info[i]->Uint32Value(info.GetIsolate()->GetCurrentContext()).FromMaybe(0); } // run(kind, token): a runtime job Node's queues dispatched. void Run(const FunctionCallbackInfo &info) { g_host->run(Uint32Arg(info, 0), Uint32Arg(info, 1)); } // listen(event, name, listener, add, once): island process listeners. void Listen(const FunctionCallbackInfo &info) { std::string name = ToStdString(info.GetIsolate(), info[1]); g_host->listen(Uint32Arg(info, 0), name.c_str(), Uint32Arg(info, 2), Uint32Arg(info, 3) != 0, Uint32Arg(info, 4) != 0); } void SetMethod(Local context, Local target, const char *name, v8::FunctionCallback callback) { target ->Set(context, Utf8(context->GetIsolate(), name), Function::New(context, callback).ToLocalChecked()) .Check(); } bool TakeFunction(Isolate *isolate, Local context, Local from, const char *name, Global *to) { Local value; if (!from->Get(context, Utf8(isolate, name)).ToLocal(&value) || !value->IsFunction()) return false; to->Reset(isolate, value.As()); return true; } // OpenSSL registers OPENSSL_cleanup with atexit on its first // initialization. In a scriptc program that loaded Node, threads Node owns // can still be inside OpenSSL when the process's exit handlers run: the // threads node:tls starts to parse the root certificates (long with // NODE_USE_SYSTEM_CA=1; RunLoop waits for them, but an exit that does not // end the loop does not) are joined only by Node's per-process teardown, // which cannot run here (see Stop). OPENSSL_cleanup // freed OpenSSL's locks and method stores under them (SIGSEGV/SIGABRT at // exit in up to half the runs, NI-7c). Initializing OpenSSL first with // OPENSSL_INIT_NO_ATEXIT keeps that handler from being registered: OpenSSL's // process-lifetime state is left to process exit, like the rest of Node's // per-process state. Packs whose libnode does not export the symbol keep // OpenSSL's default. void KeepOpenSSLAtExit() { using InitCrypto = int (*)(uint64_t, const void *); constexpr uint64_t kNoAtExit = 0x00080000L; // OPENSSL_INIT_NO_ATEXIT (OpenSSL 3) auto init = reinterpret_cast(dlsym(RTLD_DEFAULT, "OPENSSL_init_crypto")); if (init != nullptr) init(kNoAtExit, nullptr); } uint32_t ExportIdOf(Isolate *isolate, Local context, Local 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 &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; g_host = host; // Native addons resolve napi_* and node::* in the global scope: promote // libnode, a dependency of this library, to RTLD_GLOBAL. Dl_info info; if (dladdr(reinterpret_cast(&napi_create_function), &info) && info.dli_fname) dlopen(info.dli_fname, RTLD_NOW | RTLD_NOLOAD | RTLD_GLOBAL); KeepOpenSSLAtExit(); std::vector args = {argv0}; // The program is not a node process: NODE_OPTIONS from its environment // does not apply, and Node leaves the process's resource limits and code // pages as they are. auto result = node::InitializeOncePerProcess( args, {node::ProcessInitializationFlags::kNoInitializeV8, node::ProcessInitializationFlags::kNoInitializeNodeV8Platform, node::ProcessInitializationFlags::kNoStdioInitialization, node::ProcessInitializationFlags::kNoDefaultSignalHandling, node::ProcessInitializationFlags::kNoPrintHelpOrVersionOutput, node::ProcessInitializationFlags::kDisableNodeOptionsEnv, node::ProcessInitializationFlags::kNoAdjustResourceLimits, node::ProcessInitializationFlags::kNoUseLargePages}); if (result->early_return() || result->exit_code() != 0) { std::string message = "Node initialization failed"; for (const std::string &error : result->errors()) message += ": " + error; return Copy(message, nullptr); } g_platform = node::MultiIsolatePlatform::Create(1); v8::V8::InitializePlatform(g_platform.get()); v8::V8::Initialize(); g_snapshot = node::EmbedderSnapshotData::BuiltinSnapshotData(); // Modules resolve inside the program's own module tree, never from // NODE_PATH or the user's global module directories. auto flags = static_cast( node::EnvironmentFlags::kDefaultFlags | node::EnvironmentFlags::kNoCreateInspector | node::EnvironmentFlags::kNoStartDebugSignalHandler | node::EnvironmentFlags::kNoGlobalSearchPaths); std::vector errors; g_setup = g_snapshot ? node::CommonEnvironmentSetup::CreateFromSnapshot( g_platform.get(), &errors, g_snapshot.get(), result->args(), result->exec_args(), flags) : node::CommonEnvironmentSetup::Create(g_platform.get(), &errors, result->args(), result->exec_args(), flags); if (!g_setup) { std::string message = "cannot create the Node environment"; for (const std::string &error : errors) message += ": " + error; return Copy(message, nullptr); } Isolate *isolate = g_setup->isolate(); g_locker = new Locker(isolate); isolate->Enter(); // The runtime calls into Node on a dedicated stack of its own // (scr_node_island.c), not the thread's: V8's limit follows it, and // follows any other stack an entry comes from (StackLimit). if (stack_limit != 0) isolate->SetStackLimit(stack_limit); g_stack_limit = stack_limit; node::SetProcessExitHandler(g_setup->env(), [](node::Environment *, int code) { g_host->exit(code); }); if (!StartCollector(isolate)) return Copy("this libnode's isolate has no CppHeap", nullptr); HandleScope handles(isolate); Local context = g_setup->context(); Context::Scope context_scope(context); Local global = context->Global(); Local native = Object::New(isolate); SetMethod(context, native, "settle", Settle); SetMethod(context, native, "run", Run); SetMethod(context, native, "listen", Listen); SetMethod(context, native, "sharedTable", SharedTable); SetMethod(context, native, "sharedGet", SharedGet); SetMethod(context, native, "sharedTdz", SharedTdz); 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()) { std::string message = "the Node island bootstrap failed"; if (try_catch.HasCaught()) { Local stack; Local exception = try_catch.Exception(); if (exception->IsObject() && exception.As()->Get(context, Utf8(isolate, "stack")).ToLocal(&stack) && stack->IsString()) exception = stack; message += ": " + ToStdString(isolate, exception); } else if (try_catch.HasTerminated()) { message += ": execution terminated"; } return Copy(message, nullptr); } Local dispatcher; if (!global->Get(context, Utf8(isolate, "__scriptcNodeIsland")).ToLocal(&dispatcher) || !dispatcher->IsObject()) return Copy("the Node island bootstrap left no dispatcher", nullptr); Local d = dispatcher.As(); if (!TakeFunction(isolate, context, d, "call", &g_call) || !TakeFunction(isolate, context, d, "setTimer", &g_set_timer) || !TakeFunction(isolate, context, d, "clearTimer", &g_clear_timer) || !TakeFunction(isolate, context, d, "refTimer", &g_ref_timer) || !TakeFunction(isolate, context, d, "refreshTimer", &g_refresh_timer) || !TakeFunction(isolate, context, d, "setImmediate", &g_set_immediate) || !TakeFunction(isolate, context, d, "clearImmediate", &g_clear_immediate) || !TakeFunction(isolate, context, d, "refImmediate", &g_ref_immediate) || !TakeFunction(isolate, context, d, "nextTick", &g_next_tick) || !TakeFunction(isolate, context, d, "emit", &g_emit) || !TakeFunction(isolate, context, d, "load", &g_load) || !TakeFunction(isolate, context, d, "sharedPush", &g_shared_push)) return Copy("the Node island bootstrap left an incomplete dispatcher", nullptr); global->Delete(context, Utf8(isolate, "__scriptcNodeIsland")).FromMaybe(false); global->Delete(context, Utf8(isolate, "__scriptcNodeIslandHost")).FromMaybe(false); return nullptr; } 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) { Isolate *isolate = g_setup->isolate(); StackLimit limit(isolate); HandleScope handles(isolate); Local context = g_setup->context(); Context::Scope context_scope(context); std::string outcome; { // No callback scope: the runtime's checkpoint drains what the call // queued, at the end of the runtime callback that made it (NI-3). TryCatch try_catch(isolate); Local args_text; if (!MaybeUtf8(isolate, args, args_len).ToLocal(&args_text)) { // The argument JSON is longer than V8's string limit: the RangeError // V8 throws for such a string. outcome = "T{\"error\":{\"kind\":2,\"message\":\"Invalid string length\"}}"; if (!sync) { g_host->settle(id, outcome.data(), outcome.size()); return nullptr; } return Copy(outcome, len); } Local argv[] = {Integer::NewFromUnsigned(isolate, id), v8::Boolean::New(isolate, sync != 0), Utf8(isolate, file), Utf8(isolate, specifier), Utf8(isolate, name), args_text}; Local result; if (g_call.Get(isolate) ->Call(context, v8::Undefined(isolate), sizeof argv / sizeof argv[0], argv) .ToLocal(&result)) { if (result->IsString()) outcome = ToStdString(isolate, result); } else if (try_catch.HasCaught() && !try_catch.HasTerminated()) { outcome = ThrownOutcome(isolate, context, try_catch.Exception()); if (!sync) { g_host->settle(id, outcome.data(), outcome.size()); outcome.clear(); } } } if (!sync) return nullptr; if (outcome.empty()) outcome = "T{\"undefined\":true}"; return Copy(outcome, len); } void Release(void *memory) { std::free(memory); } // ── the hosted loop ────────────────────────────────────────────────────── struct Enter { Isolate *isolate; StackLimit limit; HandleScope handles; Local context; Context::Scope scope; Enter() : isolate(g_setup->isolate()), limit(isolate), handles(isolate), context(g_setup->context()), scope(context) {} }; // Calls a bootstrap helper; helpers do not throw, a failure answers empty. Local Helper(Enter &e, Global &fn, int argc, Local *argv) { TryCatch try_catch(e.isolate); Local result; if (!fn.Get(e.isolate)->Call(e.context, v8::Undefined(e.isolate), argc, argv).ToLocal(&result)) return Local(); return result; } Local Num(Enter &e, double value) { return v8::Number::New(e.isolate, value); } uint32_t HelperHandle(Enter &e, Local result) { if (result.IsEmpty()) return 0; return result->Uint32Value(e.context).FromMaybe(0); } struct PollEntry { uv_poll_t handle; int fd; bool seen; bool active; }; uv_prepare_t g_prepare; uv_check_t g_check; uv_idle_t g_idle; uv_timer_t g_cap; bool g_hosted = false; bool g_trace = false; // SCRIPTC_NODE_ISLANDS_TRACE=loop: the I/O state on stderr bool g_dispatched = false; // the runtime's stations ran this iteration std::unordered_map g_polls; void Dispatch() { if (g_dispatched) return; g_dispatched = true; g_host->io_dispatch(); } void SyncIo(int phase); void OnPoll(uv_poll_t *, int, int) { Dispatch(); SyncIo(SCR_NODE_IO_JOBS); } void OnIdle(uv_idle_t *) { Dispatch(); SyncIo(SCR_NODE_IO_JOBS); } void OnCap(uv_timer_t *) { Dispatch(); SyncIo(SCR_NODE_IO_JOBS); } // Before each poll phase, while the runtime has live work: one pass over // its dispatch stations, as the runtime's own loop makes one per turn. // Some of their work no fd announces (a server's deferred 'listening' or // 'close', a socket's queued writes), so waiting for an fd could block the // loop forever with that work pending. void OnPrepare(uv_prepare_t *handle) { if (uv_has_ref(reinterpret_cast(handle))) Dispatch(); SyncIo(SCR_NODE_IO_PREPARE); } // The end of an iteration: the next one may dispatch again (from its idle // or prepare phase, else from its poll phase). void OnCheck(uv_check_t *) { g_dispatched = false; SyncIo(SCR_NODE_IO_CHECK); } void ClosePoll(uv_handle_t *handle) { delete reinterpret_cast(handle->data); } // Mirrors the runtime's I/O state onto the loop: one unref'd poll handle // per fd, the prepare handle's ref as the runtime's liveness, the idle // handle while work is ready, the cap timer for work no fd announces. A // poll handle whose fd leaves the set is stopped, not closed, and started // again when the fd returns (the worker pool's wake pipe comes and goes // with every job): libuv cannot watch an fd with a second handle while the // first is still closing. void SyncIo(int phase) { if (!g_hosted) return; ScrNodeIoState state; state.phase = phase; g_host->io_state(&state); if (g_trace) { std::fprintf(stderr, "[node-islands] %s alive=%d ready=%d cap=%d fds=", phase == SCR_NODE_IO_CHECK ? "check" : phase == SCR_NODE_IO_JOBS ? "jobs" : "prepare", state.alive, state.ready, state.cap_ms); for (int i = 0; i < state.nfds; i++) std::fprintf(stderr, "%s%d", i ? "," : "", state.fds[i]); std::fprintf(stderr, "\n"); } uv_loop_t *loop = g_setup->event_loop(); for (auto &entry : g_polls) entry.second->seen = false; for (int i = 0; i < state.nfds; i++) { int fd = state.fds[i]; auto found = g_polls.find(fd); PollEntry *entry; if (found != g_polls.end()) { entry = found->second; } else { entry = new PollEntry(); entry->fd = fd; entry->active = false; if (uv_poll_init(loop, &entry->handle, fd) != 0) { delete entry; continue; } entry->handle.data = entry; uv_unref(reinterpret_cast(&entry->handle)); g_polls.emplace(fd, entry); } entry->seen = true; if (!entry->active && uv_poll_start(&entry->handle, UV_READABLE, OnPoll) == 0) entry->active = true; } for (auto &it : g_polls) { if (it.second->seen || !it.second->active) continue; uv_poll_stop(&it.second->handle); it.second->active = false; } if (state.alive) uv_ref(reinterpret_cast(&g_prepare)); else uv_unref(reinterpret_cast(&g_prepare)); if (state.ready) uv_idle_start(&g_idle, OnIdle); else uv_idle_stop(&g_idle); if (state.cap_ms >= 0) { if (!uv_is_active(reinterpret_cast(&g_cap))) uv_timer_start(&g_cap, OnCap, static_cast(state.cap_ms), 0); } else { uv_timer_stop(&g_cap); } } // After every microtask checkpoint, which ends every callback Node // dispatches (closing-phase ones included): the runtime's rejections, and // 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); } void HostLoop() { if (g_hosted) return; uv_loop_t *loop = g_setup->event_loop(); uv_prepare_init(loop, &g_prepare); uv_prepare_start(&g_prepare, OnPrepare); uv_check_init(loop, &g_check); uv_check_start(&g_check, OnCheck); uv_unref(reinterpret_cast(&g_check)); uv_idle_init(loop, &g_idle); uv_unref(reinterpret_cast(&g_idle)); uv_timer_init(loop, &g_cap); uv_unref(reinterpret_cast(&g_cap)); g_setup->isolate()->AddMicrotasksCompletedCallback(OnMicrotasksDone, nullptr); const char *trace = std::getenv("SCRIPTC_NODE_ISLANDS_TRACE"); g_trace = trace != nullptr && std::strcmp(trace, "loop") == 0; g_hosted = true; SyncIo(SCR_NODE_IO_JOBS); } void UnhostLoop() { if (!g_hosted) return; g_hosted = false; g_setup->isolate()->RemoveMicrotasksCompletedCallback(OnMicrotasksDone, nullptr); for (auto &entry : g_polls) { uv_poll_stop(&entry.second->handle); uv_close(reinterpret_cast(&entry.second->handle), ClosePoll); } g_polls.clear(); uv_close(reinterpret_cast(&g_prepare), nullptr); uv_close(reinterpret_cast(&g_check), nullptr); uv_close(reinterpret_cast(&g_idle), nullptr); uv_close(reinterpret_cast(&g_cap), nullptr); uv_run(g_setup->event_loop(), UV_RUN_NOWAIT); } void RunMicrotask(void *data) { g_host->run(SCR_NODE_JOB_MICROTASK, static_cast(reinterpret_cast(data))); } void Microtask(uint32_t token) { g_setup->isolate()->EnqueueMicrotask(RunMicrotask, reinterpret_cast(static_cast(token))); } void Tick(uint32_t token) { Enter e; Local argv[] = {Num(e, token)}; Helper(e, g_next_tick, 1, argv); } uint32_t Timer(double delay, double repeat, int ref, uint32_t token) { Enter e; Local argv[] = {Num(e, delay), Num(e, repeat), v8::Boolean::New(e.isolate, ref != 0), Num(e, token)}; return HelperHandle(e, Helper(e, g_set_timer, 4, argv)); } void TimerClear(uint32_t handle) { Enter e; Local argv[] = {Num(e, handle)}; Helper(e, g_clear_timer, 1, argv); } void TimerRef(uint32_t handle, int ref) { Enter e; Local argv[] = {Num(e, handle), v8::Boolean::New(e.isolate, ref != 0)}; Helper(e, g_ref_timer, 2, argv); } void TimerRefresh(uint32_t handle) { Enter e; Local argv[] = {Num(e, handle)}; Helper(e, g_refresh_timer, 1, argv); } uint32_t Immediate(int ref, uint32_t token) { Enter e; Local argv[] = {v8::Boolean::New(e.isolate, ref != 0), Num(e, token)}; return HelperHandle(e, Helper(e, g_set_immediate, 2, argv)); } void ImmediateClear(uint32_t handle) { Enter e; Local argv[] = {Num(e, handle)}; Helper(e, g_clear_immediate, 1, argv); } void ImmediateRef(uint32_t handle, int ref) { Enter e; Local argv[] = {Num(e, handle), v8::Boolean::New(e.isolate, ref != 0)}; Helper(e, g_ref_immediate, 2, argv); } // The checkpoint after a runtime macrotask: an empty callback scope, whose // close runs Node's tick queue and V8's microtasks (InternalCallbackScope:: // Close) unless Node's own dispatch is on the stack, which drains after. void Checkpoint(int microtasks_first) { Enter e; if (microtasks_first) e.context->GetMicrotaskQueue()->PerformCheckpoint(e.isolate); node::CallbackScope scope(e.isolate, Object::New(e.isolate), node::async_context{0, 0}); } // Loading node:tls starts threads that parse the root certificates into // function-local statics (bundled, NODE_EXTRA_CA_CERTS, and the system // store with NODE_USE_SYSTEM_CA=1, which takes long). Only Node's // CleanupCachedRootCertificates joins them. Once exit() begins, the // statics' destructors (registered when the threads initialize them, so // they run before any handler the program registered earlier) free the // vectors under a thread that is still parsing, and OpenSSL's state goes // away under it: heap corruption and SIGSEGV/SIGABRT at exit (NI-7c). So // when the loop is done, before the program returns, the bridge waits for // them: building a root store reads the same statics under the same // conditions, and initializing a static blocks until the thread that runs // it is done. A store built earlier means the statics were initialized // then. Programs that never loaded node:tls do nothing here. void AwaitCertificateLoading(Enter &e) { TryCatch try_catch(e.isolate); static const char kSource[] = "(() => { try { if (process.moduleLoadList.includes('NativeModule tls'))" " process.getBuiltinModule('node:tls').createSecureContext(); } catch {} })()"; Local script; if (v8::Script::Compile(e.context, Utf8(e.isolate, kSource)).ToLocal(&script)) (void)script->Run(e.context); } int RunLoop(int continue_turn) { Enter e; // The rest of the turn the switch interrupted: libuv's UV_RUN_ONCE skips // the leading timers phase and runs poll, check, closing, then timers. if (continue_turn) { uv_run(g_setup->event_loop(), UV_RUN_ONCE); g_platform->DrainTasks(e.isolate); } int code = node::SpinEventLoop(g_setup->env()).FromMaybe(0); AwaitCertificateLoading(e); return code; } // An island listener of a process event: through MakeCallback, so a throw // is an uncaught exception as in Node's own emit. void Emit(uint32_t event, uint32_t listener, const char *name, int arg) { Enter e; Local argv[] = {Num(e, event), Num(e, listener), Utf8(e.isolate, name), Num(e, arg)}; node::MakeCallback(e.isolate, e.context->Global(), g_emit.Get(e.isolate), 4, argv, node::async_context{0, 0}); } void Stop() { if (!g_setup) return; UnhostLoop(); Isolate *isolate = g_setup->isolate(); node::Stop(g_setup->env()); g_call.Reset(); g_load.Reset(); StopCollector(isolate); DropAllHandles(); DropAllExports(); for (Global *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, &g_shared_push}) fn->Reset(); isolate->Exit(); delete g_locker; g_locker = nullptr; g_setup.reset(); // As Node's own exit does (DefaultProcessExitHandlerInternal): join // libuv's thread pool, whose work (pbkdf2, key generation, ...) may be // inside OpenSSL, before the process's exit handlers clean OpenSSL up. uv_library_shutdown(); v8::V8::Dispose(); v8::V8::DisposePlatform(); // Node's per-process teardown is left to process exit: Stop runs from an // exit handler registered when Node started, after the destructors of // the certificate statics (registered later) already ran, so its // certificate cleanup would free them a second time. } // ── object handles (scope-B NI-4) ─────────────────────────────────────── // The import table: V8 values the runtime holds, one entry per value and // one id per value while held (found again through the value's identity // hash), so a V8 object crossing twice is the same runtime handle. A typed // array's slot also keeps its backing store, so memory the runtime aliases // stays valid even if JavaScript detaches the buffer. // // An entry holds its value as a v8::TracedReference, which only the // cross-heap collector's tracing marks (NI-8): as a root while native // roots may use it, otherwise through the representative of an export // whose native value reaches it. An entry nothing traced in a major GC is // reclaimed by V8 and becomes dead: the runtime no longer reaches it and // only drops its id, and any other use is an internal error. struct Slot { v8::TracedReference value; std::shared_ptr store; int hash = 0; bool live = false; // the runtime holds the id bool dead = false; // V8 reclaimed the value: the reference is never touched again bool rooted = true; // traced as a root under an exact analysis std::atomic traced{0}; // the GC cycle that last traced it }; // The GC lock guards what the collector's tracing reads, from V8's marking // threads: the slot vector, slot references and flags, the export GC // table, reach lists and the collector's mode. The main thread never holds // it while V8 may allocate. std::mutex g_gc_lock; std::vector> g_slots(1); // index = id; id 0 is never used std::vector g_free_ids; std::unordered_multimap g_ids; uint32_t g_live_handles = 0; // Strings of out-values: valid until the next outermost entry. std::deque g_scratch; int g_entry_depth = 0; void NoteFresh(uint32_t entry); int IdentityHash(Local v) { if (v->IsObject()) return v.As()->GetIdentityHash(); if (v->IsSymbol()) return v.As()->GetIdentityHash(); return 0; } uint32_t Import(Isolate *isolate, Local v, std::shared_ptr store = nullptr) { int hash = IdentityHash(v); auto range = g_ids.equal_range(hash); for (auto it = range.first; it != range.second; ++it) { Slot &slot = *g_slots[it->second]; if (slot.value.Get(isolate)->StrictEquals(v)) return it->second; } uint32_t id; if (!g_free_ids.empty()) { id = g_free_ids.back(); g_free_ids.pop_back(); } else { id = static_cast(g_slots.size()); } { std::lock_guard lock(g_gc_lock); if (id == g_slots.size()) g_slots.emplace_back(); if (!g_slots[id]) g_slots[id] = std::make_unique(); Slot &slot = *g_slots[id]; // An assigning store: a reference made while V8 marks is marked. slot.value.Reset(isolate, v); slot.store = std::move(store); slot.hash = hash; slot.live = true; slot.dead = false; slot.rooted = true; } NoteFresh(id); g_ids.emplace(hash, id); g_live_handles++; return id; } [[noreturn]] void CollectedInUse(uint32_t id) { std::fprintf(stderr, "scriptc: internal error: island handle %u was used after V8 collected its " "object (cross-heap collector)\n", id); std::fflush(stderr); std::abort(); } Local HandleValue(Isolate *isolate, uint32_t id) { if (id == 0 || id >= g_slots.size() || !g_slots[id] || !g_slots[id]->live) return v8::Undefined(isolate); Slot &slot = *g_slots[id]; if (slot.dead) CollectedInUse(id); return slot.value.Get(isolate); } void ForgetIdentity(uint32_t id, int hash) { auto range = g_ids.equal_range(hash); for (auto it = range.first; it != range.second; ++it) { if (it->second == id) { g_ids.erase(it); break; } } } void Drop(uint32_t id) { if (id == 0 || id >= g_slots.size() || !g_slots[id] || !g_slots[id]->live) return; Slot &slot = *g_slots[id]; if (!slot.dead) ForgetIdentity(id, slot.hash); std::shared_ptr store; { std::lock_guard lock(g_gc_lock); // V8 already reclaimed a dead slot's node: forget it without touching it. if (slot.dead) new (&slot.value) v8::TracedReference(); else slot.value.Reset(); store = std::move(slot.store); slot.live = false; slot.dead = false; } g_free_ids.push_back(id); g_live_handles--; } void DropAllHandles() { for (uint32_t id = 1; id < g_slots.size(); id++) Drop(id); } uint32_t LiveHandles() { return g_live_handles; } // ── cross-heap cycle collection (scope-B NI-8) ───────────────────────── // V8's marker decides what lives on both sides of the boundary. Before a // major GC marks (its prologue), the runtime analyzes the native subgraph // under the exported values (gc_analyze, scr_node_island_gc.c): which // exports its own roots hold, which imports they hold, and what every // other export's native value reaches (imports and other exports). Then: // - Roots, a CppHeap object held by a Persistent, traces the imports // native roots hold and the representatives of the exports they hold; // - every representative carries a CppHeap object (Reach, wrapped by a // carrier object the representative holds under a private symbol) that // traces what its export reaches; // - Fresh traces references made while V8 marks (new imports, new // exports, and exports JavaScript hands to the runtime or calls into // while V8 marks, which become roots), retraced through a Steele // barrier. // V8 reclaims every traced reference this left untraced: the imports // become dead (the runtime holds them only from garbage it frees next), // and representatives V8 did not mark die, which releases their exports // (export_release) so the runtime's cycle collector frees the native half. // Without an analysis (the runtime declined, or V8 skipped the prologue) // the collector is conservative: Roots traces everything. // // Node creates the isolate with its own CppHeap (NewIsolate: owned by V8) // and supports addons wrapping CppHeap objects with v8::Object::Wrap; the // bridge uses that heap. V8 13.6's EmbedderRootsHandler only resets // droppable references in minor GCs, and attaching a second CppHeap is no // longer possible, so this is the one way to put embedder edges into // V8's major-GC marking. struct ExportGc { v8::TracedReference rep; // the representative, traced only by the collector bool rooted = true; std::vector reach; // what the export reaches (SCR_NODE_GC_EXPORT marks exports) std::atomic traced{0}; uint32_t gen = 0; // tells this representative's Reach from older ones uint8_t tenure = 0; // consecutive analyses that found it held natively }; std::unordered_map> g_export_gc; uint32_t g_export_gen = 0; enum { kConservative = 0, kExact = 1 }; int g_gc_mode = kConservative; uint32_t g_epoch = 1; // the GC cycle; a reference traced in it carries it std::vector g_fresh; // entries to trace with Fresh bool g_fresh_pending = false; // Fresh will be (re)traced and sees g_fresh bool g_gc_trace = false; // SCRIPTC_NODE_ISLANDS_TRACE contains "gc" bool g_gc_off = false; // SCRIPTC_NODE_ISLANDS_GC=off: every handle a root ScrNodeGcStats g_gc_stats; struct { size_t exports, rooted, imports, free, reach, visited, touched; uint64_t ns; bool analyzed; bool incremental; // analyzed when incremental marking started } g_gc_cycle; uint64_t g_cycle_freed_exports = 0; void TraceEntry(cppgc::Visitor *visitor, uint32_t entry, uint32_t epoch) { auto *js = static_cast(visitor); if (entry & SCR_NODE_GC_EXPORT) { auto it = g_export_gc.find(entry & ~SCR_NODE_GC_EXPORT); if (it == g_export_gc.end() || it->second->rep.IsEmptyThreadSafe()) return; js->Trace(it->second->rep); it->second->traced.store(epoch, std::memory_order_relaxed); return; } if (entry == 0 || entry >= g_slots.size() || !g_slots[entry]) return; Slot &slot = *g_slots[entry]; if (!slot.live || slot.dead) return; js->Trace(slot.value); slot.traced.store(epoch, std::memory_order_relaxed); } class Roots final : public cppgc::GarbageCollected { public: void Trace(cppgc::Visitor *visitor) const { std::lock_guard lock(g_gc_lock); bool exact = g_gc_mode == kExact; uint32_t epoch = g_epoch; for (uint32_t id = 1; id < g_slots.size(); id++) if (g_slots[id] && (!exact || g_slots[id]->rooted)) TraceEntry(visitor, id, epoch); for (auto &it : g_export_gc) if (!exact || it.second->rooted) TraceEntry(visitor, it.first | SCR_NODE_GC_EXPORT, epoch); } }; class Fresh final : public cppgc::GarbageCollected { public: void Trace(cppgc::Visitor *visitor) const { std::lock_guard lock(g_gc_lock); for (uint32_t entry : g_fresh) TraceEntry(visitor, entry, g_epoch); g_fresh.clear(); g_fresh_pending = false; } }; class Reach final : public cppgc::GarbageCollected { public: Reach(uint32_t id, uint32_t gen) : id_(id), gen_(gen) {} void Trace(cppgc::Visitor *visitor) const { std::lock_guard lock(g_gc_lock); if (g_gc_mode != kExact) return; // Roots traces everything auto it = g_export_gc.find(id_); if (it == g_export_gc.end() || it->second->gen != gen_) return; for (uint32_t entry : it->second->reach) TraceEntry(visitor, entry, g_epoch); } private: uint32_t id_, gen_; }; v8::CppHeap *g_cpp_heap = nullptr; // What a representative keeps alive natively, as V8 learns of it (its // external memory): V8 schedules its major GCs by its own heap, which the // native halves of cross-heap garbage do not grow. An estimate per // representative (the native value, its closure or record and what they // hold); reported lazily, never from inside a GC. constexpr int64_t kExportExternalBytes = 2048; // The estimate follows what major GCs find. A major GC that frees fewer // exports than an eighth of those made since the previous one drops it to // kExportExternalFloor: long-lived exports would otherwise schedule a major // GC per 32k of them, each walking every handle. One that frees more // restores it. constexpr int64_t kExportExternalFloor = 64; int64_t g_export_external_bytes = kExportExternalBytes; uint64_t g_cycle_new_exports = 0; // exports made since the last major GC int64_t g_external_reported = 0; void ReportExternal(Isolate *isolate) { int64_t want = static_cast(g_export_gc.size()) * g_export_external_bytes; if (want == g_external_reported) return; isolate->AdjustAmountOfExternalAllocatedMemory(want - g_external_reported); g_external_reported = want; } cppgc::Persistent *g_roots = nullptr; cppgc::Persistent *g_fresh_root = nullptr; Global g_gc_key; Global g_carrier_template; bool Marking() { return g_cpp_heap != nullptr && cppgc::subtle::HeapState::IsMarking(g_cpp_heap->GetHeapHandle()); } // Makes V8 trace `object` again if it already did in this marking. void Retrace(const void *object) { cppgc::subtle::HeapConsistency::WriteBarrierParams params; if (cppgc::subtle::HeapConsistency::GetWriteBarrierType(object, params) == cppgc::subtle::HeapConsistency::WriteBarrierType::kMarking) cppgc::subtle::HeapConsistency::SteeleWriteBarrier(params, object); } // A reference made, or an export rooted, while V8 marks. void NoteFresh(uint32_t entry) { if (g_fresh_root == nullptr || !Marking()) return; bool retrace; { std::lock_guard lock(g_gc_lock); g_fresh.push_back(entry); retrace = !g_fresh_pending; g_fresh_pending = true; } if (retrace) Retrace(g_fresh_root->Get()); } // JavaScript handed export `export_id` to the runtime, or called into it: // under an exact analysis the runtime may now reach its native value, so it // becomes a root for the rest of this GC. void Touch(uint32_t export_id) { if (g_gc_mode != kExact) return; auto it = g_export_gc.find(export_id); if (it == g_export_gc.end() || it->second->rooted) return; { std::lock_guard lock(g_gc_lock); it->second->rooted = true; } g_gc_cycle.touched++; NoteFresh(export_id | SCR_NODE_GC_EXPORT); } uint64_t NowNs() { return static_cast(std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()) .count()); } // Exports found stable (held natively, or reaching nothing across the // boundary) in this many analyses in a row are tenured: analyzed again // only at every kTenuredEvery-th analysis, roots meanwhile (a bounded cost // for long-lived exports; their cycles are found at the next full one). constexpr uint8_t kTenure = 2; constexpr uint64_t kTenuredEvery = 8; // The analysis for this GC (in the prologue, before V8 marks or, for an // incremental GC, when its marking starts). Newest exports first: they are // the likeliest garbage, and the analysis may stop at its budget. void Analyze() { std::vector> order; // (gen, id) order.reserve(g_export_gc.size()); bool full = g_gc_stats.analyses % kTenuredEvery == 0; for (auto &it : g_export_gc) if (!it.second->rep.IsEmpty() && (full || it.second->tenure < kTenure)) order.emplace_back(it.second->gen, it.first); std::sort(order.begin(), order.end(), std::greater<>()); std::vector ids; ids.reserve(order.size()); for (auto &o : order) ids.push_back(o.second); ScrNodeGcResult r; std::memset(&r, 0, sizeof r); uint64_t t0 = NowNs(); // A host without the analysis (the smoke tests) keeps every handle a root. int status = g_host->gc_analyze != nullptr && !g_gc_off ? g_host->gc_analyze(ids.data(), ids.size(), &r) : 1; uint64_t ns = NowNs() - t0; g_gc_stats.analysis_ns += ns; if (ns > g_gc_stats.max_analysis_ns) g_gc_stats.max_analysis_ns = ns; std::lock_guard lock(g_gc_lock); for (auto &slot : g_slots) if (slot) slot->rooted = true; for (auto &it : g_export_gc) { it.second->rooted = true; it.second->reach.clear(); } g_gc_cycle = {}; g_gc_cycle.ns = ns; if (status != 0) { g_gc_mode = kConservative; return; } g_gc_stats.analyses++; g_gc_cycle.analyzed = true; g_gc_cycle.exports = ids.size(); g_gc_cycle.imports = g_live_handles; g_gc_cycle.visited = r.visited; for (size_t i = 0; i < r.nfree_imports; i++) { uint32_t id = r.free_imports[i]; if (id < g_slots.size() && g_slots[id] && g_slots[id]->live) { g_slots[id]->rooted = false; g_gc_cycle.free++; } } for (size_t i = 0; i < ids.size(); i++) { ExportGc &x = *g_export_gc[ids[i]]; x.rooted = r.rooted[i] != 0; if (x.rooted) g_gc_cycle.rooted++; // Stable: held natively, or reaching nothing across the boundary // (rooting such an export only delays freeing its representative). bool stable = x.rooted || r.reach_len[i] == 0; x.tenure = stable ? (x.tenure < 255 ? x.tenure + 1 : 255) : 0; x.reach.assign(r.reach + r.reach_start[i], r.reach + r.reach_start[i] + r.reach_len[i]); g_gc_cycle.reach += r.reach_len[i]; } g_gc_mode = kExact; } void OnGcPrologue(Isolate *, v8::GCType type, v8::GCCallbackFlags, void *) { bool marking = Marking(); // V8 calls the prologue of an incremental GC before its marking starts, // so nothing was traced yet: a new marking cycle, whose traced references // carry a new epoch. if (!marking) g_epoch++; // The final pause of an incremental GC keeps the analysis made when its // marking started: anything the program reached since went through Touch // or NoteFresh. if (type == v8::kGCTypeMarkSweepCompact && marking && g_gc_mode == kExact) return; Analyze(); g_gc_cycle.incremental = type == v8::kGCTypeIncrementalMarking; if (marking && type == v8::kGCTypeIncrementalMarking && g_gc_trace) std::fprintf(stderr, "[node-islands] gc: marking had started before the prologue\n"); } const char *Scratch(std::string text, size_t *len) { g_scratch.push_back(std::move(text)); *len = g_scratch.back().size(); return g_scratch.back().data(); } // WTF-8: V8's UTF-8 writer encodes a lone surrogate as its 3-byte form. std::string Wtf8(Isolate *isolate, Local s) { size_t n = s->Utf8LengthV2(isolate); std::string out(n, '\0'); if (n > 0) s->WriteUtf8V2(isolate, out.data(), n); return out; } bool HasLoneSurrogate(const char *p, size_t n) { for (size_t i = 0; i + 1 < n; i++) if (static_cast(p[i]) == 0xED && static_cast(p[i + 1]) >= 0xA0) return true; return false; } // A runtime string in V8: NewFromUtf8 would replace lone surrogates, so a // string that holds one decodes WTF-8 to UTF-16 here. Local FromWtf8(Isolate *isolate, const char *p, size_t n) { if (!HasLoneSurrogate(p, n)) return Utf8(isolate, p, n); std::vector units; units.reserve(n); for (size_t i = 0; i < n;) { unsigned char c = static_cast(p[i]); uint32_t cp; size_t k; if (c < 0x80) { cp = c; k = 1; } else if ((c & 0xE0) == 0xC0 && i + 1 < n) { cp = ((c & 0x1F) << 6) | (p[i + 1] & 0x3F); k = 2; } else if ((c & 0xF0) == 0xE0 && i + 2 < n) { cp = ((c & 0x0F) << 12) | ((p[i + 1] & 0x3F) << 6) | (p[i + 2] & 0x3F); k = 3; } else if ((c & 0xF8) == 0xF0 && i + 3 < n) { cp = ((c & 0x07) << 18) | ((p[i + 1] & 0x3F) << 12) | ((p[i + 2] & 0x3F) << 6) | (p[i + 3] & 0x3F); k = 4; } else { cp = 0xFFFD; k = 1; } if (cp >= 0x10000) { cp -= 0x10000; units.push_back(static_cast(0xD800 + (cp >> 10))); units.push_back(static_cast(0xDC00 + (cp & 0x3FF))); } else { units.push_back(static_cast(cp)); } i += k; } return String::NewFromTwoByte(isolate, units.data(), NewStringType::kNormal, static_cast(units.size())) .ToLocalChecked(); } // A decimal BigInt text in V8, through 64-bit words. Local BigIntFromText(Local context, const char *p, size_t n) { int sign = 0; size_t i = 0; if (i < n && p[i] == '-') { sign = 1; i++; } std::vector words(1, 0); for (; i < n; i++) { unsigned __int128 carry = static_cast(p[i] - '0'); for (uint64_t &w : words) { unsigned __int128 v = static_cast(w) * 10 + carry; w = static_cast(v); carry = v >> 64; } if (carry != 0) words.push_back(static_cast(carry)); } return v8::BigInt::NewFromWords(context, sign, static_cast(words.size()), words.data()) .ToLocalChecked(); } uint32_t ElemOf(Local view) { // ScrBytesElem numbering (scr_bytes.h). if (view->IsUint8Array()) return 0; if (view->IsUint32Array()) return 1; if (view->IsFloat32Array()) return 2; if (view->IsInt32Array()) return 3; if (view->IsFloat64Array()) return 4; if (view->IsInt8Array()) return 5; if (view->IsUint16Array()) return 6; if (view->IsInt16Array()) return 7; if (view->IsUint8ClampedArray()) return 8; if (view->IsFloat16Array()) return 9; return 0; // DataView: bytes } size_t ElemSize(uint32_t elem) { static const size_t sizes[] = {1, 4, 4, 4, 8, 1, 2, 2, 1, 2}; return elem < 10 ? sizes[elem] : 1; } Local ToV8(Isolate *isolate, Local context, const ScrNodeValue *in); void FromV8(Isolate *isolate, Local context, Local 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 g_export_key; Global g_export_template; std::unordered_map> g_exports; std::unordered_map> g_resolvers; std::vector g_released_exports; Local 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); } // Array exports are Proxies (Array.isArray holds); a Proxy has no private // symbols of its own, so they are found by identity hash. std::unordered_multimap g_proxy_ids; uint32_t ExportIdOf(Isolate *isolate, Local context, Local v) { if (!v->IsObject() || g_exports.empty()) return 0; if (v->IsProxy()) { auto range = g_proxy_ids.equal_range(v.As()->GetIdentityHash()); for (auto it = range.first; it != range.second; ++it) { auto found = g_exports.find(it->second); if (found != g_exports.end() && found->second.Get(isolate)->StrictEquals(v)) return it->second; } return 0; } Local id; if (!v.As()->GetPrivate(context, ExportKey(isolate)).ToLocal(&id) || !id->IsUint32()) return 0; return id.As()->Value(); } void OnExportGone(const v8::WeakCallbackInfo &info) { uint32_t id = static_cast(reinterpret_cast(info.GetParameter())); auto it = g_exports.find(id); if (it != g_exports.end()) { it->second.Reset(); g_exports.erase(it); } { // Nothing traced the representative: V8 reclaims its traced reference. std::lock_guard lock(g_gc_lock); g_export_gc.erase(id); } g_gc_stats.freed_exports++; g_cycle_freed_exports++; for (auto p = g_proxy_ids.begin(); p != g_proxy_ids.end();) { if (p->second == id) p = g_proxy_ids.erase(p); else ++p; } g_released_exports.push_back(id); } // Delivers the releases V8's collector queued (never from inside a GC). void FlushReleasedExports() { if (!g_released_exports.empty() && g_setup) ReportExternal(g_setup->isolate()); 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); } } // The export id a representative's callback carries. JavaScript is // calling into that export: the runtime may reach its native value now. uint32_t ExportIdArg(Local data) { uint32_t id = data->IsUint32() ? data.As()->Value() : 0; if (id != 0) Touch(id); return id; } // 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, int status, const ScrNodeValue *out, Local *result) { Local v = ToV8(isolate, context, out); if (status == 1) isolate->ThrowException(v); else *result = v; } void ExportCall(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); HostCall scope; uint32_t id = ExportIdArg(info.Data()); std::vector args(static_cast(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()) { // `class Sub extends NativeClass` in island code: the native instance // cannot take the island prototype's overrides (NI-6 shortcut). auto self = g_exports.find(id); if (self != g_exports.end() && !info.NewTarget()->StrictEquals(self->second.Get(isolate))) { isolate->ThrowException(v8::Exception::TypeError(Utf8( isolate, "scriptc: island code cannot extend a native class of a shared module yet"))); return; } 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 result; ThrowOrReturn(isolate, context, status, &out, &result); if (!result.IsEmpty()) info.GetReturnValue().Set(result); } void ExportHasInstance(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); HostCall scope; ScrNodeValue v; FromV8(isolate, context, info[0], &v); info.GetReturnValue().Set(g_host->export_instance_of(ExportIdArg(info.Data()), &v) == 1); } uint32_t HolderId(Local holder) { if (holder->InternalFieldCount() < 1) return 0; return ExportIdArg(holder->GetInternalField(0).As()); } template uint32_t InfoId(const v8::PropertyCallbackInfo &info) { return HolderId(info.HolderV2()); } void NameKey(Isolate *isolate, Local context, Local 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 &info, const ScrNodeValue *key) { Isolate *isolate = info.GetIsolate(); Local 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 result; ThrowOrReturn(isolate, context, status, &out, &result); if (!result.IsEmpty()) info.GetReturnValue().Set(result); return v8::Intercepted::kYes; } v8::Intercepted SetKey(const v8::PropertyCallbackInfo &info, const ScrNodeValue *key, Local value) { Isolate *isolate = info.GetIsolate(); Local 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 &info, const ScrNodeValue *key) { HostCall scope; if (!g_host->export_has(InfoId(info), key)) return v8::Intercepted::kNo; info.GetReturnValue().Set(static_cast(v8::None)); return v8::Intercepted::kYes; } v8::Intercepted DeleteKey(const v8::PropertyCallbackInfo &info, const ScrNodeValue *key) { Isolate *isolate = info.GetIsolate(); Local 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 name, const v8::PropertyCallbackInfo &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 name, Local value, const v8::PropertyCallbackInfo &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 name, const v8::PropertyCallbackInfo &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 name, const v8::PropertyCallbackInfo &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 &info) { ScrNodeValue key; IndexKey(index, &key); return GetKey(info, &key); } v8::Intercepted IndexedSet(uint32_t index, Local value, const v8::PropertyCallbackInfo &info) { ScrNodeValue key; IndexKey(index, &key); return SetKey(info, &key, value); } v8::Intercepted IndexedQuery(uint32_t index, const v8::PropertyCallbackInfo &info) { ScrNodeValue key; IndexKey(index, &key); return QueryKey(info, &key); } v8::Intercepted IndexedDelete(uint32_t index, const v8::PropertyCallbackInfo &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 ExportKeys(const v8::PropertyCallbackInfo &info, bool indices) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); HostCall scope; ScrNodeValue out; std::memset(&out, 0, sizeof out); std::vector> keep; if (g_host->export_keys(InfoId(info), &out) == 0) { Local all = ToV8(isolate, context, &out); if (all->IsArray()) { Local list = all.As(); for (uint32_t i = 0; i < list->Length(); i++) { Local key = list->Get(context, i).ToLocalChecked(); uint32_t index = 0; bool is_index = false; if (key->IsString()) { Local 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(v8::Integer::NewFromUnsigned(isolate, index)) : key); } } } return v8::Array::New(isolate, keep.data(), keep.size()); } void NamedEnum(const v8::PropertyCallbackInfo &info) { info.GetReturnValue().Set(ExportKeys(info, false)); } void IndexedEnum(const v8::PropertyCallbackInfo &info) { info.GetReturnValue().Set(ExportKeys(info, true)); } Local ExportTemplate(Isolate *isolate) { if (g_export_template.IsEmpty()) { Local t = v8::ObjectTemplate::New(isolate); t->SetInternalFieldCount(1); t->SetHandler(v8::NamedPropertyHandlerConfiguration( NamedGet, NamedSet, NamedQuery, NamedDelete, NamedEnum, Local(), v8::PropertyHandlerFlags::kOnlyInterceptStrings)); t->SetHandler(v8::IndexedPropertyHandlerConfiguration(IndexedGet, IndexedSet, IndexedQuery, IndexedDelete, IndexedEnum)); g_export_template.Reset(isolate, t); } return g_export_template.Get(isolate); } // ── array exports: a Proxy over an empty Array whose traps reach the // native array (length and elements); everything else (the Array.prototype // methods, symbols) is the target's, so `map`, `push`, iteration and // JSON.stringify work through the traps. uint32_t TrapId(const FunctionCallbackInfo &info) { return ExportIdArg(info.Data()); } bool TrapStringKey(Isolate *isolate, Local context, Local key, ScrNodeValue *out) { if (!key->IsString()) return false; FromV8(isolate, context, key, out); return true; } // The Proxy target mirrors the native array, for what reads the target // directly (util.inspect formats a Proxy's target): refreshed whenever the // array crosses into Node, and updated by the island's own writes. void MirrorSet(Local context, Local target, Local key, Local value) { if (target->IsArray()) target.As()->Set(context, key, value).FromMaybe(false); } Local ArrayLength(Isolate *isolate, Local context, uint32_t id); void SyncArrayTarget(Isolate *isolate, Local context, uint32_t id, Local proxy) { if (!proxy->IsProxy()) return; Local target_value = proxy.As()->GetTarget(); if (!target_value->IsArray()) return; Local target = target_value.As(); uint32_t n = ArrayLength(isolate, context, id)->Uint32Value(context).FromMaybe(0); target->Set(context, Utf8(isolate, "length"), v8::Integer::NewFromUnsigned(isolate, 0)).FromMaybe(false); for (uint32_t i = 0; i < n; i++) { std::string index = std::to_string(i); ScrNodeValue key, out; std::memset(&key, 0, sizeof key); std::memset(&out, 0, sizeof out); key.tag = SCR_NV_STRING; key.u.str.data = index.data(); key.u.str.len = index.size(); if (g_host->export_get(id, &key, &out) != 0) continue; target->Set(context, i, ToV8(isolate, context, &out)).FromMaybe(false); } } void ArrayGet(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); HostCall scope; ScrNodeValue key, out; std::memset(&out, 0, sizeof out); if (TrapStringKey(isolate, context, info[1], &key)) { int status = g_host->export_get(TrapId(info), &key, &out); if (status != 2) { Local result; ThrowOrReturn(isolate, context, status, &out, &result); if (!result.IsEmpty()) info.GetReturnValue().Set(result); return; } } Local v; if (info[0].As()->Get(context, info[1]).ToLocal(&v)) info.GetReturnValue().Set(v); } void ArraySet(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); HostCall scope; ScrNodeValue key, value, out; std::memset(&out, 0, sizeof out); if (!TrapStringKey(isolate, context, info[1], &key)) { info.GetReturnValue().Set(info[0].As()->Set(context, info[1], info[2]).FromMaybe(false)); return; } FromV8(isolate, context, info[2], &value); int status = g_host->export_set(TrapId(info), &key, &value, &out); if (status == 1) { isolate->ThrowException(ToV8(isolate, context, &out)); return; } MirrorSet(context, info[0], info[1], info[2]); info.GetReturnValue().Set(true); } void ArrayHas(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); HostCall scope; ScrNodeValue key; if (TrapStringKey(isolate, context, info[1], &key) && g_host->export_has(TrapId(info), &key)) { info.GetReturnValue().Set(true); return; } info.GetReturnValue().Set(info[0].As()->Has(context, info[1]).FromMaybe(false)); } void ArrayDelete(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); HostCall scope; ScrNodeValue key, out; std::memset(&out, 0, sizeof out); if (TrapStringKey(isolate, context, info[1], &key)) { int status = g_host->export_delete(TrapId(info), &key, &out); if (status == 1) { isolate->ThrowException(ToV8(isolate, context, &out)); return; } } info[0].As()->Delete(context, info[1]).FromMaybe(false); info.GetReturnValue().Set(true); } Local ArrayLength(Isolate *isolate, Local context, uint32_t id) { ScrNodeValue key, out; std::memset(&key, 0, sizeof key); std::memset(&out, 0, sizeof out); key.tag = SCR_NV_STRING; key.u.str.data = "length"; key.u.str.len = 6; if (g_host->export_get(id, &key, &out) != 0) return v8::Integer::New(isolate, 0); return ToV8(isolate, context, &out); } void ArrayOwnKeys(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); HostCall scope; ScrNodeValue out; std::memset(&out, 0, sizeof out); std::vector> keys; if (g_host->export_keys(TrapId(info), &out) == 0) { Local all = ToV8(isolate, context, &out); if (all->IsArray()) { Local list = all.As(); for (uint32_t i = 0; i < list->Length(); i++) keys.push_back(list->Get(context, i).ToLocalChecked()); } } keys.push_back(Utf8(isolate, "length")); info.GetReturnValue().Set(v8::Array::New(isolate, keys.data(), keys.size())); } void ArrayDescriptor(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); HostCall scope; uint32_t id = TrapId(info); ScrNodeValue key, out; std::memset(&out, 0, sizeof out); if (!TrapStringKey(isolate, context, info[1], &key)) return; Local desc = Object::New(isolate); if (key.u.str.len == 6 && std::memcmp(key.u.str.data, "length", 6) == 0) { desc->Set(context, Utf8(isolate, "value"), ArrayLength(isolate, context, id)).Check(); desc->Set(context, Utf8(isolate, "writable"), v8::True(isolate)).Check(); desc->Set(context, Utf8(isolate, "enumerable"), v8::False(isolate)).Check(); desc->Set(context, Utf8(isolate, "configurable"), v8::False(isolate)).Check(); info.GetReturnValue().Set(desc); return; } if (!g_host->export_has(id, &key)) return; int status = g_host->export_get(id, &key, &out); if (status == 1) { isolate->ThrowException(ToV8(isolate, context, &out)); return; } desc->Set(context, Utf8(isolate, "value"), status == 0 ? ToV8(isolate, context, &out) : Local(v8::Undefined(isolate))).Check(); desc->Set(context, Utf8(isolate, "writable"), v8::True(isolate)).Check(); desc->Set(context, Utf8(isolate, "enumerable"), v8::True(isolate)).Check(); desc->Set(context, Utf8(isolate, "configurable"), v8::True(isolate)).Check(); info.GetReturnValue().Set(desc); } void ArrayDefine(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); HostCall scope; ScrNodeValue key, value, out; std::memset(&out, 0, sizeof out); if (!TrapStringKey(isolate, context, info[1], &key) || !info[2]->IsObject()) { info.GetReturnValue().Set(false); return; } Local v; if (!info[2].As()->Get(context, Utf8(isolate, "value")).ToLocal(&v)) return; FromV8(isolate, context, v, &value); int status = g_host->export_set(TrapId(info), &key, &value, &out); if (status == 1) { isolate->ThrowException(ToV8(isolate, context, &out)); return; } MirrorSet(context, info[0], info[1], v); info.GetReturnValue().Set(true); } // ── Map and Set exports: a real Map/Set made by the bootstrap's // makeCollection, whose methods call `op` (host->export_op). void CollectionOp(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); HostCall scope; uint32_t op = info.Length() > 0 ? info[0]->Uint32Value(context).FromMaybe(0) : 0; std::vector args(info.Length() > 1 ? static_cast(info.Length() - 1) : 0); for (int i = 1; i < info.Length(); i++) FromV8(isolate, context, info[i], &args[i - 1]); ScrNodeValue out; std::memset(&out, 0, sizeof out); int status = g_host->export_op(TrapId(info), op, args.data(), args.size(), &out); Local result; ThrowOrReturn(isolate, context, status, &out, &result); if (!result.IsEmpty()) info.GetReturnValue().Set(result); } Local CollectionView(Isolate *isolate, Local context, Local data, bool map) { Local helpers, make, view; Local load_argv[] = {Utf8(isolate, "scriptc:island-helpers"), v8::False(isolate)}; if (g_load.Get(isolate)->Call(context, v8::Undefined(isolate), 2, load_argv).ToLocal(&helpers) && helpers->IsObject() && helpers.As()->Get(context, Utf8(isolate, "makeCollection")).ToLocal(&make) && make->IsFunction()) { Local argv[] = {v8::Boolean::New(isolate, map), Function::New(context, CollectionOp, data).ToLocalChecked()}; if (make.As()->Call(context, helpers, 2, argv).ToLocal(&view) && view->IsObject()) return view.As(); } return Object::New(isolate); } Local ArrayProxy(Isolate *isolate, Local context, Local data) { Local handler = Object::New(isolate); struct Trap { const char *name; v8::FunctionCallback fn; } traps[] = {{"get", ArrayGet}, {"set", ArraySet}, {"has", ArrayHas}, {"deleteProperty", ArrayDelete}, {"ownKeys", ArrayOwnKeys}, {"getOwnPropertyDescriptor", ArrayDescriptor}, {"defineProperty", ArrayDefine}}; for (const Trap &t : traps) handler->Set(context, Utf8(isolate, t.name), Function::New(context, t.fn, data).ToLocalChecked()) .Check(); return v8::Proxy::New(context, v8::Array::New(isolate, 0), handler).ToLocalChecked(); } // Arrays, Maps and Sets keep a copy of their contents where util.inspect // looks (the Proxy target, the collection's internal slots), refreshed at // every crossing. // The exports whose mirrors are being refreshed: an array or collection // that holds itself reaches its own refresh again through its contents. std::unordered_set g_refreshing; void RefreshMirror(Isolate *isolate, Local context, uint32_t id, uint32_t flags, Local obj) { if (!g_refreshing.insert(id).second) return; struct Done { uint32_t id; ~Done() { g_refreshing.erase(id); } } done{id}; HostCall scope; if (flags & SCR_NVF_ARRAY) { SyncArrayTarget(isolate, context, id, obj); } else if (flags & (SCR_NVF_MAP | SCR_NVF_SET)) { Local helpers, sync; Local load_argv[] = {Utf8(isolate, "scriptc:island-helpers"), v8::False(isolate)}; if (g_load.Get(isolate)->Call(context, v8::Undefined(isolate), 2, load_argv).ToLocal(&helpers) && helpers->IsObject() && helpers.As()->Get(context, Utf8(isolate, "syncCollection")).ToLocal(&sync) && sync->IsFunction()) { Local argv[] = {obj}; sync.As()->Call(context, helpers, 1, argv).FromMaybe(Local()); } } } // NI-8: the representative of export `id` holds a carrier whose CppHeap // object traces what the export reaches, and the export's traced reference // (marked only by the collector) starts out a root. void AttachReach(Isolate *isolate, Local context, uint32_t id, Local rep) { if (g_cpp_heap == nullptr) return; if (g_carrier_template.IsEmpty()) { Local t = v8::ObjectTemplate::New(isolate); t->SetInternalFieldCount(1); g_carrier_template.Reset(isolate, t); } if (g_gc_key.IsEmpty()) g_gc_key.Reset(isolate, v8::Private::New(isolate, Utf8(isolate, "scriptc:gc"))); Local carrier = g_carrier_template.Get(isolate)->NewInstance(context).ToLocalChecked(); uint32_t gen = ++g_export_gen; Reach *reach = cppgc::MakeGarbageCollected(g_cpp_heap->GetAllocationHandle(), id, gen); v8::Object::Wrap(isolate, carrier, reach); // A Proxy holds its target: an array export's private lives there. Local holder = rep->IsProxy() ? rep.As()->GetTarget().As() : rep; holder->SetPrivate(context, g_gc_key.Get(isolate), carrier).Check(); { std::lock_guard lock(g_gc_lock); std::unique_ptr &x = g_export_gc[id]; x = std::make_unique(); x->rep.Reset(isolate, rep); x->gen = gen; } g_cycle_new_exports++; NoteFresh(id | SCR_NODE_GC_EXPORT); ReportExternal(isolate); } Local ExportValue(Isolate *isolate, Local context, const ScrNodeValue *in) { uint32_t id = in->u.exp.id; auto found = g_exports.find(id); if (found != g_exports.end()) { Touch(id); Local existing = found->second.Get(isolate); RefreshMirror(isolate, context, id, in->flags, existing); return existing; } Local obj; Local data = v8::Integer::NewFromUnsigned(isolate, id); if (in->flags & SCR_NVF_FUNCTION) { Local fn = Function::New(context, ExportCall, data, static_cast(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)); if (in->flags & SCR_NVF_CONSTRUCTOR) { // A native class: instanceof asks the runtime (NI-6). fn->DefineOwnProperty(context, v8::Symbol::GetHasInstance(isolate), Function::New(context, ExportHasInstance, data, 1).ToLocalChecked(), static_cast(v8::DontEnum | v8::DontDelete | v8::ReadOnly)) .Check(); } obj = fn; } else if (in->flags & SCR_NVF_PROMISE) { Local resolver = v8::Promise::Resolver::New(context).ToLocalChecked(); g_resolvers[id].Reset(isolate, resolver); obj = resolver->GetPromise(); } else if (in->flags & (SCR_NVF_MAP | SCR_NVF_SET)) { obj = CollectionView(isolate, context, data, (in->flags & SCR_NVF_MAP) != 0); } else if (in->flags & SCR_NVF_ARRAY) { obj = ArrayProxy(isolate, context, data); g_proxy_ids.emplace(obj->GetIdentityHash(), id); } else { obj = ExportTemplate(isolate)->NewInstance(context).ToLocalChecked(); obj->SetInternalField(0, data); // A template instance inherits from its implicit constructor's own // prototype object; a native plain object is a plain object to island // code (`Object.getPrototypeOf(o) === Object.prototype`, as jose's // isObject checks), so it inherits Object.prototype directly. obj->SetPrototypeV2(context, v8::Object::New(isolate)->GetPrototypeV2()).FromMaybe(false); } if (!obj->IsProxy()) obj->SetPrivate(context, ExportKey(isolate), data).Check(); AttachReach(isolate, context, id, obj); Global &slot = g_exports[id]; slot.Reset(isolate, obj); slot.SetWeak(reinterpret_cast(static_cast(id)), OnExportGone, v8::WeakCallbackType::kParameter); if (in->flags & SCR_NVF_PROMISE) g_host->export_watch(id); RefreshMirror(isolate, context, id, in->flags, obj); 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 resolver = it->second.Get(e.isolate); it->second.Reset(); g_resolvers.erase(it); Local v = ToV8(e.isolate, e.context, value); if (rejected) resolver->Reject(e.context, v).Check(); else resolver->Resolve(e.context, v).Check(); } void DropAllExports() { { std::lock_guard lock(g_gc_lock); for (auto &it : g_export_gc) it.second->rep.Reset(); g_export_gc.clear(); } 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_proxy_ids.clear(); g_export_template.Reset(); g_export_key.Reset(); } // After a major GC: what the collector left untraced is reclaimed. Dead // imports are forgotten (the runtime drops them from the garbage it frees // next); a representative that lived on through JavaScript alone gets a new // traced reference. Then the collector is conservative again until the // next analysis. void OnGcEpilogue(Isolate *isolate, v8::GCType, v8::GCCallbackFlags, void *) { HandleScope handles(isolate); uint64_t freed_imports = 0; std::vector refresh, dead; { std::lock_guard lock(g_gc_lock); uint32_t epoch = g_epoch; for (uint32_t id = 1; id < g_slots.size(); id++) { Slot *slot = g_slots[id].get(); if (slot == nullptr || !slot->live || slot->dead) continue; if (slot->traced.load(std::memory_order_relaxed) == epoch) continue; slot->dead = true; new (&slot->value) v8::TracedReference(); dead.push_back(id); freed_imports++; } for (auto &it : g_export_gc) { ExportGc &x = *it.second; if (x.traced.load(std::memory_order_relaxed) != epoch) { new (&x.rep) v8::TracedReference(); refresh.push_back(it.first); } x.rooted = true; x.reach.clear(); } for (auto &slot : g_slots) if (slot) slot->rooted = true; g_gc_mode = kConservative; g_fresh.clear(); g_fresh_pending = false; g_epoch++; } for (uint32_t id : dead) ForgetIdentity(id, g_slots[id]->hash); for (uint32_t id : refresh) { auto weak = g_exports.find(id); std::lock_guard lock(g_gc_lock); auto x = g_export_gc.find(id); if (x == g_export_gc.end()) continue; if (weak == g_exports.end() || weak->second.IsEmpty()) g_export_gc.erase(x); else x->second->rep.Reset(isolate, weak->second.Get(isolate)); } g_gc_stats.major_gcs++; g_gc_stats.freed_imports += freed_imports; if (g_cycle_new_exports >= 1024) g_export_external_bytes = g_cycle_freed_exports * 8 < g_cycle_new_exports ? kExportExternalFloor : kExportExternalBytes; g_cycle_new_exports = 0; if (g_gc_trace) { std::fflush(stdout); if (g_gc_cycle.analyzed) std::fprintf(stderr, "[node-islands] gc %llu%s: exports %zu (native roots %zu, reach %zu, touched " "%zu), imports %zu (free %zu), walked %zu in %.3f ms; freed exports %llu, " "imports %llu\n", (unsigned long long)g_gc_stats.major_gcs, g_gc_cycle.incremental ? " (incremental)" : "", g_gc_cycle.exports, g_gc_cycle.rooted, g_gc_cycle.reach, g_gc_cycle.touched, g_gc_cycle.imports, g_gc_cycle.free, g_gc_cycle.visited, (double)g_gc_cycle.ns / 1e6, (unsigned long long)g_cycle_freed_exports, (unsigned long long)freed_imports); else std::fprintf(stderr, "[node-islands] gc %llu: not analyzed (every handle a root); freed exports " "%llu, imports %llu\n", (unsigned long long)g_gc_stats.major_gcs, (unsigned long long)g_cycle_freed_exports, (unsigned long long)freed_imports); } g_gc_cycle = {}; g_cycle_freed_exports = 0; } void GcStats(ScrNodeGcStats *out) { *out = g_gc_stats; out->live_exports = static_cast(g_exports.size()); out->live_imports = g_live_handles; } bool StartCollector(Isolate *isolate) { const char *trace = std::getenv("SCRIPTC_NODE_ISLANDS_TRACE"); g_gc_trace = trace != nullptr && std::strstr(trace, "gc") != nullptr; const char *mode = std::getenv("SCRIPTC_NODE_ISLANDS_GC"); g_gc_off = mode != nullptr && std::strcmp(mode, "off") == 0; // Node creates every isolate with a CppHeap; the import table's traced // references need one, or V8 would reclaim them at its first major GC. g_cpp_heap = isolate->GetCppHeap(); if (g_cpp_heap == nullptr) return false; g_roots = new cppgc::Persistent( cppgc::MakeGarbageCollected(g_cpp_heap->GetAllocationHandle())); g_fresh_root = new cppgc::Persistent( cppgc::MakeGarbageCollected(g_cpp_heap->GetAllocationHandle())); isolate->AddGCPrologueCallback( OnGcPrologue, nullptr, static_cast(v8::kGCTypeMarkSweepCompact | v8::kGCTypeIncrementalMarking)); isolate->AddGCEpilogueCallback(OnGcEpilogue, nullptr, v8::kGCTypeMarkSweepCompact); return true; } void StopCollector(Isolate *isolate) { if (g_roots == nullptr) return; if (g_external_reported != 0) isolate->AdjustAmountOfExternalAllocatedMemory(-g_external_reported); g_external_reported = 0; isolate->RemoveGCPrologueCallback(OnGcPrologue, nullptr); isolate->RemoveGCEpilogueCallback(OnGcEpilogue, nullptr); delete g_roots; delete g_fresh_root; g_roots = nullptr; g_fresh_root = nullptr; g_carrier_template.Reset(); g_gc_key.Reset(); } void FromV8(Isolate *isolate, Local context, Local v, ScrNodeValue *out) { std::memset(out, 0, sizeof *out); if (v.IsEmpty() || v->IsUndefined()) { out->tag = SCR_NV_UNDEFINED; return; } if (v->IsNull()) { out->tag = SCR_NV_NULL; return; } if (v->IsBoolean()) { out->tag = SCR_NV_BOOL; out->u.boolean = v->IsTrue() ? 1 : 0; return; } if (v->IsNumber()) { out->tag = SCR_NV_NUMBER; out->u.num = v.As()->Value(); return; } if (v->IsString()) { out->tag = SCR_NV_STRING; out->u.str.data = Scratch(Wtf8(isolate, v.As()), &out->u.str.len); return; } if (v->IsBigInt()) { out->tag = SCR_NV_BIGINT; Local text = v->ToString(context).ToLocalChecked(); out->u.str.data = Scratch(Wtf8(isolate, text), &out->u.str.len); return; } if (v->IsArrayBufferView()) { Local view = v.As(); bool dataview = view->IsDataView(); // Buffer() moves on-heap elements off the heap: the data stays put. Local buffer = view->Buffer(); std::shared_ptr store = buffer->GetBackingStore(); if (!store->IsShared() && !store->IsResizableByUserJavaScript()) { uint32_t elem = dataview ? 0 : ElemOf(view); out->tag = SCR_NV_BYTES; out->u.bytes.elem = elem; out->u.bytes.data = static_cast(store->Data()) + view->ByteOffset(); out->u.bytes.len = view->ByteLength() / ElemSize(elem); if (node::Buffer::HasInstance(v)) out->flags |= SCR_NVF_BUFFER; if (dataview) out->flags |= SCR_NVF_DATAVIEW; out->u.bytes.handle = Import(isolate, v, store); return; } } if (uint32_t export_id = ExportIdOf(isolate, context, v)) { Touch(export_id); 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; if (v->IsFunction()) { flags |= SCR_NVF_FUNCTION; if (v.As()->IsConstructor()) flags |= SCR_NVF_CONSTRUCTOR; } if (v->IsNativeError()) flags |= SCR_NVF_ERROR; if (v->IsArray()) flags |= SCR_NVF_ARRAY; if (v->IsPromise()) flags |= SCR_NVF_PROMISE; out->flags = flags; out->u.handle = Import(isolate, v); } void FreeRuntimeBytes(void *, size_t, void *owner) { if (owner != nullptr) g_host->bytes_free(owner); } Local ErrorConstructor(Isolate *isolate, Local context, uint32_t kind) { static const char *const names[] = {"Error", "TypeError", "RangeError", "SyntaxError", "Error", "ReferenceError", "EvalError", "URIError"}; const char *name = kind < 8 ? names[kind] : "Error"; return context->Global()->Get(context, Utf8(isolate, name)).ToLocalChecked(); } Local ToV8(Isolate *isolate, Local context, const ScrNodeValue *in) { if (in == nullptr) return v8::Undefined(isolate); switch (in->tag) { case SCR_NV_NULL: return v8::Null(isolate); case SCR_NV_BOOL: return v8::Boolean::New(isolate, in->u.boolean != 0); case SCR_NV_NUMBER: return v8::Number::New(isolate, in->u.num); case SCR_NV_STRING: return FromWtf8(isolate, in->u.str.data, in->u.str.len); case SCR_NV_BIGINT: return BigIntFromText(context, in->u.str.data, in->u.str.len); case SCR_NV_HANDLE: return HandleValue(isolate, in->u.handle); case SCR_NV_BYTES: { if (in->u.bytes.handle != 0) return HandleValue(isolate, in->u.bytes.handle); size_t elem_size = ElemSize(in->u.bytes.elem); size_t byte_len = in->u.bytes.len * elem_size; Local buffer; if (byte_len == 0 || in->u.bytes.data == nullptr) { buffer = v8::ArrayBuffer::New(isolate, 0); if (in->u.bytes.owner != nullptr) g_host->bytes_free(in->u.bytes.owner); } else { std::unique_ptr store = v8::ArrayBuffer::NewBackingStore( in->u.bytes.data, byte_len, FreeRuntimeBytes, in->u.bytes.owner); buffer = v8::ArrayBuffer::New(isolate, std::move(store)); } size_t n = in->u.bytes.len; if (in->flags & SCR_NVF_DATAVIEW) return v8::DataView::New(buffer, 0, byte_len); if (in->flags & SCR_NVF_BUFFER) return node::Buffer::New(isolate, buffer, 0, byte_len).ToLocalChecked(); switch (in->u.bytes.elem) { case 1: return v8::Uint32Array::New(buffer, 0, n); case 2: return v8::Float32Array::New(buffer, 0, n); case 3: return v8::Int32Array::New(buffer, 0, n); case 4: return v8::Float64Array::New(buffer, 0, n); case 5: return v8::Int8Array::New(buffer, 0, n); case 6: return v8::Uint16Array::New(buffer, 0, n); case 7: return v8::Int16Array::New(buffer, 0, n); case 8: return v8::Uint8ClampedArray::New(buffer, 0, n); case 9: return v8::Float16Array::New(buffer, 0, n); default: return v8::Uint8Array::New(buffer, 0, n); } } case SCR_NV_ERROR: { Local ctor = ErrorConstructor(isolate, context, in->u.error.kind); Local argv[] = {FromWtf8(isolate, in->u.error.message, in->u.error.message_len)}; Local error = ctor.As()->NewInstance(context, 1, argv).ToLocalChecked(); std::string name(in->u.error.name, in->u.error.name_len); Local default_name; if (!error->Get(context, Utf8(isolate, "name")).ToLocal(&default_name) || ToStdString(isolate, default_name) != name) error->Set(context, Utf8(isolate, "name"), FromWtf8(isolate, name.data(), name.size())).Check(); if (in->u.error.stack != nullptr) error->Set(context, Utf8(isolate, "stack"), FromWtf8(isolate, in->u.error.stack, in->u.error.stack_len)).Check(); if (in->u.error.code != nullptr) error->Set(context, Utf8(isolate, "code"), 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 parsed; if (v8::JSON::Parse(context, FromWtf8(isolate, in->u.str.data, in->u.str.len)).ToLocal(&parsed)) return parsed; return v8::Undefined(isolate); } default: return v8::Undefined(isolate); } } // Node's arrow header for an exception's throw site, recorded once on the // error object (as Node decorates an error the first time it is thrown): // ":\n\n", the caret under the // message's columns with tabs kept. Printed by the runtime when the error // ends the program uncaught (ErrorInfo's `arrow`). Local ArrowKey(Isolate *isolate) { return v8::Private::ForApi(isolate, Utf8(isolate, "scriptc.node-island.arrow")); } void RecordArrow(Isolate *isolate, Local context, const TryCatch &try_catch, Local exception) { if (!exception->IsObject()) return; Local error = exception.As(); Local key = ArrowKey(isolate); if (error->HasPrivate(context, key).FromMaybe(true)) return; // Node decorates from the error's own message (its construction site, // as Exception::CreateMessage answers for an Error with a stack), not // from where the throw left the call. Local message = v8::Exception::CreateMessage(isolate, exception); if (message.IsEmpty()) message = try_catch.Message(); if (message.IsEmpty()) return; Local source; if (!message->GetSourceLine(context).ToLocal(&source)) return; std::string line = *String::Utf8Value(isolate, source); std::string resource = *String::Utf8Value(isolate, message->GetScriptResourceName()); int linenum = message->GetLineNumber(context).FromMaybe(0); int start = message->GetStartColumn(context).FromMaybe(0); int end = message->GetEndColumn(context).FromMaybe(start + 1); std::string arrow = resource + ":" + std::to_string(linenum) + "\n" + line + "\n"; for (int i = 0; i < start; i++) arrow += (i < static_cast(line.size()) && line[i] == '\t') ? '\t' : ' '; for (int i = start; i < end || i == start; i++) arrow += '^'; (void)error->SetPrivate(context, key, Utf8(isolate, arrow.data(), arrow.size())); } // One entry from the runtime: the isolate scope, V8's stack limit for the // caller's stack, and the out-value scratch (reset by outermost entries). struct Op { Enter e; TryCatch try_catch; Op() : try_catch(e.isolate) { if (g_entry_depth++ == 0) { g_scratch.clear(); // Representatives V8 collected since: the runtime drops their exports // now (NI-8), whatever runs JavaScript next. The runtime holds nothing // of theirs while it starts an operation: their values were reachable // only through the table. FlushReleasedExports(); } } ~Op() { g_entry_depth--; } // The operation's outcome: 0 and the result, or 1 and the thrown value. int Finish(Local result, bool ok, ScrNodeValue *out) { if (ok) { FromV8(e.isolate, e.context, result, out); return 0; } if (try_catch.HasCaught() && !try_catch.HasTerminated()) { Local exception = try_catch.Exception(); RecordArrow(e.isolate, e.context, try_catch, exception); try_catch.Reset(); FromV8(e.isolate, e.context, exception, out); } else { std::memset(out, 0, sizeof *out); out->tag = SCR_NV_UNDEFINED; } return 1; } Local V(const ScrNodeValue *in) { return ToV8(e.isolate, e.context, in); } }; int Load(const char *path, int esm, ScrNodeValue *out) { Op op; Local argv[] = {Utf8(op.e.isolate, path), v8::Boolean::New(op.e.isolate, esm != 0)}; Local result; bool ok = g_load.Get(op.e.isolate) ->Call(op.e.context, v8::Undefined(op.e.isolate), 2, argv) .ToLocal(&result); return op.Finish(result, ok, out); } // The target of a property operation: an object, or a primitive whose // wrapper answers (ToObject throws for undefined and null). bool Target(Op &op, uint32_t h, Local *target) { return HandleValue(op.e.isolate, h)->ToObject(op.e.context).ToLocal(target); } bool Key(Op &op, const ScrNodeValue *key, Local *out) { Local k = op.V(key); if (k->IsString() || k->IsSymbol() || k->IsNumber()) { *out = k; return true; } Local name; if (!k->ToString(op.e.context).ToLocal(&name)) return false; *out = name; return true; } int Get(uint32_t h, const ScrNodeValue *key, ScrNodeValue *out) { Op op; Local target; Local k, result; bool ok = Target(op, h, &target) && Key(op, key, &k) && target->Get(op.e.context, k).ToLocal(&result); return op.Finish(result, ok, out); } int SetProp(uint32_t h, const ScrNodeValue *key, const ScrNodeValue *value, ScrNodeValue *out) { Op op; Local target; Local k; bool ok = Target(op, h, &target) && Key(op, key, &k); if (ok) { v8::Maybe done = target->Set(op.e.context, k, op.V(value)); ok = done.IsJust(); // Strict-mode semantics: a refused assignment throws, as in a module. if (ok && !done.FromJust()) { op.e.isolate->ThrowException(v8::Exception::TypeError( Utf8(op.e.isolate, "Cannot assign to read only property"))); ok = false; } } return op.Finish(v8::Undefined(op.e.isolate), ok, out); } int Has(uint32_t h, const ScrNodeValue *key, ScrNodeValue *out) { Op op; Local target = HandleValue(op.e.isolate, h); if (!target->IsObject()) { op.e.isolate->ThrowException(v8::Exception::TypeError( Utf8(op.e.isolate, "Cannot use 'in' operator to search for a key in a primitive"))); return op.Finish(Local(), false, out); } Local k; bool ok = Key(op, key, &k); v8::Maybe found = ok ? target.As()->Has(op.e.context, k) : v8::Nothing(); ok = found.IsJust(); return op.Finish(v8::Boolean::New(op.e.isolate, ok && found.FromJust()), ok, out); } int Del(uint32_t h, const ScrNodeValue *key, ScrNodeValue *out) { Op op; Local target; Local k; bool ok = Target(op, h, &target) && Key(op, key, &k); v8::Maybe done = ok ? target->Delete(op.e.context, k) : v8::Nothing(); ok = done.IsJust(); if (ok && !done.FromJust()) { op.e.isolate->ThrowException(v8::Exception::TypeError( Utf8(op.e.isolate, "Cannot delete property"))); ok = false; } return op.Finish(v8::Boolean::New(op.e.isolate, ok), ok, out); } std::vector> Args(Op &op, const ScrNodeValue *args, size_t argc) { std::vector> argv; argv.reserve(argc); for (size_t i = 0; i < argc; i++) argv.push_back(op.V(&args[i])); return argv; } void NotAFunction(Op &op, const char *what) { op.e.isolate->ThrowException(v8::Exception::TypeError( Utf8(op.e.isolate, (std::string(what) + " is not a function").c_str()))); } int Invoke(uint32_t fn, const ScrNodeValue *this_, const ScrNodeValue *args, size_t argc, ScrNodeValue *out) { Op op; Local callee = HandleValue(op.e.isolate, fn); if (!callee->IsFunction()) { NotAFunction(op, "value"); return op.Finish(Local(), false, out); } std::vector> argv = Args(op, args, argc); Local result; bool ok = callee.As() ->Call(op.e.context, op.V(this_), static_cast(argv.size()), argv.data()) .ToLocal(&result); return op.Finish(result, ok, out); } int InvokeMethod(uint32_t h, const ScrNodeValue *key, const ScrNodeValue *args, size_t argc, ScrNodeValue *out) { Op op; Local receiver = HandleValue(op.e.isolate, h); Local target; Local k, callee; if (!receiver->ToObject(op.e.context).ToLocal(&target) || !Key(op, key, &k) || !target->Get(op.e.context, k).ToLocal(&callee)) return op.Finish(Local(), false, out); if (!callee->IsFunction()) { std::string what = "property"; if (k->IsString()) what = ToStdString(op.e.isolate, k); NotAFunction(op, what.c_str()); return op.Finish(Local(), false, out); } std::vector> argv = Args(op, args, argc); Local result; bool ok = callee.As() ->Call(op.e.context, receiver, static_cast(argv.size()), argv.data()) .ToLocal(&result); return op.Finish(result, ok, out); } int Construct(uint32_t h, const ScrNodeValue *args, size_t argc, ScrNodeValue *out) { Op op; Local callee = HandleValue(op.e.isolate, h); if (!callee->IsFunction() || !callee.As()->IsConstructor()) { op.e.isolate->ThrowException(v8::Exception::TypeError( Utf8(op.e.isolate, "value is not a constructor"))); return op.Finish(Local(), false, out); } std::vector> argv = Args(op, args, argc); Local result; bool ok = callee.As() ->NewInstance(op.e.context, static_cast(argv.size()), argv.data()) .ToLocal(&result); return op.Finish(result, ok, out); } int InstanceOf(const ScrNodeValue *value, uint32_t ctor, ScrNodeValue *out) { Op op; Local v = op.V(value); Local c = HandleValue(op.e.isolate, ctor); if (!c->IsObject()) { op.e.isolate->ThrowException(v8::Exception::TypeError( Utf8(op.e.isolate, "Right-hand side of 'instanceof' is not callable"))); return op.Finish(Local(), false, out); } v8::Maybe is = v->InstanceOf(op.e.context, c.As()); bool ok = is.IsJust(); return op.Finish(v8::Boolean::New(op.e.isolate, ok && is.FromJust()), ok, out); } int OwnKeys(uint32_t h, int mode, ScrNodeValue *out) { Op op; Local target; Local keys; bool ok = Target(op, h, &target) && target ->GetOwnPropertyNames( op.e.context, mode == 0 ? static_cast(v8::ONLY_ENUMERABLE | v8::SKIP_SYMBOLS) : v8::ALL_PROPERTIES, v8::KeyConversionMode::kConvertToString) .ToLocal(&keys); return op.Finish(keys, ok, out); } void OnSettled(const FunctionCallbackInfo &info, int rejected) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); uint32_t token = info.Data()->Uint32Value(context).FromMaybe(0); ScrNodeValue value; FromV8(isolate, context, info[0], &value); g_host->settle_value(token, rejected, &value); } void OnFulfilled(const FunctionCallbackInfo &info) { OnSettled(info, 0); } void OnRejected(const FunctionCallbackInfo &info) { OnSettled(info, 1); } // await of a value: PromiseResolve (a native promise is itself) and one // reaction, as `await` does in JavaScript. int Then(const ScrNodeValue *value, uint32_t token, ScrNodeValue *out) { Op op; Local v = op.V(value); Local promise; if (v->IsPromise()) { promise = v.As(); } else { Local resolver; if (!v8::Promise::Resolver::New(op.e.context).ToLocal(&resolver) || resolver->Resolve(op.e.context, v).IsNothing()) return op.Finish(Local(), false, out); promise = resolver->GetPromise(); } Local data = Integer::NewFromUnsigned(op.e.isolate, token); Local fulfilled, rejected; Local chained; bool ok = Function::New(op.e.context, OnFulfilled, data).ToLocal(&fulfilled) && Function::New(op.e.context, OnRejected, data).ToLocal(&rejected) && promise->Then(op.e.context, fulfilled, rejected).ToLocal(&chained); return op.Finish(v8::Undefined(op.e.isolate), ok, out); } void ErrorInfo(uint32_t h, ScrNodeErrorInfo *out) { Op op; std::memset(out, 0, sizeof *out); Local v = HandleValue(op.e.isolate, h); if (!v->IsObject()) return; Local error = v.As(); static const struct { const char *name; uint32_t kind; } kinds[] = { {"TypeError", 1}, {"RangeError", 2}, {"SyntaxError", 3}, {"ReferenceError", 5}, {"EvalError", 6}, {"URIError", 7}}; for (const auto &k : kinds) { Local ctor = op.e.context->Global()->Get(op.e.context, Utf8(op.e.isolate, k.name)).ToLocalChecked(); if (error->InstanceOf(op.e.context, ctor.As()).FromMaybe(false)) { out->kind = k.kind; break; } } auto read = [&](const char *key, const char **data, size_t *len, bool optional) { Local value; if (!error->Get(op.e.context, Utf8(op.e.isolate, key)).ToLocal(&value)) { op.try_catch.Reset(); return; } if (optional && !value->IsString()) return; Local text; if (!value->ToString(op.e.context).ToLocal(&text)) { op.try_catch.Reset(); return; } *data = Scratch(Wtf8(op.e.isolate, text), len); }; read("name", &out->name, &out->name_len, false); read("message", &out->message, &out->message_len, false); read("stack", &out->stack, &out->stack_len, true); read("code", &out->code, &out->code_len, true); Local arrow; if (error->GetPrivate(op.e.context, ArrowKey(op.e.isolate)).ToLocal(&arrow) && arrow->IsString()) out->arrow = Scratch(Wtf8(op.e.isolate, arrow.As()), &out->arrow_len); } void DropHandle(uint32_t h) { if (!g_setup) return; Isolate *isolate = g_setup->isolate(); HandleScope handles(isolate); Drop(h); } // ── shared modules (NI-6) ────────────────────────────────────────────── // Natives of the loader hooks (scr_node_island_hooks.cjs): the table of // modules the program shares, the current value of one export, and the // exports still in their temporal dead zone. void SharedTable(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); HostCall scope; ScrNodeValue out; std::memset(&out, 0, sizeof out); g_host->shared_table(&out); info.GetReturnValue().Set(FromWtf8(isolate, out.u.str.data, out.u.str.len)); } void SharedTdz(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); HostCall scope; ScrNodeValue out; std::memset(&out, 0, sizeof out); g_host->shared_tdz(&out); info.GetReturnValue().Set(FromWtf8(isolate, out.u.str.data, out.u.str.len)); } // sharedGet(mod, slot, mode, tdz): the value, or `tdz` for a binding in its // temporal dead zone. void SharedGet(const FunctionCallbackInfo &info) { Isolate *isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); HostCall scope; ScrNodeValue out; std::memset(&out, 0, sizeof out); int status = g_host->shared_get(Uint32Arg(info, 0), Uint32Arg(info, 1), static_cast(Uint32Arg(info, 2)), &out); if (status == 3) { info.GetReturnValue().Set(info[3]); return; } Local result; ThrowOrReturn(isolate, context, status, &out, &result); if (!result.IsEmpty()) info.GetReturnValue().Set(result); } void SharedPush(uint32_t mod, uint32_t slot, const ScrNodeValue *value) { Enter e; Local argv[] = {Integer::NewFromUnsigned(e.isolate, mod), Integer::NewFromUnsigned(e.isolate, slot), ToV8(e.isolate, e.context, value)}; Helper(e, g_shared_push, 3, argv); } const ScrNodeBridge kBridge = { SCR_NODE_BRIDGE_ABI, NODE_VERSION_STRING, NODE_MODULE_VERSION, Start, Call, Release, HostLoop, Microtask, Tick, Timer, TimerClear, TimerRef, TimerRefresh, Immediate, ImmediateClear, ImmediateRef, Checkpoint, RunLoop, Emit, Stop, Load, Get, SetProp, Has, Del, Invoke, InvokeMethod, Construct, InstanceOf, OwnKeys, Then, ErrorInfo, DropHandle, LiveHandles, ExportSettle, SharedPush, GcStats, }; } // namespace #if defined(__linux__) // V8 asks v8::base::Stack for the start of the current thread's stack when // it records the bound of its conservative stack scan (isolate creation, // Locker). libnode calls these exported functions through its PLT, and this // library precedes libnode in the lookup scope, so the definitions below // answer for it: the runtime's stack while the caller runs on it, else // libnode's own answer (RTLD_NEXT). Without them a garbage collection that // scans the stack while JavaScript runs on the runtime's stack walks from // that stack up to the main thread's and faults in the unmapped gap. // macOS binds libnode's calls inside libnode instead: there the runtime // points the main thread's pthread stack record at its stack while Node // starts (ni_pthread_stack_enter in scr_node_island.c). #define SCR_V8_STACK_START(fn, symbol) \ extern "C" __attribute__((visibility("default"))) void *fn() __asm__(symbol); \ extern "C" void *fn() { \ static void *(*next)() = nullptr; \ uintptr_t start = HostStackStart(); \ if (start != 0) return reinterpret_cast(start); \ if (next == nullptr) next = reinterpret_cast(dlsym(RTLD_NEXT, symbol)); \ return next != nullptr ? next() : nullptr; \ } SCR_V8_STACK_START(ScrV8GetStackStart, "_ZN2v84base5Stack13GetStackStartEv") SCR_V8_STACK_START(ScrV8GetStackStartUnchecked, "_ZN2v84base5Stack22GetStackStartUncheckedEv") SCR_V8_STACK_START(ScrV8ObtainCurrentThreadStackStart, "_ZN2v84base5Stack29ObtainCurrentThreadStackStartEv") #undef SCR_V8_STACK_START #endif extern "C" __attribute__((visibility("default"))) const ScrNodeBridge *scr_node_bridge_v1() { return &kBridge; }