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.

## 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;
}

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