Skip to content

feat: structuredClone global (HTML structured clone, ArrayBuffer transfer) - #2000

Open
edusperoni wants to merge 2 commits into
feat/ns-utilfrom
feat/structured-clone
Open

feat: structuredClone global (HTML structured clone, ArrayBuffer transfer)#2000
edusperoni wants to merge 2 commits into
feat/ns-utilfrom
feat/structured-clone

Conversation

@edusperoni

@edusperoni edusperoni commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Android port of NativeScript/ios#431 — implements the WHATWG structuredClone(value, { transfer }) global, in lockstep with the iOS runtime.

Architecture

Follows the post-context JS builtin pattern from the js-builtins stack (#1989#1992), which is why this PR is based on feat/ns-util:

  • Native stays thin. StructuredClone.{h,cpp} exposes a one-function binding bag (clone(value, transferArray)) and runs BuiltinId::kStructuredClone. The clone is a v8::ValueSerializerv8::ValueDeserializer round-trip inside the one isolate, which is what StructuredDeserialize(StructuredSerializeWithTransfer(...)) reduces to when there is no second agent.
  • All WebIDL coercion is portable JS. js/structured-clone.js owns the argument checks and the sequence<object> conversion for transfer. It is byte-for-byte the same file as the iOS runtime's — nothing platform-specific in it.

ArrayBuffer membership in the transfer list is brand-checked from JS through the captured ArrayBuffer.prototype.byteLength getter (tamper-proof, and correctly excludes SharedArrayBuffer, which is not transferable); the native side re-checks with IsArrayBuffer().

Adds the Error and SymbolIterator primordials.

Serialization consolidation

Mirrors the iOS PR's consolidation so structuredClone and worker postMessage can never diverge:

  • StructuredSerialization.{h,cpp} (tns::serialization) owns the serializer/deserializer delegate pair, DataCloneError construction, transfer-list validation (duplicates, detached/non-detachable), and the register→write→claim→detach ordering. SerializedValue keeps serialize/deserialize as separate halves because a worker message deserializes on a different isolate. WorkerMessage.cpp is deleted (WorkerMessage.h is a one-line alias), and the MallocedBuffer scaffolding it carried goes with it.
  • postMessage gains the ArrayBuffer transfer list on both entry points (Array of ArrayBuffers, TypeError otherwise), using the shared validation. Both callbacks now accept 1 or 2 arguments.
  • Host objects stay intentionally asymmetric, encoded in one place: HostObjectPolicy { kReject, kDegrade }. structuredClone rejects with DataCloneError (spec); postMessage keeps the shipped degrade-to-{} behavior that the cross-runtime worker suite asserts on both runtimes. Unifying on reject is a coordinated follow-up with the iOS runtime; it will be a one-line policy change here.
  • DataCloneError gains its name: worker clone failures previously threw a plain Error with a "DataCloneError: ..." message prefix; both entry points now throw an Error whose name is "DataCloneError", matching iOS and the shared suite's e.name detection.

V8 14.9 notes (same as iOS)

  • ArrayBuffer::Detach() on a non-detachable buffer aborts the process — it does not throw. The IsDetachable() pre-check before detaching is therefore load-bearing, not defensive.
  • The SharedArrayBuffer/shared-value delegate hooks are overridden to keep the DataCloneError name — with a delegate installed, V8's base implementations throw a plain Error directly onto the isolate.
  • Release() must be called and its buffer free()d even after a failed write — the memory is owned by the caller after Release() regardless of whether serialization succeeded.
  • Registering a buffer via TransferArrayBuffer makes the serializer skip its was_detached check, so detaching after a successful WriteValue (rather than before) is the correct ordering.

Deviations from the specification

Same set as iOS, documented in docs/structured-clone.md:

  • DataCloneError is an Error with .name = "DataCloneError", not a DOMException (this runtime has no DOMException).
  • ArrayBuffer is the only transferable type.
  • SharedArrayBuffer shares (a new SAB over the same backing store) and is not transferable.
  • Host objects are never cloneable by structuredClone; postMessage degrades them (see above).

Tests

Shared cross-runtime suite: NativeScript/common-runtime-tests-app#26 (54 specs — clone semantics, graph identity and cycles, transfer, SharedArrayBuffer sharing, worker message transfer, DataCloneError cases), wired up via shared.runStructuredCloneTests() in mainpage.js. An unguarded canary in testRuntimeImplementedAPIs.js asserts the global exists, so the suite's self-gating cannot hide a regression.

Full suite green on a Pixel 3a API 36 emulator (arm64): 701 specs, 0 failures, 5 skipped, 11 disabled.

Merge sequencing

…sfer)

Android port of NativeScript/ios#431, in lockstep with the iOS runtime.

Implements the WHATWG structuredClone(value, { transfer }) global as a
post-context JS builtin: js/structured-clone.js (shared unchanged with
iOS) owns the WebIDL argument coercion, and a thin native binding runs a
v8::ValueSerializer -> ValueDeserializer round-trip in the one isolate.

The serialization machinery is consolidated into
StructuredSerialization.{h,cpp} (tns::serialization) so structuredClone
and worker postMessage run on one core: delegate pair, DataCloneError
construction (an Error carrying that name -- previously the worker path
threw a plain Error with a message prefix), transfer-list validation,
and the register->write->claim->detach ordering that V8 14.9 requires
(Detach() aborts on non-detachable buffers; Release() must be claimed
even after a failed write). WorkerMessage.cpp is deleted and
WorkerMessage.h reduced to an alias.

postMessage gains the ArrayBuffer transfer list on both entry points.
Host objects stay intentionally asymmetric via HostObjectPolicy:
structuredClone rejects (spec), postMessage keeps the shipped
degrade-to-{} behavior.

Tests: shared cross-runtime suite (common-runtime-tests-app#26, 54
specs) wired via shared.runStructuredCloneTests(), plus an unguarded
canary in testRuntimeImplementedAPIs.js. Documented in
docs/structured-clone.md.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 52a1a16c-cc75-4625-a07d-5b890fe72961

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Both tests PRs (#25 Performance, #26 StructuredClone) squash-merged
upstream; the branch commit this previously pinned is now orphaned.
StructuredClone/ content is byte-identical to the tested commit; master
additionally carries the Performance suite, which mainpage.js does not
invoke.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant