Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 47 additions & 14 deletions NativeScript/runtime/BuiltinLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,30 @@ constexpr const char* kRequireParamName = "require";
constexpr const char* kModuleParamName = "module";
constexpr const char* kBindingParamName = "binding";
constexpr const char* kPrimordialsParamName = "primordials";
constexpr int kParamCount = 5;
constexpr const char* kInternalsParamName = "internals";
constexpr int kParamCount = 6;

// Per-isolate `internals` object handed to every builtin: the private
// channel for cross-builtin capabilities (hook keys, setters) that must
// never reach app code. Producers publish during their init, consumers read
// during theirs, so Runtime::Init's ordering is the dependency graph.
struct BuiltinInternalsState {
Persistent<Object> internals;
};

MaybeLocal<Object> GetInternals(Local<Context> context) {
Isolate* isolate = v8::Isolate::GetCurrent();
auto* state = Caches::StateFor<BuiltinInternalsState>(isolate);
if (state == nullptr) {
return MaybeLocal<Object>();
}
if (!state->internals.IsEmpty()) {
return state->internals.Get(isolate);
}
Local<Object> internals = Object::New(isolate);
state->internals.Reset(isolate, internals);
return internals;
}

// The `require` every builtin receives: builtin specifiers only, so a builtin
// can never reach application code or the filesystem.
Expand Down Expand Up @@ -94,12 +117,12 @@ MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
);
Local<v8::String> sourceText = tns::ToV8String(
isolate, builtin.source, static_cast<int>(builtin.length));
Local<v8::String> params[] = {
tns::ToV8String(isolate, kExportsParamName),
tns::ToV8String(isolate, kRequireParamName),
tns::ToV8String(isolate, kModuleParamName),
tns::ToV8String(isolate, kBindingParamName),
tns::ToV8String(isolate, kPrimordialsParamName)};
Local<v8::String> params[] = {tns::ToV8String(isolate, kExportsParamName),
tns::ToV8String(isolate, kRequireParamName),
tns::ToV8String(isolate, kModuleParamName),
tns::ToV8String(isolate, kBindingParamName),
tns::ToV8String(isolate, kPrimordialsParamName),
tns::ToV8String(isolate, kInternalsParamName)};

Local<v8::Function> fn;
if (!blob.empty()) {
Expand Down Expand Up @@ -140,7 +163,8 @@ MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
}

MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id,
Local<Value> binding, Local<Value> primordials) {
Local<Value> binding, Local<Value> primordials,
Local<Object> internals) {
Isolate* isolate = v8::Isolate::GetCurrent();

Local<v8::Function> fn;
Expand All @@ -161,9 +185,12 @@ MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id,
}

Local<Value> args[] = {
exportsObj, require, moduleObj,
exportsObj,
require,
moduleObj,
binding.IsEmpty() ? v8::Undefined(isolate).As<Value>() : binding,
primordials};
primordials,
internals};
if (fn->Call(context, v8::Undefined(isolate), kParamCount, args).IsEmpty()) {
return MaybeLocal<Value>();
}
Expand All @@ -175,7 +202,8 @@ MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id,
// isolate — during runtime init, before user code can replace a global.
// Builtins compiled later in the isolate's life get the same pristine
// snapshot.
MaybeLocal<Object> GetPrimordials(Local<Context> context) {
MaybeLocal<Object> GetPrimordials(Local<Context> context,
Local<Object> internals) {
Isolate* isolate = v8::Isolate::GetCurrent();
std::shared_ptr<Caches> cache = Caches::Get(isolate);
if (cache->Primordials != nullptr) {
Expand All @@ -184,7 +212,7 @@ MaybeLocal<Object> GetPrimordials(Local<Context> context) {

Local<Value> result;
if (!CallBuiltin(context, BuiltinId::kPrimordials, Local<Value>(),
v8::Undefined(isolate))
v8::Undefined(isolate), internals)
.ToLocal(&result) ||
!result->IsObject()) {
return MaybeLocal<Object>();
Expand All @@ -201,12 +229,17 @@ MaybeLocal<Object> GetPrimordials(Local<Context> context) {
MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context,
BuiltinId id,
Local<Value> binding) {
Local<Object> internals;
if (!GetInternals(context).ToLocal(&internals)) {
return MaybeLocal<Value>();
}

Local<Object> primordials;
if (!GetPrimordials(context).ToLocal(&primordials)) {
if (!GetPrimordials(context, internals).ToLocal(&primordials)) {
return MaybeLocal<Value>();
}

return CallBuiltin(context, id, binding, primordials);
return CallBuiltin(context, id, binding, primordials, internals);
}

} // namespace tns
14 changes: 9 additions & 5 deletions NativeScript/runtime/BuiltinLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ class BuiltinLoader {
public:
// Compiles the builtin identified by id as a function body with the fixed
// parameters `exports`, `require`, `module`, `binding` (Node's module wrapper
// plus its internalBinding idiom) and `primordials`, calls it with the given
// bag of natives (or undefined when omitted) plus this isolate's frozen
// intrinsics snapshot, and returns the resulting `module.exports`. `require`
// reaches the builtin modules (NsBuiltinModules) and nothing else. The
// snapshot is
// plus its internalBinding idiom), `primordials` and `internals`, calls it
// with the given bag of natives (or undefined when omitted), this isolate's
// frozen intrinsics snapshot, and the isolate's shared internals object, and
// returns the resulting `module.exports`. `require` reaches the builtin
// modules (NsBuiltinModules) and nothing else. `internals` is one plain
// object per isolate handed identically to every builtin and never exposed
// anywhere app code can reach: the channel for cross-builtin capabilities
// (see the js README; interim until a Node-style private internal-module
// tier exists). The snapshot is
// produced by the kPrimordials builtin on first use and cached per isolate,
// so it is taken before any user code can replace a global. Scripts carry
// an "internal/<name>.js" origin so runtime
Expand Down
9 changes: 9 additions & 0 deletions NativeScript/runtime/Events.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ void Events::Init(Local<Context> context) {
auto cache = Caches::Get(isolate);
cache->GlobalEventTarget =
std::make_unique<Persistent<v8::Object>>(isolate, result.As<Object>());

// AbortController/AbortSignal (internal/abort-signal.js) build directly on
// the event primitives installed above; the listener-mutation hook key for
// its GC-liveness accounting arrives through the shared `internals`
// parameter, published by the events builtin.
Local<Value> abortResult;
success = BuiltinLoader::RunBuiltin(context, BuiltinId::kAbortSignal)
.ToLocal(&abortResult);
tns::Assert(success, isolate);
}

} // namespace tns
5 changes: 3 additions & 2 deletions NativeScript/runtime/Events.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ class Events {
public:
// Installs the generic WHATWG event primitives: the Event and EventTarget
// constructors on globalThis, the EventTarget methods (addEventListener /
// removeEventListener / dispatchEvent) bound onto globalThis, and the
// internal EventTarget instance backing the global. Evaluated once per
// removeEventListener / dispatchEvent) bound onto globalThis, the
// internal EventTarget instance backing the global, and — layered on top —
// the AbortController/AbortSignal interfaces. Evaluated once per
// isolate during Runtime::Init, right after PromiseProxy::Init and before
// ErrorEvents::Init, for both main and worker isolates. Stashes the backing
// target in Caches->GlobalEventTarget so native layers can dispatch without
Expand Down
16 changes: 14 additions & 2 deletions NativeScript/runtime/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ at runtime `BuiltinLoader::RunBuiltin` compiles and executes them with an
## Contract (Node's module wrapper + internalBinding idiom)

Every file is compiled as a **function body** via `v8::ScriptCompiler::CompileFunction`
with the fixed parameters `exports`, `require`, `module`, `binding` and
`primordials`:
with the fixed parameters `exports`, `require`, `module`, `binding`,
`primordials` and `internals`:

```js
const { someNative, anotherNative } = binding;
Expand All @@ -29,6 +29,18 @@ module.exports = somethingTheCallSiteNeeds;
Requiring a module that is still loading throws rather than recursing.
- `primordials` is the frozen intrinsics snapshot built by `primordials.js`
(see below), the same object for every builtin in an isolate.
- `internals` is one plain per-isolate object handed identically to every
builtin and reachable from nowhere else — the private channel for
cross-builtin capabilities that must never leak to app code (the
`kListenerChanged` hook key events.js publishes for abort-signal.js, the
`setListenerErrorReporter` setter error-events.js calls). Producers
publish during their init, consumers read during theirs, so the
`Runtime::Init` ordering is the dependency graph; a missing key fails
loudly at init, not at first use. **Interim mechanism**: if cross-builtin
needs outgrow one shared object (many producers, lazy consumers), migrate
to a Node-style private internal-module tier — `require("internal/…")`
resolved for builtins only, never through the public `ns:`/`node:`
registry — and fold `internals` into it.
- **`module.exports` is the export channel** — whatever it holds when the file
finishes is what `RunBuiltin` hands back to C++ (used for factory functions
and init results). Both CommonJS styles work: replace the whole export with
Expand Down
Loading
Loading