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
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Runtime documentation

- [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`.
- [Implementing additional Chrome DevTools protocol Domains](extending-inspector.md)

## Knowledge
Expand Down
55 changes: 55 additions & 0 deletions docs/structured-clone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# structuredClone

The runtime exposes the WHATWG [`structuredClone(value, options)`](https://html.spec.whatwg.org/multipage/structured-data.html#dom-structuredclone) global. It performs a deep, structure-preserving copy of `value` using V8's structured clone serializer — the same one worker `postMessage` uses — optionally taking ownership of `ArrayBuffer`s named in `options.transfer`.

```js
const clone = structuredClone({ when: new Date(), tags: new Set(["a"]) });

const buffer = new ArrayBuffer(1024);
const moved = structuredClone(buffer, { transfer: [buffer] });
buffer.byteLength; // 0 — the memory now belongs to `moved`
```

## Surface

`structuredClone(value)` returns a clone of `value`. `structuredClone(value, { transfer })` additionally transfers every `ArrayBuffer` in `transfer`.

- `value` is required; calling with no arguments throws a `TypeError`.
- `options` may be `undefined` or `null` (both mean "no transfer"); anything else must be an object, or a `TypeError` is thrown.
- `options.transfer` is a WebIDL sequence: any object with a callable `Symbol.iterator` works (an array, a `Set`, a generator). A non-iterable value — including a string primitive — throws a `TypeError`.

Cloneable: every primitive value except symbols — numbers (including `-0`, `NaN` and the infinities), strings, booleans, `BigInt`, `null` and `undefined`; plain objects and arrays; `Date`, `RegExp`, `Map`, `Set`, `Error`; `Boolean`/`String`/`Number` wrapper objects; `ArrayBuffer`, every typed array and `DataView`.

The clone preserves the shape of the graph, not just the values: an object referenced twice in the input is a single object referenced twice in the output, and cycles round-trip. Prototypes do not survive — a class instance clones to a plain object with the same own properties. Getters are invoked during cloning and their result is stored as a plain data property. Property insertion order is preserved.

`SharedArrayBuffer` is **shared, not copied**: the clone is a second `SharedArrayBuffer` over the same memory, so writes through either are visible through the other.

Not cloneable — each throws (see the deviations below): functions, symbols, `WeakMap`/`WeakSet`/`WeakRef`, `Promise`, and every native/interop object (Java proxies and the objects the metadata layer hands out), which have no serialized form.

## Transfer semantics

Listed buffers are validated before anything is serialized: each entry must be an `ArrayBuffer`, must not already be detached, must be detachable, and must appear at most once. A violation throws before the source buffers are touched, so a rejected call never leaves a half-transferred graph behind.

On success the memory changes hands rather than being copied: the source buffer is detached (`byteLength` becomes 0, and every typed array over it becomes zero-length) and the clone receives the original backing store. A transferred buffer need not appear inside `value` at all; a buffer reached through a typed array in `value` is transferred as a unit, so the cloned view sees the original bytes.

## Worker `postMessage`

`structuredClone` and worker `postMessage` run on the same serialization core, so everything above — which types clone, graph identity, cycles, `SharedArrayBuffer` sharing — holds for messages too. `postMessage` takes the same transfer list as a second argument:

```js
worker.postMessage({ pixels: buffer }, [buffer]); // buffer is detached here,
// its memory now in the worker
```

Two differences are intentional:

- **The transfer list must be an array.** Omitting it, or passing `undefined` or `null`, means "transfer nothing"; every other non-array value is a `TypeError`. The WebIDL iterable-to-sequence conversion that lets `structuredClone` take a `Set` or any iterable lives in the JavaScript wrapper around `structuredClone`; `postMessage` is native all the way down and has no such wrapper.
- **Host objects degrade instead of throwing.** Posting a native/interop object delivers an empty object to the receiver rather than raising a `DataCloneError`. This is long-standing shipped behavior, and app code relies on it; `structuredClone`, being new, follows the spec and rejects. The asymmetry is encoded in exactly one place — the `HostObjectPolicy` enum in `test-app/runtime/src/main/cpp/StructuredSerialization.h` — and unifying the two on rejection is a breaking change that needs the iOS runtime to move at the same time.
Comment on lines +37 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document both postMessage entry points.

The PR adds transfer-list support to both worker postMessage entry points, but this section only names and demonstrates worker.postMessage. State both entry points and their shared array-only transfer-list rule so the worker-side API is also discoverable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/structured-clone.md` around lines 37 - 47, Update the “postMessage”
documentation to name both worker postMessage entry points, including the
worker-side API, and state that both accept an optional transfer list only when
it is an array; omitted, undefined, or null transfer nothing, while other
non-array values throw TypeError. Keep the existing shared serialization and
host-object behavior details unchanged.


## 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.
- **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.

`SharedArrayBuffer` follows the spec: it is shared rather than copied, and it is not transferable (listing one throws a `DataCloneError`).
1 change: 1 addition & 0 deletions test-app/app/src/main/assets/app/mainpage.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ shared.runRequireTests();
shared.runWeakRefTests();
shared.runRuntimeTests();
shared.runWorkerTests();
shared.runStructuredCloneTests();
require("./tests/testWebAssembly");
require("./tests/testMultithreadedJavascript");
require("./tests/testInterfaceDefaultMethods");
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 @@ -31,3 +31,12 @@ describe("Runtime exposes", function () {
expect(ok).toBe(true, "__time delta " + timeDelta + "ms diverged from Date.now delta " + dateDelta + "ms (tolerance " + tolerance + "ms) on all " + attempts + " attempts");
});
});

// The shared StructuredClone suite skips itself where the API is missing, which
// would turn this runtime losing structuredClone into a green run. This spec is
// deliberately unguarded so that regression fails instead.
describe("structuredClone canary", function () {
it("is implemented by this runtime", function () {
expect(typeof structuredClone).toBe("function");
});
});
4 changes: 3 additions & 1 deletion test-app/runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ set(RUNTIME_BUILTIN_JS
${RUNTIME_BUILTIN_JS_DIR}/ns-util.js
${RUNTIME_BUILTIN_JS_DIR}/primordials.js
${RUNTIME_BUILTIN_JS_DIR}/require-factory.js
${RUNTIME_BUILTIN_JS_DIR}/structured-clone.js
${RUNTIME_BUILTIN_JS_DIR}/weak-ref.js
)
set(RUNTIME_BUILTINS_GENERATED_DIR ${PROJECT_SOURCE_DIR}/src/main/cpp/generated)
Expand Down Expand Up @@ -184,11 +185,12 @@ add_library(
src/main/cpp/Runtime.cpp
src/main/cpp/SimpleAllocator.cpp
src/main/cpp/SimpleProfiler.cpp
src/main/cpp/StructuredClone.cpp
src/main/cpp/StructuredSerialization.cpp
src/main/cpp/Util.cpp
src/main/cpp/V8GlobalHelpers.cpp
src/main/cpp/V8StringConstants.cpp
src/main/cpp/WeakRef.cpp
src/main/cpp/WorkerMessage.cpp
src/main/cpp/WorkerWrapper.cpp
src/main/cpp/Timers.cpp
src/main/cpp/com_tns_AssetExtractor.cpp
Expand Down
33 changes: 27 additions & 6 deletions test-app/runtime/src/main/cpp/CallbackHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1222,12 +1222,18 @@ CallbackHandlers::WorkerObjectPostMessageCallback(const v8::FunctionCallbackInfo
HandleScope scope(isolate);

try {
if (args.Length() != 1) {
if (args.Length() < 1) {
isolate->ThrowException(ArgConverter::ConvertToV8String(isolate,
"Failed to execute 'postMessage' on 'Worker': 1 argument required."));
return;
}

if (args.Length() > 2) {
isolate->ThrowException(ArgConverter::ConvertToV8String(isolate,
"Failed to execute 'postMessage' on 'Worker': no more than 2 arguments accepted."));
return;
}

auto thiz = args.This(); // Worker instance

Local<Value> jsId;
Expand All @@ -1249,9 +1255,15 @@ CallbackHandlers::WorkerObjectPostMessageCallback(const v8::FunctionCallbackInfo
return;
}

Local<Value> transferList = args.Length() > 1
? args[1]
: v8::Undefined(isolate).As<Value>();
auto message = std::make_shared<worker::Message>();
if (message->Serialize(isolate, context, args[0]).IsNothing()) {
// a DataCloneError is already pending on the isolate
if (message->Serialize(isolate, context, args[0], transferList,
serialization::HostObjectPolicy::kDegrade)
.IsNothing()) {
// The transfer list was rejected or the value could not be cloned;
// the exception is already pending and nothing may be posted.
return;
}

Expand Down Expand Up @@ -1283,9 +1295,12 @@ CallbackHandlers::WorkerGlobalPostMessageCallback(const v8::FunctionCallbackInfo
TryCatch tc(isolate);

// TODO: Pete: Discuss whether this is the way to go
if (args.Length() != 1) {
if (args.Length() < 1) {
isolate->ThrowException(ArgConverter::ConvertToV8String(isolate,
"Failed to execute 'postMessage' on WorkerGlobalScope: 1 argument required."));
} else if (args.Length() > 2) {
isolate->ThrowException(ArgConverter::ConvertToV8String(isolate,
"Failed to execute 'postMessage' on WorkerGlobalScope: no more than 2 arguments accepted."));
}

if (tc.HasCaught()) {
Expand All @@ -1301,9 +1316,15 @@ CallbackHandlers::WorkerGlobalPostMessageCallback(const v8::FunctionCallbackInfo
}

auto context = isolate->GetCurrentContext();
Local<Value> transferList = args.Length() > 1
? args[1]
: v8::Undefined(isolate).As<Value>();
auto message = std::make_shared<worker::Message>();
if (message->Serialize(isolate, context, args[0]).IsNothing()) {
// a DataCloneError is already pending on the isolate
if (message->Serialize(isolate, context, args[0], transferList,
serialization::HostObjectPolicy::kDegrade)
.IsNothing()) {
// The transfer list was rejected or the value could not be cloned;
// the exception is already pending and nothing may be posted.
return;
Comment on lines +1323 to 1328

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how worker callback errors are dispatched and whether worker clone
# failures have regression coverage.
rg -n -C 8 '\bWorkerGlobalPostMessageCallback\b|\bCallWorkerScopeOnErrorHandle\b' \
  test-app/runtime/src/main/cpp/CallbackHandlers.cpp

rg -n -C 5 'DataCloneError|structuredClone|postMessage\s*\(' \
  test-app/app/src/main/assets/app

Repository: NativeScript/android

Length of output: 10333


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the complete callback, error-dispatch helper, and Message::Serialize
# implementation without executing repository code.
rg -n -l 'CallWorkerScopeOnErrorHandle|class Message|Message::Serialize|\.Serialize\(' \
  test-app/runtime/src/main/cpp

printf '\n--- callback and error helper ---\n'
sed -n '1288,1535p' test-app/runtime/src/main/cpp/CallbackHandlers.cpp

printf '\n--- serialization declarations and definitions ---\n'
rg -n -C 12 'Serialize\s*\(|class Message|struct Message' \
  test-app/runtime/src/main/cpp test-app/runtime/src/main

Repository: NativeScript/android

Length of output: 40340


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- complete worker error helper ---'
sed -n '1504,1585p' test-app/runtime/src/main/cpp/CallbackHandlers.cpp

printf '%s\n' '--- serialization failure paths ---'
sed -n '1,230p' test-app/runtime/src/main/cpp/StructuredSerialization.cpp
sed -n '230,430p' test-app/runtime/src/main/cpp/StructuredSerialization.cpp

printf '%s\n' '--- related postMessage callback and tests ---'
sed -n '1210,1290p' test-app/runtime/src/main/cpp/CallbackHandlers.cpp
rg -n -C 10 'postMessage|DataCloneError|transfer list|structured clone|clone' \
  test-app/app/src/main/assets/app/tests test-app/runtime/src/main/cpp \
  -g '*.js' -g '*.cpp' -g '*.h'

Repository: NativeScript/android

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- worker execution and exception forwarding ---'
rg -n -C 12 'PassUncaughtExceptionFromWorkerToParent|TryCatch|Execute|RunScript|Script::Run|onerror' \
  test-app/runtime/src/main/cpp \
  -g '*.cpp' -g '*.h' \
  | grep -v 'robin_hood.h' | head -n 500

printf '%s\n' '--- all local TryCatch rethrow patterns ---'
rg -n -C 5 'TryCatch|ReThrow|IsNothing\(\).*return|IsNothing\(\)' \
  test-app/runtime/src/main/cpp \
  -g '*.cpp' -g '*.h' | grep -v 'robin_hood.h' | head -n 500

printf '%s\n' '--- relevant test names and worker error assertions ---'
rg -n -C 8 'worker\.onerror|onerror\s*=|DataCloneError|postMessage.*catch|catch.*postMessage|could not be cloned|transfer list' \
  test-app/app/src/main/assets/app/tests \
  -g '*.js'

Repository: NativeScript/android

Length of output: 44955


🌐 Web query:

V8 TryCatch ReThrow exception caught callback destructor documentation

💡 Result:

In the V8 engine, the v8::TryCatch class is an external exception handler used to catch JavaScript exceptions [1][2]. Its destructor (~TryCatch) is responsible for unregistering and deleting the try/catch block from V8 [3][4]. Regarding the specific behaviors you mentioned: 1. Destructor Functionality: The destructor ~TryCatch performs necessary cleanup by unregistering the block [3][1]. Because it is designed to be stack-allocated, its cleanup happens automatically when the object goes out of scope [3][5]. 2. ReThrow Method: The ReThrow method is used to re-throw an exception that has been caught by the current v8::TryCatch instance [1]. Crucially, it re-throws the exception in a manner that avoids it being caught again by the same TryCatch instance [3][4]. After calling ReThrow, it is illegal to execute any further JavaScript operations; the caller must return immediately to the point where the exception is propagated [1][2]. 3. Destructor Callback: V8 does not provide a specific callback mechanism tied to the destruction of a v8::TryCatch instance itself. If you are looking for callbacks related to object lifecycles (such as cleaning up C++ resources associated with a JavaScript object), V8 provides WeakPersistent handles and finalizers (e.g., v8::Global::SetWeak or v8::FinalizationRegistry) rather than hooks on the TryCatch destructor [6]. In summary, the v8::TryCatch destructor handles internal engine state management to ensure exception handling chains remain consistent [3][1]. It is not a user-extensible callback point [3][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- worker message-dispatch control flow ---'
sed -n '130,245p' test-app/runtime/src/main/cpp/WorkerWrapper.cpp
sed -n '365,415p' test-app/runtime/src/main/cpp/WorkerWrapper.cpp

printf '%s\n' '--- focused test inventory ---'
rg -n -C 6 'worker\.onerror|onerror\s*=|DataCloneError|postMessage.*catch|catch.*postMessage|transfer list|could not be cloned' \
  test-app/app/src/main/assets/app/tests \
  -g '*.js' || true

printf '%s\n' '--- static path check ---'
python3 - <<'PY'
from pathlib import Path

p = Path("test-app/runtime/src/main/cpp/CallbackHandlers.cpp")
text = p.read_text()
start = text.index("CallbackHandlers::WorkerGlobalPostMessageCallback")
end = text.index("void\nCallbackHandlers::WorkerObjectTerminateCallback", start)
fn = text[start:end]

checks = {
    "callback creates TryCatch": "TryCatch tc(isolate);" in fn,
    "serialization can return Nothing": ".IsNothing()" in fn,
    "serialization failure returns directly": "                    return;" in fn[fn.index(".IsNothing()"):],
    "serialization failure calls worker error helper": "CallWorkerScopeOnErrorHandle(isolate, tc);" in fn[fn.index(".IsNothing()"):],
    "serialization failure rethrows": "tc.ReThrow();" in fn[fn.index(".IsNothing()"):],
}
for name, result in checks.items():
    print(f"{name}: {result}")
PY

Repository: NativeScript/android

Length of output: 9333


Re-throw the serialization exception from WorkerGlobalScope.postMessage.

When Serialize returns Nothing, it leaves a DataCloneError or TypeError pending in tc. Call tc.ReThrow() before returning so JavaScript can catch the synchronous failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-app/runtime/src/main/cpp/CallbackHandlers.cpp` around lines 1323 - 1328,
Update the WorkerGlobalScope.postMessage serialization failure path around
Serialize to call tc.ReThrow() before returning when Serialize yields Nothing,
preserving the pending DataCloneError or TypeError so JavaScript can catch the
synchronous failure.

}

Expand Down
3 changes: 3 additions & 0 deletions test-app/runtime/src/main/cpp/Runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include "NativeScriptException.h"
#include "SimpleAllocator.h"
#include "SimpleProfiler.h"
#include "StructuredClone.h"
#include "URLImpl.h"
#include "URLPatternImpl.h"
#include "URLSearchParamsImpl.h"
Expand Down Expand Up @@ -853,6 +854,8 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath,
Events::Init(context);
ErrorEvents::Init(context);

StructuredClone::Init(context);

// The `interop` global (interop.escapeException), mirroring iOS.
Interop::Init(context);

Expand Down
67 changes: 67 additions & 0 deletions test-app/runtime/src/main/cpp/StructuredClone.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include "StructuredClone.h"

#include <cassert>

#include "ArgConverter.h"
#include "BuiltinLoader.h"
#include "StructuredSerialization.h"

using namespace v8;

namespace tns {

namespace {

/*
* binding.clone(value, transferArrayOrUndefined): serialize and deserialize in
* this one isolate, which is what StructuredDeserialize(
* StructuredSerializeWithTransfer(...)) amounts to when there is no second
* agent involved.
*/
void CloneCallback(const FunctionCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<Value> value =
info.Length() > 0 ? info[0] : v8::Undefined(isolate).As<Value>();
Local<Value> transferList =
info.Length() > 1 ? info[1] : v8::Undefined(isolate).As<Value>();

serialization::SerializedValue serialized;
if (serialized
.Serialize(isolate, context, value, transferList,
serialization::HostObjectPolicy::kReject)
.IsNothing()) {
return;
}

Local<Value> result;
if (!serialized.Deserialize(isolate, context).ToLocal(&result)) {
return;
}
info.GetReturnValue().Set(result);
}

} // namespace

void StructuredClone::Init(Local<Context> context) {
Isolate* isolate = Isolate::GetCurrent();

Local<v8::Function> clone;
bool success = v8::Function::New(context, CloneCallback).ToLocal(&clone);
assert(success);

Local<Object> binding = Object::New(isolate);
success = binding->Set(context,
ArgConverter::ConvertToV8String(isolate, "clone"),
clone)
.FromMaybe(false);
assert(success);

Local<Value> result;
success = BuiltinLoader::RunBuiltin(context, BuiltinId::kStructuredClone,
binding)
.ToLocal(&result);
assert(success);
}

} // namespace tns
20 changes: 20 additions & 0 deletions test-app/runtime/src/main/cpp/StructuredClone.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#ifndef STRUCTUREDCLONE_H_
#define STRUCTUREDCLONE_H_

#include "v8.h"

namespace tns {

class StructuredClone {
public:
/*
* Installs the structuredClone global (internal/structured-clone.js). The
* builtin owns the argument coercion and hands the native side a value
* plus an already-materialized array of ArrayBuffers to transfer.
*/
static void Init(v8::Local<v8::Context> context);
};

} // namespace tns

#endif /* STRUCTUREDCLONE_H_ */
Loading