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
69 changes: 19 additions & 50 deletions NativeScript/runtime/BuiltinLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ constexpr const char* kRequireParamName = "require";
constexpr const char* kModuleParamName = "module";
constexpr const char* kBindingParamName = "binding";
constexpr const char* kPrimordialsParamName = "primordials";
constexpr const char* kInternalsParamName = "internals";
constexpr int kParamCount = 6;
constexpr int kParamCount = 5;

// `module.exports` of every builtin that has run in this isolate, indexed by
// id. Per isolate because a builtin is a singleton per realm, so workers run
Expand All @@ -38,30 +37,9 @@ struct BuiltinExportsState {
Persistent<Object> exports[static_cast<unsigned>(BuiltinId::kCount)];
};

// 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.
// The `require` every builtin receives: builtin specifiers only — including
// the internal tier app code can never name — so a builtin can never reach
// application code or the filesystem.
void BuiltinRequireCallback(const FunctionCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
if (info.Length() < 1 || !info[0]->IsString()) {
Expand All @@ -75,7 +53,8 @@ void BuiltinRequireCallback(const FunctionCallbackInfo<Value>& info) {
Local<Object> exports;
if (NsBuiltinModules::GetExports(context, specifier).ToLocal(&exports)) {
info.GetReturnValue().Set(exports);
} else if (!NsBuiltinModules::IsRegistered(specifier)) {
} else if (!NsBuiltinModules::IsRegistered(specifier,
/* includeInternal */ true)) {
isolate->ThrowException(Exception::Error(tns::ToV8String(
isolate, NsBuiltinModules::NotFoundMessage(specifier))));
}
Expand Down Expand Up @@ -124,12 +103,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),
tns::ToV8String(isolate, kInternalsParamName)};
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::Function> fn;
if (!blob.empty()) {
Expand Down Expand Up @@ -170,8 +149,7 @@ MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
}

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

Local<v8::Function> fn;
Expand All @@ -192,12 +170,9 @@ 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,
internals};
primordials};
if (fn->Call(context, v8::Undefined(isolate), kParamCount, args).IsEmpty()) {
return MaybeLocal<Value>();
}
Expand All @@ -209,8 +184,7 @@ 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,
Local<Object> internals) {
MaybeLocal<Object> GetPrimordials(Local<Context> context) {
Isolate* isolate = v8::Isolate::GetCurrent();
std::shared_ptr<Caches> cache = Caches::Get(isolate);
if (cache->Primordials != nullptr) {
Expand All @@ -219,7 +193,7 @@ MaybeLocal<Object> GetPrimordials(Local<Context> context,

Local<Value> result;
if (!CallBuiltin(context, BuiltinId::kPrimordials, Local<Value>(),
v8::Undefined(isolate), internals)
v8::Undefined(isolate))
.ToLocal(&result) ||
!result->IsObject()) {
return MaybeLocal<Object>();
Expand All @@ -236,17 +210,12 @@ 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, internals).ToLocal(&primordials)) {
if (!GetPrimordials(context).ToLocal(&primordials)) {
return MaybeLocal<Value>();
}

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

MaybeLocal<Object> BuiltinLoader::GetExports(Local<Context> context,
Expand Down
17 changes: 7 additions & 10 deletions NativeScript/runtime/BuiltinLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,13 @@ class BuiltinLoader {
using BindingFactory = v8::MaybeLocal<v8::Object> (*)(v8::Local<v8::Context>);

// 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), `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
// 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) and this isolate's
// frozen intrinsics snapshot, and returns the resulting `module.exports`.
// `require` reaches the builtin modules (NsBuiltinModules) — including the
// internal-only tier, which is also how builtins hand each other
// capabilities app code must not see — and nothing else. 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
32 changes: 20 additions & 12 deletions NativeScript/runtime/Events.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,34 @@ namespace tns {

void Events::Init(Local<Context> context) {
// Generic WHATWG event primitives (internal/events.js). The builtin installs
// Event/EventTarget and the global EventTarget methods, then exports the
// internal EventTarget instance backing the global so native dispatch
// survives app code overwriting globalThis.dispatchEvent. The error-events
// layer (ErrorEvents::Init) runs immediately after and installs the native
// Event/EventTarget and the global EventTarget methods; its exports carry
// the internal EventTarget instance backing the global (cached here so
// native dispatch survives app code overwriting globalThis.dispatchEvent)
// and the CustomEvent interface the lazy-global tier places. Run through
// GetExports so that tier's read shares this run. The error-events layer
// (ErrorEvents::Init) runs immediately after and installs the native
// listener-error reporter through _installListenerErrorReporter.
Isolate* isolate = v8::Isolate::GetCurrent();

Local<Value> result;
bool success =
BuiltinLoader::RunBuiltin(context, BuiltinId::kEvents).ToLocal(&result);
tns::Assert(success && result->IsObject(), isolate);
Local<Object> exports;
bool success = BuiltinLoader::GetExports(context, BuiltinId::kEvents, nullptr)
.ToLocal(&exports);
tns::Assert(success, isolate);

Local<Value> globalEventTarget;
success = exports->Get(context, tns::ToV8String(isolate, "globalEventTarget"))
.ToLocal(&globalEventTarget) &&
globalEventTarget->IsObject();
tns::Assert(success, isolate);

auto cache = Caches::Get(isolate);
cache->GlobalEventTarget =
std::make_unique<Persistent<v8::Object>>(isolate, result.As<Object>());
cache->GlobalEventTarget = std::make_unique<Persistent<v8::Object>>(
isolate, globalEventTarget.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.
// its GC-liveness accounting comes from the events builtin's exports, via
// require("internal/events").
Local<Value> abortResult;
success = BuiltinLoader::RunBuiltin(context, BuiltinId::kAbortSignal)
.ToLocal(&abortResult);
Expand Down
12 changes: 12 additions & 0 deletions NativeScript/runtime/LazyGlobals.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "LazyGlobals.h"

#include "Base64.h"
#include "BuiltinLoader.h"
#include "Helpers.h"
#include "TextEncoding.h"

Expand All @@ -21,11 +22,22 @@ struct LazyGlobalEntry {
ExportsAccessor exports;
};

// Exports accessor for a builtin with no natives of its own; modules with a
// binding (TextEncoding, Base64) own a hand-written accessor instead.
template <BuiltinId id>
MaybeLocal<Object> BuiltinExports(Local<Context> context) {
return BuiltinLoader::GetExports(context, id, nullptr);
}

constexpr LazyGlobalEntry kLazyGlobals[] = {
{"TextEncoder", "TextEncoder", TextEncoding::GetExports},
{"TextDecoder", "TextDecoder", TextEncoding::GetExports},
{"atob", "atob", Base64::GetExports},
{"btoa", "btoa", Base64::GetExports},
{"DOMException", "DOMException", BuiltinExports<BuiltinId::kDomException>},
// events.js is an eager builtin (Events::Init), so this row never runs a
// file: the read hits the exports cache and only the placement is lazy.
{"CustomEvent", "CustomEvent", BuiltinExports<BuiltinId::kEvents>},
};

void LazyGlobalGetter(Local<v8::Name> property,
Expand Down
5 changes: 3 additions & 2 deletions NativeScript/runtime/LazyGlobals.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ namespace tns {
// plain data property so later reads cost nothing.
//
// A builtin behind this tier runs at an arbitrary point in the isolate's life
// rather than during init, so it may only consume `internals` keys published
// by eager builtins (see NativeScript/runtime/js/README.md).
// rather than during init, so anything it needs from a sibling builtin must
// come through `require` or its `binding`, never from init order (see
// NativeScript/runtime/js/README.md).
class LazyGlobals {
public:
// Registers every lazy global. Must run before Context::New, on the same
Expand Down
28 changes: 22 additions & 6 deletions NativeScript/runtime/NsBuiltinModules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,25 @@ struct Registration {
// Natives the file receives as `binding`, null when it needs none (every
// `node:` shim, which reaches its `ns:` module through require instead).
BuiltinLoader::BindingFactory binding;
// The internal tier (runtime/js/README.md): resolvable only through the
// require builtins receive, never through the module system, so the file's
// exports can carry capabilities app code must not reach.
bool internalOnly = false;
};

// The public registry (docs/ns-builtin-modules.md). One specifier, one source
// file: a `node:` shim is its own builtin that requires the `ns:` module it
// adapts, so the two module objects stay distinct and the standard module
// never carries compatibility code.
// The public registry (docs/ns-builtin-modules.md) plus the internal tier.
// One specifier, one source file: a `node:` shim is its own builtin that
// requires the `ns:` module it adapts, so the two module objects stay
// distinct and the standard module never carries compatibility code.
constexpr Registration kRegistry[] = {
{"ns:module", BuiltinId::kNsModule, NsModuleBinding},
{"ns:runtime", BuiltinId::kNsRuntime, NsRuntimeBinding},
{"ns:util", BuiltinId::kNsUtil, NsUtilBinding},
{"node:module", BuiltinId::kNodeModule, nullptr},
{"node:url", BuiltinId::kNodeUrl, nullptr},
{"node:util", BuiltinId::kNodeUtil, nullptr},
{"internal/dom-exception", BuiltinId::kDomException, nullptr, true},
{"internal/events", BuiltinId::kEvents, nullptr, true},
};

// ns:runtime config keys. Each key defines its value domain and scope here;
Expand Down Expand Up @@ -299,8 +305,11 @@ bool NsBuiltinModules::IsNsScheme(const std::string& specifier) {
return HasPrefix(specifier, kNsPrefix);
}

bool NsBuiltinModules::IsRegistered(const std::string& specifier) {
return Find(specifier) != nullptr;
bool NsBuiltinModules::IsRegistered(const std::string& specifier,
bool includeInternal) {
const Registration* registration = Find(specifier);
return registration != nullptr &&
(includeInternal || !registration->internalOnly);
}

std::string NsBuiltinModules::NotFoundMessage(const std::string& specifier) {
Expand Down Expand Up @@ -348,6 +357,13 @@ MaybeLocal<Object> NsBuiltinModules::GetExports(Local<Context> context,

MaybeLocal<Module> NsBuiltinModules::GetModule(Local<Context> context,
const std::string& specifier) {
// The module system is the app-facing entry point; the internal tier only
// exists behind the require builtins receive.
const Registration* registration = Find(specifier);
if (registration == nullptr || registration->internalOnly) {
return MaybeLocal<Module>();
}

Isolate* isolate = v8::Isolate::GetCurrent();
std::shared_ptr<Caches> cache = Caches::Get(isolate);

Expand Down
6 changes: 5 additions & 1 deletion NativeScript/runtime/NsBuiltinModules.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ class NsBuiltinModules {
static bool IsNsScheme(const std::string& specifier);

// Whether a module of that name exists.
static bool IsRegistered(const std::string& specifier);
// Whether the specifier names a registered builtin module. Internal-tier
// rows (see kRegistry) only count when includeInternal is set, which is the
// builtin require's privilege; every app-facing caller keeps the default.
static bool IsRegistered(const std::string& specifier,
bool includeInternal = false);

// Frozen exports object of a registered specifier. Empty when the specifier
// is not registered (nothing thrown) or when the module failed to build (an
Expand Down
31 changes: 31 additions & 0 deletions NativeScript/runtime/StructuredSerialization.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "StructuredSerialization.h"

#include "BuiltinLoader.h"
#include "Helpers.h"
#include "NativeScriptException.h"

Expand All @@ -9,6 +10,36 @@ namespace tns {
namespace serialization {

void ThrowDataCloneError(Isolate* isolate, const std::string& message) {
// The spec's DataCloneError is a DOMException; build it through the
// builtin's exports cache so native and JS throw sites produce the same
// class. Delegates may call into JS here — V8 allows it, and Node's
// serializer delegates do the same. The fallback covers a builtin that can
// no longer run (isolate teardown, broken realm).
Local<Object> domException;
{
TryCatch tc(isolate);
Local<Context> context = isolate->GetCurrentContext();
Local<Object> exports;
Local<Value> ctor;
if (BuiltinLoader::GetExports(context, BuiltinId::kDomException, nullptr)
.ToLocal(&exports) &&
exports->Get(context, tns::ToV8String(isolate, "DOMException"))
.ToLocal(&ctor) &&
ctor->IsFunction()) {
Local<Value> args[] = {tns::ToV8String(isolate, message),
tns::ToV8String(isolate, "DataCloneError")};
Local<Object> instance;
if (ctor.As<v8::Function>()
->NewInstance(context, 2, args)
.ToLocal(&instance)) {
domException = instance;
}
}
}
if (!domException.IsEmpty()) {
isolate->ThrowException(domException);
return;
}
NativeScriptException exception(isolate, message, "DataCloneError");
exception.ReThrowToV8(isolate);
}
Expand Down
6 changes: 3 additions & 3 deletions NativeScript/runtime/StructuredSerialization.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ enum class HostObjectPolicy {
kDegrade,
};

// Throws the runtime's DataCloneError. There is no DOMException here, so it is
// an Error carrying that name — routed through NativeScriptException so the
// object is shaped like every other error the runtime raises.
// Throws a "DataCloneError" DOMException, the same class the JS half of
// structuredClone raises. Falls back to a NativeScriptException-shaped Error
// carrying that name when the dom-exception builtin cannot run.
void ThrowDataCloneError(v8::Isolate* isolate, const std::string& message);

// A value serialized out of one isolate, plus the memory that travels with it.
Expand Down
Loading
Loading