Skip to content
Open
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
65 changes: 53 additions & 12 deletions NativeScript/runtime/BuiltinLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <mutex>
#include <vector>

#include "Caches.h"
#include "Helpers.h"

using namespace v8;
Expand All @@ -17,12 +18,14 @@ std::vector<uint8_t> builtinCache[static_cast<unsigned>(BuiltinId::kCount)];

// Every builtin is compiled as a function body receiving these fixed
// parameters, mirroring Node's module wrapper: a file exports through
// `module.exports`/`exports`, and natives arrive as properties of the
// `binding` bag (Node's internalBinding idiom) for each file to destructure.
// `module.exports`/`exports`, natives arrive as properties of the `binding`
// bag (Node's internalBinding idiom) and intrinsics as properties of
// `primordials`; each file destructures what it needs.
constexpr const char* kExportsParamName = "exports";
constexpr const char* kModuleParamName = "module";
constexpr const char* kBindingParamName = "binding";
constexpr int kParamCount = 3;
constexpr const char* kPrimordialsParamName = "primordials";
constexpr int kParamCount = 4;

MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
Isolate* isolate = v8::Isolate::GetCurrent();
Expand All @@ -49,9 +52,11 @@ 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, kModuleParamName),
tns::ToV8String(isolate, kBindingParamName)};
Local<v8::String> params[] = {
tns::ToV8String(isolate, kExportsParamName),
tns::ToV8String(isolate, kModuleParamName),
tns::ToV8String(isolate, kBindingParamName),
tns::ToV8String(isolate, kPrimordialsParamName)};

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

} // namespace

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

Local<v8::Function> fn;
Expand All @@ -112,12 +114,51 @@ MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context,

Local<Value> args[] = {
exportsObj, moduleObj,
binding.IsEmpty() ? v8::Undefined(isolate).As<Value>() : binding};
binding.IsEmpty() ? v8::Undefined(isolate).As<Value>() : binding,
primordials};
if (fn->Call(context, v8::Undefined(isolate), kParamCount, args).IsEmpty()) {
return MaybeLocal<Value>();
}

return moduleObj->Get(context, exportsKey);
}

// Snapshot of the intrinsics, taken the first time any builtin runs in this
// isolate — during runtime init, before user code can replace a global. Later
// builtins (smart-stringify compiles lazily, on the first object logged) get
// the same pristine snapshot.
MaybeLocal<Object> GetPrimordials(Local<Context> context) {
Isolate* isolate = v8::Isolate::GetCurrent();
std::shared_ptr<Caches> cache = Caches::Get(isolate);
if (cache->Primordials != nullptr) {
return cache->Primordials->Get(isolate);
}

Local<Value> result;
if (!CallBuiltin(context, BuiltinId::kPrimordials, Local<Value>(),
v8::Undefined(isolate))
.ToLocal(&result) ||
!result->IsObject()) {
return MaybeLocal<Object>();
}

Local<Object> primordials = result.As<Object>();
cache->Primordials =
std::make_unique<Persistent<Object>>(isolate, primordials);
return primordials;
}

} // namespace

MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context,
BuiltinId id,
Local<Value> binding) {
Local<Object> primordials;
if (!GetPrimordials(context).ToLocal(&primordials)) {
return MaybeLocal<Value>();
}

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

} // namespace tns
11 changes: 7 additions & 4 deletions NativeScript/runtime/BuiltinLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ namespace tns {
class BuiltinLoader {
public:
// Compiles the builtin identified by id as a function body with the fixed
// parameters `exports`, `module` and `binding` (Node's module wrapper plus
// its internalBinding idiom), calls it with the given bag of natives (or
// undefined when omitted), and returns the resulting `module.exports`.
// Scripts carry an "internal/<name>.js" origin so runtime
// parameters `exports`, `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`. 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
// frames are identifiable in stack traces. Compilation goes through a
// process-wide bytecode cache: the first run in the process compiles
// eagerly and populates the cache, later isolates (workers) consume it
Expand Down
6 changes: 6 additions & 0 deletions NativeScript/runtime/Caches.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ class Caches {
std::unique_ptr<v8::Persistent<v8::Function>> UnmanagedTypeCtorFunc =
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);

// Frozen intrinsics snapshot returned by internal/primordials.js, passed to
// every builtin as its second fixed parameter (BuiltinLoader::RunBuiltin).
// Per isolate, so workers snapshot their own realm's intrinsics.
std::unique_ptr<v8::Persistent<v8::Object>> Primordials =
std::unique_ptr<v8::Persistent<v8::Object>>(nullptr);

// Internal EventTarget instance backing the global, returned by the generic
// event-primitives bootstrap IIFE (Events::Init). Holds the real listener
// store, so native layers dispatch through it without going through
Expand Down
49 changes: 42 additions & 7 deletions NativeScript/runtime/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ 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`, `module` and `binding`:
with the fixed parameters `exports`, `module`, `binding` and `primordials`:

```js
const { someNative, anotherNative } = binding;
const { ArrayPrototypeSlice, ObjectCreate } = primordials;

module.exports = somethingTheCallSiteNeeds;
```

- `binding` is a plain object of natives built by the C++ call site; a file
that needs nothing from C++ simply doesn't mention it.
- `primordials` is the frozen intrinsics snapshot built by `primordials.js`
(see below), the same object for every builtin in an isolate.
- **`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 All @@ -28,20 +31,52 @@ module.exports = somethingTheCallSiteNeeds;
every tool that isn't reading this repo's ESLint config (editors' TS server,
prettier, review bots) rejects the file as invalid JavaScript.
- Strict mode is per-file: start the file with `"use strict";` to opt in.
- Destructure `binding` once, at the top of the file, so the file's native
dependencies are visible and greppable.
- Destructure `binding` and `primordials` once, at the top of the file, so the
file's dependencies are visible and greppable.

## Rules

- Run at isolate init, before any user code: capture any global you rely on
(e.g. `globalThis.Event`) eagerly so later monkey-patching can't break you.
(Planned: a frozen `primordials` namespace as a second fixed parameter,
Node-style, to make this systematic for closures that outlive init.)
For intrinsics that is what `primordials` is; for everything else
(`URLSearchParams`, …) capture it into a file-level `const`.
- No `import`/`export` — these are classic function bodies, not modules.
- ESLint (`eslint.config.mjs` at the repo root, run by lint-staged) declares
`exports`, `module`, `binding` and the reachable native globals; `no-undef`
is the typo net. If a builtin starts using a new native global, add it there.
`exports`, `module`, `binding`, `primordials` and the reachable native
globals; `no-undef` is the typo net. If a builtin starts using a new native
global, add it there. `no-restricted-properties` fails the lint on direct use
of the captured statics (`JSON.stringify`, `Object.defineProperty`, …).
Uncurried instance methods can't be matched that way, so `list.slice()`
instead of `ArrayPrototypeSlice(list)` is caught by review, not by the
linter.
- File names are kebab-case; the name determines the `BuiltinId` enum value
(`promise-proxy.js` → `kPromiseProxy`) and the script origin. New files must
also be added to `tools/js2c-inputs.xcfilelist` — the build fails with an
explicit message if that list drifts out of sync (`js2c.mjs --filelist`).

## primordials

`primordials.js` runs first in every isolate — lazily, on the first
`RunBuiltin` call, which happens during runtime init — and its frozen,
null-prototype export is cached per isolate (`Caches::Primordials`) and
handed to every other builtin. Builtins that compile late (`smart-stringify`
is compiled on the first object logged) therefore still see intrinsics as they
were before user code ran.

Naming follows Node: statics keep their path (`JSONStringify`,
`ObjectDefineProperty`), instance methods are **uncurried** so the receiver
becomes the first argument:

```js
ArrayPrototypeSlice(list, 1) // not list.slice(1)
FunctionPrototypeCall(cb, this, event) // not cb.call(this, event)
```

Uncurrying is `Function.prototype.bind.bind(Function.prototype.call)`, which on
the jitless configuration the runtime ships is both faster than a captured
`fn.call(...)` and immune to a replaced `Function.prototype.call`.

Add only what a builtin actually needs; this is not a mirror of Node's list.
Plain constructor calls made once at init time (`new Map()` while
bootstrapping) may stay direct — the rule targets code in closures that
outlive init.
29 changes: 22 additions & 7 deletions NativeScript/runtime/js/blob-url.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
const {
Map,
MapPrototypeDelete,
MapPrototypeGet,
MapPrototypeSet,
ObjectDefineProperty,
StringPrototypeToLowerCase,
} = primordials;

// The searchParams accessor below outlives init, so the constructor it reaches
// for is captured now rather than looked up on the global at call time.
const URLSearchParamsCtor = URLSearchParams;

const BLOB_STORE = new Map();
URL.createObjectURL = function (object, options = null) {
try {
// Blob/File come from the app layer, not the runtime, so these stay
// live lookups; the catch below covers them not existing yet.
if (object instanceof Blob || object instanceof File) {
const id = NSUUID.UUID().UUIDString.toLowerCase();
const id = StringPrototypeToLowerCase(NSUUID.UUID().UUIDString);
const ret = `blob:nativescript/${id}`;
BLOB_STORE.set(ret, {
MapPrototypeSet(BLOB_STORE, ret, {
blob: object,
type: object?.type,
ext: options?.ext,
Expand All @@ -17,18 +32,18 @@ URL.createObjectURL = function (object, options = null) {
return null;
};
URL.revokeObjectURL = function (url) {
BLOB_STORE.delete(url);
MapPrototypeDelete(BLOB_STORE, url);
};
const InternalAccessor = class {};
InternalAccessor.getData = function (url) {
return BLOB_STORE.get(url);
return MapPrototypeGet(BLOB_STORE, url);
};
URL.InternalAccessor = InternalAccessor;
Object.defineProperty(URL.prototype, 'searchParams', {
ObjectDefineProperty(URL.prototype, 'searchParams', {
get() {
if (this._searchParams == null) {
this._searchParams = new URLSearchParams(this.search);
Object.defineProperty(this._searchParams, '_url', {
this._searchParams = new URLSearchParamsCtor(this.search);
ObjectDefineProperty(this._searchParams, '_url', {
enumerable: false,
writable: false,
value: this,
Expand Down
9 changes: 5 additions & 4 deletions NativeScript/runtime/js/class-extends.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
function __extends(d, b) {
const { ObjectCreate, ObjectPrototypeHasOwnProperty } = primordials;
function __extends(d, b) {
for (var p in b) {
if (b.hasOwnProperty(p)) {
if (ObjectPrototypeHasOwnProperty(b, p)) {
d[p] = b[p];
}
}
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) :
d.prototype = b === null ? ObjectCreate(b) :
(__.prototype = b.prototype, new __());
}
}
module.exports = __extends;
9 changes: 5 additions & 4 deletions NativeScript/runtime/js/error-events.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
"use strict";

const { globalTarget, nativeReportFatal } = binding;
const { FunctionPrototypeCall, ObjectCreate, String, TypeError } = primordials;
var g = globalThis;
var Event = g.Event;

function ErrorEvent(type, opts) {
opts = opts || {};
Event.call(this, type, opts);
FunctionPrototypeCall(Event, this, type, opts);
this.message = opts.message !== undefined ? String(opts.message) : "";
this.filename = opts.filename !== undefined ? String(opts.filename) : "";
this.lineno = opts.lineno !== undefined ? (opts.lineno | 0) : 0;
this.colno = opts.colno !== undefined ? (opts.colno | 0) : 0;
this.error = opts.error !== undefined ? opts.error : null;
}
ErrorEvent.prototype = Object.create(Event.prototype);
ErrorEvent.prototype = ObjectCreate(Event.prototype);
ErrorEvent.prototype.constructor = ErrorEvent;

function PromiseRejectionEvent(type, opts) {
opts = opts || {};
Event.call(this, type, opts);
FunctionPrototypeCall(Event, this, type, opts);
this.promise = opts.promise;
this.reason = opts.reason;
}
PromiseRejectionEvent.prototype = Object.create(Event.prototype);
PromiseRejectionEvent.prototype = ObjectCreate(Event.prototype);
PromiseRejectionEvent.prototype.constructor = PromiseRejectionEvent;

// A listener that throws must not stop other listeners: route the thrown
Expand Down
25 changes: 17 additions & 8 deletions NativeScript/runtime/js/events.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
"use strict";
const {
ArrayPrototypeIndexOf,
ArrayPrototypePush,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
FunctionPrototypeCall,
ObjectCreate,
String,
} = primordials;
var g = globalThis;

function Event(type, opts) {
Expand Down Expand Up @@ -29,7 +38,7 @@ Event.prototype.stopImmediatePropagation = function () {
// user code runs); until then a thrown listener is swallowed.
var reportListenerError = function (e) {};

function EventTargetImpl() { this._listeners = Object.create(null); }
function EventTargetImpl() { this._listeners = ObjectCreate(null); }
EventTargetImpl.prototype.addEventListener = function (type, callback, options) {
if (callback === null || callback === undefined) { return; }
type = String(type);
Expand All @@ -45,7 +54,7 @@ EventTargetImpl.prototype.addEventListener = function (type, callback, options)
for (var i = 0; i < list.length; i++) {
if (list[i].callback === callback && list[i].capture === capture) { return; }
}
list.push({ callback: callback, once: once, capture: capture });
ArrayPrototypePush(list, { callback: callback, once: once, capture: capture });
};
EventTargetImpl.prototype.removeEventListener = function (type, callback, options) {
type = String(type);
Expand All @@ -59,7 +68,7 @@ EventTargetImpl.prototype.removeEventListener = function (type, callback, option
if (!list) { return; }
for (var i = 0; i < list.length; i++) {
if (list[i].callback === callback && list[i].capture === capture) {
list.splice(i, 1);
ArrayPrototypeSplice(list, i, 1);
return;
}
}
Expand All @@ -71,16 +80,16 @@ EventTargetImpl.prototype.dispatchEvent = function (event) {
if (list) {
// Snapshot so listeners added during dispatch are not invoked and
// registration order is preserved.
var snapshot = list.slice();
var snapshot = ArrayPrototypeSlice(list);
for (var i = 0; i < snapshot.length; i++) {
var entry = snapshot[i];
var idx = list.indexOf(entry);
var idx = ArrayPrototypeIndexOf(list, entry);
if (idx === -1) { continue; } // removed since snapshot
if (entry.once) { list.splice(idx, 1); }
if (entry.once) { ArrayPrototypeSplice(list, idx, 1); }
var cb = entry.callback;
try {
if (typeof cb === "function") {
cb.call(this, event);
FunctionPrototypeCall(cb, this, event);
} else if (cb && typeof cb.handleEvent === "function") {
cb.handleEvent(event);
}
Expand Down Expand Up @@ -116,7 +125,7 @@ g.dispatchEvent = function (event) {
return globalTarget.dispatchEvent(event);
};

function EventTarget() { EventTargetImpl.call(this); }
function EventTarget() { FunctionPrototypeCall(EventTargetImpl, this); }
EventTarget.prototype.addEventListener = EventTargetImpl.prototype.addEventListener;
EventTarget.prototype.removeEventListener = EventTargetImpl.prototype.removeEventListener;
EventTarget.prototype.dispatchEvent = EventTargetImpl.prototype.dispatchEvent;
Expand Down
Loading
Loading