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
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
(`AbortController`, `AbortSignal` with the `abort`/`timeout`/`any` statics)
layered on the runtime's `EventTarget`, the GC contract (weak timers and
`any()` links, listener-driven persistence), and the `DOMException`
stand-in (name-patched `Error` reasons).
reasons.
- [TextEncoder / TextDecoder and atob / btoa](text-encoding.md) — the WHATWG
encoding and base64 globals (`TextEncoder`, `TextDecoder`, `atob`, `btoa`),
the supported encodings with their label sets, streaming decode semantics,
and the lazy-global tier that runs their builtins only on first use.
- [Error handling](error-handling.md) — global `error`/`unhandledrejection` events, `reportError`, catching Java exceptions in JS (`error.nativeException`), forwarding JS throws to Java callers (`interop.escapeException`), JS stacks on Java exceptions (`com.tns.JavaScriptStackTrace`), configuration flags, and crash-reporter integration.
- [structuredClone](structured-clone.md) — the WHATWG `structuredClone(value, { transfer })` global: what clones, how graph identity and cycles are preserved, `ArrayBuffer` transfer, and the `DataCloneError`-named `Error` that stands in for `DOMException`.
- [structuredClone](structured-clone.md) — the WHATWG `structuredClone(value, { transfer })` global: what clones, how graph identity and cycles are preserved, `ArrayBuffer` transfer, and the `DataCloneError` `DOMException` on failure.
- [Implementing additional Chrome DevTools protocol Domains](extending-inspector.md)

## Knowledge
Expand Down
16 changes: 8 additions & 8 deletions docs/abort-signal.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,20 @@ be dropped.
accounting comes from an internal symbol-keyed hook the events builtin
calls from every listener-list mutation path (add, remove, and `once`
removal during dispatch); the key travels only through the builtin-only
`internals` object (see `test-app/runtime/src/main/cpp/js/README.md`) and
never reaches app code, so the accounting cannot be bypassed via a
captured `EventTarget.prototype.addEventListener`.
`require("internal/events")` tier (see
`test-app/runtime/src/main/cpp/js/README.md`) and never reaches app code,
so the accounting cannot be bypassed via a captured
`EventTarget.prototype.addEventListener`.

Entries leave the persistent set on abort, on the last abort-listener
removal, or when a composite loses its last source.

Default reasons are real `DOMException`s — `"AbortError"` for a plain abort,
`"TimeoutError"` for `timeout()` — so both `reason.name` and
`instanceof DOMException` checks work.

## Deviations from Node / the web

- **No `DOMException`.** As with [structuredClone](structured-clone.md) and
the [Performance API](performance.md), default reasons are `Error`
instances with `name` patched: `"AbortError"` (default abort) and
`"TimeoutError"` (timeout). `instanceof DOMException` checks cannot work;
match on `reason.name`.
- Abort events carry no `isTrusted` flag (the runtime's `Event` doesn't
model it).

Expand Down
10 changes: 5 additions & 5 deletions docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ Both paths produce the same two arguments with the same exactness.
asynchronous relative to `mark()`/`measure()` but precedes timer callbacks
scheduled in the same turn. Callback exceptions are routed to
`reportError`, so one throwing observer does not starve the others.
- **No `DOMException`.** Errors the specs express as `DOMException` — the
`SyntaxError` for a missing mark name, the `InvalidModificationError` for
switching an observer between the `entryTypes` and `type` forms — are
`Error` instances with `name` patched. `err.name` checks work;
`instanceof DOMException` does not.
- Errors the specs express as `DOMException` — the `SyntaxError` for a
missing mark name, the `InvalidModificationError` for switching an
observer between the `entryTypes` and `type` forms — are real
`DOMException`s: both `err.name` and `instanceof DOMException` checks
work.
- Browser-only surface is absent: no resource/navigation timing, no
`eventCounts`, and no `PerformanceTiming`-attribute resolution in
`measure()`.
2 changes: 1 addition & 1 deletion docs/structured-clone.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Two differences are intentional:

## Deviations from the specification

- **`DataCloneError` is an `Error`, not a `DOMException`.** This runtime has no `DOMException`, so failures throw an `Error` whose `name` is set to `"DataCloneError"`. Detect failures with `e.name === "DataCloneError"`; `instanceof DOMException` cannot work.
- **`DataCloneError` is a `DOMException`.** Failures throw a `DOMException` named `"DataCloneError"`, from the JS argument checks and the native serializer alike, so both `e.name === "DataCloneError"` and `instanceof DOMException` detect them. (The serializer falls back to a `DataCloneError`-named `Error` only when the builtin can no longer run, e.g. during isolate teardown.)
- **Only `ArrayBuffer` is transferable.** The spec's other transferable types — `MessagePort`, `ImageBitmap`, `ReadableStream` and friends — do not exist here. A non-`ArrayBuffer` in the transfer list is a `DataCloneError`.
- **Host objects are not cloneable by `structuredClone`.** The spec leaves platform objects to each host; here every native/interop wrapper is rejected with a `DataCloneError`, because a JavaScript copy detached from its native counterpart would be a wrapper around nothing. Worker `postMessage` deliberately differs — see above.

Expand Down
3 changes: 1 addition & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Lint setup for the runtime's builtin JavaScript
// (test-app/runtime/src/main/cpp/js). Each file is compiled by BuiltinLoader
// as a FUNCTION BODY with the fixed parameters `exports`, `require`, `module`,
// `binding`, `primordials` and `internals` (see that directory's README.md), which are
// `binding` and `primordials` (see that directory's README.md), which are
// declared as globals here. no-undef is the typo net for binding-bag destructures and
// native-global usage alike; no-restricted-properties keeps the captured
// intrinsics from being read off the live globals again.
Expand Down Expand Up @@ -56,7 +56,6 @@ export default [
module: 'readonly',
binding: 'readonly',
primordials: 'readonly',
internals: 'readonly',
global: 'readonly',
console: 'readonly',
URL: 'readonly',
Expand Down
2 changes: 2 additions & 0 deletions test-app/app/src/main/assets/app/mainpage.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ shared.runWorkerTests();
shared.runPerformanceTests();
shared.runStructuredCloneTests();
shared.runTextEncodingTests();
shared.runDOMExceptionTests();
shared.runEventsTests();
require("./tests/testWebAssembly");
require("./tests/testEventLoop");
require("./tests/testMultithreadedJavascript");
Expand Down
2 changes: 1 addition & 1 deletion test-app/app/src/main/assets/app/shared
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,23 @@ describe("structuredClone canary", function () {
expect(typeof structuredClone).toBe("function");
});
});

// Same contract as above for the shared DOMException / CustomEvent suites:
// they self-gate, these unguarded specs turn absence into a failure.
describe("DOMException canary", function () {
it("is implemented by this runtime", function () {
expect(typeof DOMException).toBe("function");
expect(new DOMException("x", "AbortError") instanceof Error).toBe(true);
});

it("is not reachable as a module from app code", function () {
expect(function () { require("internal/dom-exception"); }).toThrow();
});
});

describe("CustomEvent canary", function () {
it("is implemented by this runtime", function () {
expect(typeof CustomEvent).toBe("function");
expect(new CustomEvent("x") instanceof Event).toBe(true);
});
});
1 change: 1 addition & 0 deletions test-app/runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ set(RUNTIME_BUILTIN_JS
${RUNTIME_BUILTIN_JS_DIR}/abort-signal.js
${RUNTIME_BUILTIN_JS_DIR}/base64.js
${RUNTIME_BUILTIN_JS_DIR}/blob-url.js
${RUNTIME_BUILTIN_JS_DIR}/dom-exception.js
${RUNTIME_BUILTIN_JS_DIR}/error-events.js
${RUNTIME_BUILTIN_JS_DIR}/events.js
${RUNTIME_BUILTIN_JS_DIR}/inspect.js
Expand Down
67 changes: 17 additions & 50 deletions test-app/runtime/src/main/cpp/BuiltinLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,15 @@ std::vector<uint8_t> builtinCache[static_cast<unsigned>(BuiltinId::kCount)];
* parameters, mirroring Node's module wrapper: a file exports through
* `module.exports`/`exports`, reaches sibling builtin modules through
* `require`, natives arrive as properties of the `binding` bag (Node's
* internalBinding idiom), intrinsics as properties of `primordials` and
* cross-builtin capabilities as properties of `internals`; each file
* destructures what it needs.
* internalBinding idiom) and intrinsics as properties of `primordials`; each
* file destructures what it needs.
*/
constexpr const char* kExportsParamName = "exports";
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 size_t kParamCount = 6;
constexpr size_t kParamCount = 5;

/*
* `module.exports` of every builtin that has run in this isolate, indexed by
Expand All @@ -48,43 +46,19 @@ struct BuiltinExportsState {
};

/*
* This runtime's intrinsics snapshot, builtin require and shared internals
* object. Per-runtime state rather than an isolate-keyed shared map, so
* reaching it needs no lock and it is released with the runtime, while the
* isolate is still alive.
* This runtime's intrinsics snapshot and builtin require. Per-runtime state
* rather than an isolate-keyed shared map, so reaching it needs no lock and
* it is released with the runtime, while the isolate is still alive.
*/
struct BuiltinRealm {
v8::Global<v8::Object> primordials;
v8::Global<v8::Function> builtinRequire;
v8::Global<v8::Object> internals;
};

/*
* 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 PrepareV8Runtime's ordering is the dependency graph.
*/
MaybeLocal<Object> GetInternals(Local<Context> context) {
Isolate* isolate = v8::Isolate::GetCurrent();

auto* realm = RuntimeState::For<BuiltinRealm>(isolate);
if (realm == nullptr) {
return MaybeLocal<Object>();
}

if (!realm->internals.IsEmpty()) {
return realm->internals.Get(isolate);
}

Local<Object> internals = Object::New(isolate);
realm->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();
Expand All @@ -99,7 +73,7 @@ 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(ArgConverter::ConvertToV8String(
isolate, NsBuiltinModules::NotFoundMessage(specifier))));
}
Expand Down Expand Up @@ -149,8 +123,7 @@ MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
ArgConverter::ConvertToV8String(isolate, kRequireParamName),
ArgConverter::ConvertToV8String(isolate, kModuleParamName),
ArgConverter::ConvertToV8String(isolate, kBindingParamName),
ArgConverter::ConvertToV8String(isolate, kPrimordialsParamName),
ArgConverter::ConvertToV8String(isolate, kInternalsParamName)};
ArgConverter::ConvertToV8String(isolate, kPrimordialsParamName)};

Local<v8::Function> fn;
if (!blob.empty()) {
Expand Down Expand Up @@ -189,7 +162,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> primordials) {
Isolate* isolate = v8::Isolate::GetCurrent();

Local<v8::Function> fn;
Expand All @@ -211,7 +184,7 @@ MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id, Local<Value>

Local<Value> args[] = {exportsObj, require, moduleObj,
binding.IsEmpty() ? Undefined(isolate).As<Value>() : binding,
primordials, internals};
primordials};
if (fn->Call(context, Undefined(isolate), static_cast<int>(kParamCount), args).IsEmpty()) {
return MaybeLocal<Value>();
}
Expand All @@ -225,7 +198,7 @@ MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id, Local<Value>
* 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();

auto* realm = RuntimeState::For<BuiltinRealm>(isolate);
Expand All @@ -238,8 +211,7 @@ MaybeLocal<Object> GetPrimordials(Local<Context> context, Local<Object> internal
}

Local<Value> result;
if (!CallBuiltin(context, BuiltinId::kPrimordials, Local<Value>(), Undefined(isolate),
internals)
if (!CallBuiltin(context, BuiltinId::kPrimordials, Local<Value>(), Undefined(isolate))
.ToLocal(&result) ||
!result->IsObject()) {
return MaybeLocal<Object>();
Expand All @@ -254,17 +226,12 @@ MaybeLocal<Object> GetPrimordials(Local<Context> context, Local<Object> internal

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, BuiltinId id,
Expand Down
16 changes: 7 additions & 9 deletions test-app/runtime/src/main/cpp/BuiltinLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@ class BuiltinLoader {
/*
* 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).
* 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.
Expand Down
24 changes: 18 additions & 6 deletions test-app/runtime/src/main/cpp/Events.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "Events.h"

#include "ArgConverter.h"
#include "BuiltinLoader.h"
#include "NativeScriptException.h"
#include "Runtime.h"
Expand All @@ -9,24 +10,35 @@ using namespace tns;
using namespace v8;

void Events::Init(Local<Context> context) {
// The builtin installs 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.
auto isolate = v8::Isolate::GetCurrent();
auto runtime = Runtime::TryGetRuntime(isolate);
if (runtime == nullptr) {
throw NativeScriptException("Events::Init: no runtime for isolate");
}

Local<Value> result;
if (!BuiltinLoader::RunBuiltin(context, BuiltinId::kEvents).ToLocal(&result) ||
!result->IsObject()) {
Local<Object> exports;
if (!BuiltinLoader::GetExports(context, BuiltinId::kEvents, nullptr).ToLocal(&exports)) {
throw NativeScriptException("Events::Init: the event-primitives bootstrap failed");
}

Local<Value> globalEventTarget;
if (!exports->Get(context, ArgConverter::ConvertToV8String(isolate, "globalEventTarget"))
.ToLocal(&globalEventTarget) ||
!globalEventTarget->IsObject()) {
throw NativeScriptException("Events::Init: the event-primitives bootstrap did not return the backing target");
}

runtime->GlobalEventTarget().Reset(isolate, result.As<Object>());
runtime->GlobalEventTarget().Reset(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;
if (!BuiltinLoader::RunBuiltin(context, BuiltinId::kAbortSignal).ToLocal(&abortResult)) {
throw NativeScriptException("Events::Init: the abort-signal bootstrap failed");
Expand Down
15 changes: 15 additions & 0 deletions test-app/runtime/src/main/cpp/LazyGlobals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

using namespace v8;
Expand All @@ -23,11 +24,25 @@ 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 test-app/runtime/src/main/cpp/LazyGlobals.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ namespace tns {
* with a 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 src/main/cpp/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
* src/main/cpp/js/README.md).
*/
class LazyGlobals {
public:
Expand Down
Loading