diff --git a/NativeScript/runtime/Message.cpp b/NativeScript/runtime/Message.cpp deleted file mode 100644 index efd3a4c4..00000000 --- a/NativeScript/runtime/Message.cpp +++ /dev/null @@ -1,490 +0,0 @@ -// -// Message.cpp -// NativeScript -// -// Created by Eduardo Speroni on 11/22/23. -// Copyright © 2023 Progress. All rights reserved. -// - -#include "Message.hpp" - -#include "Helpers.h" -#include "NativeScriptException.h" - -using namespace v8; - -namespace tns { -namespace worker { -namespace { -void ThrowDataCloneException(Local context, - Local message) { - Isolate* isolate = v8::Isolate::GetCurrent(); - // Local argv[] = {message, - // FIXED_ONE_BYTE_STRING(isolate, "DataCloneError")}; - NativeScriptException except(isolate, tns::ToString(isolate, message), - "DataCloneError"); - except.ReThrowToV8(isolate); -} -class SerializerDelegate : public v8::ValueSerializer::Delegate { - public: - SerializerDelegate(Isolate* isolate, Local context, Message* m) - : isolate_(isolate), context_(context), msg_(m) {} - - void ThrowDataCloneError(Local message) override { - ThrowDataCloneException(context_, message); - } - - Maybe WriteHostObject(Isolate* isolate, Local object) override { - return Just(true); - // if (BaseObject::IsBaseObject(object)) { - // return WriteHostObject( - // BaseObjectPtr { Unwrap(object) }); - // } - // - // // Convert process.env to a regular object. - // auto env_proxy_ctor_template = env_->env_proxy_ctor_template(); - // if (!env_proxy_ctor_template.IsEmpty() && - // env_proxy_ctor_template->HasInstance(object)) { - // HandleScope scope(isolate); - // // TODO(bnoordhuis) Prototype-less object in case process.env - // contains - // // a "__proto__" key? process.env has a prototype with concomitant - // // methods like toString(). It's probably confusing if that gets - // lost - // // in transmission. - // Local normal_object = Object::New(isolate); - // env_->env_vars()->AssignToObject(isolate, env_->context(), - // normal_object); serializer->WriteUint32(kNormalObject); // Instead - // of a BaseObject. return serializer->WriteValue(env_->context(), - // normal_object); - // } - // - // ThrowDataCloneError(env_->clone_unsupported_type_str()); - // return Nothing(); - } - - Maybe GetSharedArrayBufferId( - Isolate* isolate, Local shared_array_buffer) override { - uint32_t i; - for (i = 0; i < seen_shared_array_buffers_.size(); ++i) { - if (PersistentToLocal::Strong(seen_shared_array_buffers_[i]) == - shared_array_buffer) { - return Just(i); - } - } - - seen_shared_array_buffers_.emplace_back( - Global{isolate, shared_array_buffer}); - msg_->AddSharedArrayBuffer(shared_array_buffer->GetBackingStore()); - return Just(i); - } - - // Maybe GetWasmModuleTransferId( - // Isolate* isolate, Local module) override { - // return Just(msg_->AddWASMModule(module->GetCompiledModule())); - // } - - // bool AdoptSharedValueConveyor(Isolate* isolate, - // SharedValueConveyor&& conveyor) override { - // msg_->AdoptSharedValueConveyor(std::move(conveyor)); - // return true; - // } - - // Maybe Finish(Local context) { - // for (uint32_t i = 0; i < host_objects_.size(); i++) { - // BaseObjectPtr host_object = std::move(host_objects_[i]); - // std::unique_ptr data; - // if (i < first_cloned_object_index_) - // data = host_object->TransferForMessaging(); - // if (!data) - // data = host_object->CloneForMessaging(); - // if (!data) return Nothing(); - // if (data->FinalizeTransferWrite(context, serializer).IsNothing()) - // return Nothing(); - // msg_->AddTransferable(std::move(data)); - // } - // return Just(true); - // } - - // inline void AddHostObject(BaseObjectPtr host_object) { - // // Make sure we have not started serializing the value itself yet. - // CHECK_EQ(first_cloned_object_index_, SIZE_MAX); - // host_objects_.emplace_back(std::move(host_object)); - // } - // - // // Some objects in the transfer list may register sub-objects that can be - // // transferred. This could e.g. be a public JS wrapper object, such as a - // // FileHandle, that is registering its C++ handle for transfer. - // inline Maybe AddNestedHostObjects() { - // for (size_t i = 0; i < host_objects_.size(); i++) { - // std::vector> nested_transferables; - // if - // (!host_objects_[i]->NestedTransferables().To(&nested_transferables)) - // return Nothing(); - // for (auto& nested_transferable : nested_transferables) { - // if (std::find(host_objects_.begin(), - // host_objects_.end(), - // nested_transferable) == host_objects_.end()) { - // AddHostObject(nested_transferable); - // } - // } - // } - // return Just(true); - // } - - ValueSerializer* serializer = nullptr; - - private: - // Maybe WriteHostObject(BaseObjectPtr host_object) { - // BaseObject::TransferMode mode = host_object->GetTransferMode(); - // if (mode == BaseObject::TransferMode::kUntransferable) { - // ThrowDataCloneError(env_->clone_unsupported_type_str()); - // return Nothing(); - // } - // - // for (uint32_t i = 0; i < host_objects_.size(); i++) { - // if (host_objects_[i] == host_object) { - // serializer->WriteUint32(i); - // return Just(true); - // } - // } - // - // if (mode == BaseObject::TransferMode::kTransferable) { - // THROW_ERR_MISSING_TRANSFERABLE_IN_TRANSFER_LIST(env_); - // return Nothing(); - // } - // - // CHECK_EQ(mode, BaseObject::TransferMode::kCloneable); - // uint32_t index = host_objects_.size(); - // if (first_cloned_object_index_ == SIZE_MAX) - // first_cloned_object_index_ = index; - // serializer->WriteUint32(index); - // host_objects_.push_back(host_object); - // return Just(true); - // } - - __unused Isolate* isolate_; - __unused Local context_; - Message* msg_; - std::vector> seen_shared_array_buffers_; - // std::vector> host_objects_; - __unused size_t first_cloned_object_index_ = SIZE_MAX; - - friend class tns::worker::Message; -}; - -class DeserializerDelegate : public ValueDeserializer::Delegate { - public: - DeserializerDelegate( - Message* m, Isolate* isolate, - // const std::vector>& host_objects, - const std::vector>& shared_array_buffers - // const std::vector& wasm_modules, - // const std::optional& shared_value_conveyor - ) - : // host_objects_(host_objects), - shared_array_buffers_(shared_array_buffers) - // wasm_modules_(wasm_modules), - // shared_value_conveyor_(shared_value_conveyor) - {} - - MaybeLocal ReadHostObject(Isolate* isolate) override { - EscapableHandleScope scope(isolate); - Local object = Object::New(isolate); - return scope.Escape(object).As(); - // // Identifying the index in the message's BaseObject array is - // sufficient. uint32_t id; if (!deserializer->ReadUint32(&id)) - // return MaybeLocal(); - // if (id != kNormalObject) { - // CHECK_LT(id, host_objects_.size()); - // return host_objects_[id]->object(isolate); - // } - // EscapableHandleScope scope(isolate); - // Local context = isolate->GetCurrentContext(); - // Local object; - // if (!deserializer->ReadValue(context).ToLocal(&object)) - // return MaybeLocal(); - // CHECK(object->IsObject()); - // return scope.Escape(object.As()); - } - - MaybeLocal GetSharedArrayBufferFromId( - Isolate* isolate, uint32_t clone_id) override { - // CHECK_LT(clone_id, shared_array_buffers_.size()); - return shared_array_buffers_[clone_id]; - } - - // MaybeLocal GetWasmModuleFromId( - // Isolate* isolate, uint32_t transfer_id) override { - //// CHECK_LT(transfer_id, wasm_modules_.size()); - // return WasmModuleObject::FromCompiledModule( - // isolate, wasm_modules_[transfer_id]); - // } - - // const SharedValueConveyor* GetSharedValueConveyor(Isolate* isolate) - // override { - //// CHECK(shared_value_conveyor_.has_value()); - // return &shared_value_conveyor_.value(); - // } - - ValueDeserializer* deserializer = nullptr; - - private: - // const std::vector>& host_objects_; - const std::vector>& shared_array_buffers_; - // const std::vector& wasm_modules_; - // const std::optional& shared_value_conveyor_; -}; -}; // namespace - -v8::Maybe Message::Serialize(v8::Isolate* isolate, - v8::Local context, - v8::Local input) { - HandleScope handle_scope(isolate); - v8::Context::Scope context_scope(context); - - // Verify that we're not silently overwriting an existing message. - tns::Assert(main_message_buf_.is_empty()); - - SerializerDelegate delegate(isolate, context, this); - ValueSerializer serializer(isolate, &delegate); - delegate.serializer = &serializer; - - std::vector> array_buffers; - // for (uint32_t i = 0; i < transfer_list_v.length(); ++i) { - // Local entry = transfer_list_v[i]; - // if (entry->IsObject()) { - // // See - // https://github.com/nodejs/node/pull/30339#issuecomment-552225353 - // // for details. - // bool untransferable; - // if (!entry.As()->HasPrivate( - // context, - // env->untransferable_object_private_symbol()) - // .To(&untransferable)) { - // return Nothing(); - // } - // if (untransferable) { - // ThrowDataCloneException(context, - // env->transfer_unsupported_type_str()); return Nothing(); - // } - // } - // - // // Currently, we support ArrayBuffers and BaseObjects for which - // // GetTransferMode() returns kTransferable. - // if (entry->IsArrayBuffer()) { - // Local ab = entry.As(); - // // If we cannot render the ArrayBuffer unusable in this Isolate, - // // copying the buffer will have to do. - // // Note that we can currently transfer ArrayBuffers even if they - // were - // // not allocated by Node’s ArrayBufferAllocator in the first - // place, - // // because we pass the underlying v8::BackingStore around rather - // than - // // raw data *and* an Isolate with a non-default ArrayBuffer - // allocator - // // is always going to outlive any Workers it creates, and so will - // its - // // allocator along with it. - // if (!ab->IsDetachable() || ab->WasDetached()) { - // ThrowDataCloneException(context, - // env->transfer_unsupported_type_str()); return Nothing(); - // } - // if (std::find(array_buffers.begin(), array_buffers.end(), ab) != - // array_buffers.end()) { - // ThrowDataCloneException( - // context, - // FIXED_ONE_BYTE_STRING( - // env->isolate(), - // "Transfer list contains duplicate ArrayBuffer")); - // return Nothing(); - // } - // // We simply use the array index in the `array_buffers` list as - // the - // // ID that we write into the serialized buffer. - // uint32_t id = array_buffers.size(); - // array_buffers.push_back(ab); - // serializer.TransferArrayBuffer(id, ab); - // continue; - // } else if (entry->IsObject() && - // BaseObject::IsBaseObject(entry.As())) { - // // Check if the source MessagePort is being transferred. - // if (!source_port.IsEmpty() && entry == source_port) { - // ThrowDataCloneException( - // context, - // FIXED_ONE_BYTE_STRING(env->isolate(), - // "Transfer list contains source - // port")); - // return Nothing(); - // } - // BaseObjectPtr host_object { - // Unwrap(entry.As()) }; - // if (env->message_port_constructor_template()->HasInstance(entry) - // && - // (!host_object || - // static_cast(host_object.get())->IsDetached())) - // { - // ThrowDataCloneException( - // context, - // FIXED_ONE_BYTE_STRING( - // env->isolate(), - // "MessagePort in transfer list is already detached")); - // return Nothing(); - // } - // if (std::find(delegate.host_objects_.begin(), - // delegate.host_objects_.end(), - // host_object) != delegate.host_objects_.end()) { - // ThrowDataCloneException( - // context, - // String::Concat(env->isolate(), - // FIXED_ONE_BYTE_STRING( - // env->isolate(), - // "Transfer list contains duplicate "), - // entry.As()->GetConstructorName())); - // return Nothing(); - // } - // if (host_object && host_object->GetTransferMode() == - // BaseObject::TransferMode::kTransferable) { - // delegate.AddHostObject(host_object); - // continue; - // } - // } - // - // THROW_ERR_INVALID_TRANSFER_OBJECT(env); - // return Nothing(); - // } - // if (delegate.AddNestedHostObjects().IsNothing()) - // return Nothing(); - - serializer.WriteHeader(); - if (serializer.WriteValue(context, input).IsNothing()) { - return Nothing(); - } - - for (Local ab : array_buffers) { - // If serialization succeeded, we render it inaccessible in this Isolate. - std::shared_ptr backing_store = ab->GetBackingStore(); - // A null key is accepted for buffers without a detach key. The result is - // deliberately discarded: the void Detach() this replaced could not report - // failure either, and a detach-key mismatch must not abort the process. - ab->Detach(v8::Local()).FromMaybe(false); - - array_buffers_.emplace_back(std::move(backing_store)); - } - - // if (delegate.Finish(context).IsNothing()) - // return Nothing(); - - // The serializer gave us a buffer allocated using `malloc()`. - std::pair data = serializer.Release(); - tns::Assert(data.first != NULL, isolate); - main_message_buf_ = - MallocedBuffer(reinterpret_cast(data.first), data.second); - return Just(true); -} - -MaybeLocal Message::Deserialize(Isolate* isolate, - Local context) { - Context::Scope context_scope(context); - - // CHECK(!IsCloseMessage()); - // if (port_list != nullptr && !transferables_.empty()) { - // // Need to create this outside of the EscapableHandleScope, but inside - // // the Context::Scope. - // *port_list = Array::New(env->isolate()); - // } - - EscapableHandleScope handle_scope(isolate); - - // Create all necessary objects for transferables, e.g. MessagePort handles. - // std::vector> - // host_objects(transferables_.size()); auto cleanup = OnScopeLeave([&]() { - // for (BaseObjectPtr object : host_objects) { - // if (!object) continue; - // - // // If the function did not finish successfully, host_objects will - // contain - // // a list of objects that will never be passed to JS. Therefore, we - // // destroy them here. - // object->Detach(); - // } - // }); - - // for (uint32_t i = 0; i < transferables_.size(); ++i) { - // HandleScope handle_scope(env->isolate()); - // TransferData* data = transferables_[i].get(); - // host_objects[i] = data->Deserialize( - // env, context, std::move(transferables_[i])); - // if (!host_objects[i]) return {}; - // if (port_list != nullptr) { - // // If we gather a list of all message ports, and this transferred - // object - // // is a message port, add it to that list. This is a bit of an odd - // case - // // of special handling for MessagePorts (as opposed to applying to all - // // transferables), but it's required for spec compliance. - // DCHECK((*port_list)->IsArray()); - // Local port_list_array = port_list->As(); - // Local obj = host_objects[i]->object(); - // if (env->message_port_constructor_template()->HasInstance(obj)) { - // if (port_list_array->Set(context, - // port_list_array->Length(), - // obj).IsNothing()) { - // return {}; - // } - // } - // } - // } - // transferables_.clear(); - - std::vector> shared_array_buffers; - // Attach all transferred SharedArrayBuffers to their new Isolate. - for (uint32_t i = 0; i < shared_array_buffers_.size(); ++i) { - Local sab = - SharedArrayBuffer::New(isolate, shared_array_buffers_[i]); - shared_array_buffers.push_back(sab); - } - - DeserializerDelegate delegate( - this, isolate, - // host_objects, - shared_array_buffers - // wasm_modules_, - // shared_value_conveyor_ - ); - ValueDeserializer deserializer( - isolate, reinterpret_cast(main_message_buf_.data), - main_message_buf_.size, &delegate); - delegate.deserializer = &deserializer; - - // Attach all transferred ArrayBuffers to their new Isolate. - for (uint32_t i = 0; i < array_buffers_.size(); ++i) { - Local ab = - ArrayBuffer::New(isolate, std::move(array_buffers_[i])); - deserializer.TransferArrayBuffer(i, ab); - } - - if (deserializer.ReadHeader(context).IsNothing()) return {}; - Local return_value; - if (!deserializer.ReadValue(context).ToLocal(&return_value)) return {}; - - // for (BaseObjectPtr base_object : host_objects) { - // if (base_object->FinalizeTransferRead(context, - // &deserializer).IsNothing()) - // return {}; - // } - - // host_objects.clear(); - return handle_scope.Escape(return_value); -} - -void Message::AddSharedArrayBuffer( - std::shared_ptr backing_store) { - shared_array_buffers_.emplace_back(std::move(backing_store)); -} - -Message::Message(MallocedBuffer&& payload) - : main_message_buf_(std::move(payload)) {} -}; // namespace worker -}; // namespace tns diff --git a/NativeScript/runtime/Message.hpp b/NativeScript/runtime/Message.hpp index e90ec00d..6771ee9a 100644 --- a/NativeScript/runtime/Message.hpp +++ b/NativeScript/runtime/Message.hpp @@ -8,129 +8,18 @@ #ifndef Message_hpp #define Message_hpp -#include "v8.h" -namespace tns { - -template -inline T* Malloc(size_t n) { - T* ret = malloc(n); - return ret; -} - -template -T* UncheckedRealloc(T* pointer, size_t n) { - size_t full_size = sizeof(T) * n; - - if (full_size == 0) { - free(pointer); - return nullptr; - } - - void* allocated = realloc(pointer, full_size); - - // if (UNLIKELY(allocated == nullptr)) { - // // Tell V8 that memory is low and retry. - // LowMemoryNotification(); - // allocated = realloc(pointer, full_size); - // } - - return static_cast(allocated); -} - -template -struct MallocedBuffer { - T* data; - size_t size; - - T* release() { - T* ret = data; - data = nullptr; - return ret; - } - - void Truncate(size_t new_size) { - CHECK_LE(new_size, size); - size = new_size; - } - - void Realloc(size_t new_size) { - Truncate(new_size); - data = UncheckedRealloc(data, new_size); - } - - bool is_empty() const { return data == nullptr; } - - MallocedBuffer() : data(nullptr), size(0) {} - explicit MallocedBuffer(size_t size) : data(Malloc(size)), size(size) {} - MallocedBuffer(T* data, size_t size) : data(data), size(size) {} - MallocedBuffer(MallocedBuffer&& other) : data(other.data), size(other.size) { - other.data = nullptr; - } - MallocedBuffer& operator=(MallocedBuffer&& other) { - this->~MallocedBuffer(); - return *new (this) MallocedBuffer(std::move(other)); - } - ~MallocedBuffer() { free(data); } - MallocedBuffer(const MallocedBuffer&) = delete; - MallocedBuffer& operator=(const MallocedBuffer&) = delete; -}; +#include "StructuredSerialization.h" +namespace tns { namespace worker { -class Message { - public: - Message(MallocedBuffer&& payload = MallocedBuffer()); - Message(Message&& other) = default; - Message& operator=(Message&& other) = default; - Message& operator=(const Message&) = delete; - Message(const Message&) = delete; - v8::Maybe Serialize(v8::Isolate* isolate, - v8::Local context, - v8::Local input); - v8::MaybeLocal Deserialize(v8::Isolate* isolate, - v8::Local context); - // Internal method of Message that is called when a new SharedArrayBuffer - // object is encountered in the incoming value's structure. - void AddSharedArrayBuffer(std::shared_ptr backing_store); - // Internal method of Message that is called once serialization finishes - // and that transfers ownership of `data` to this message. - // void AddTransferable(std::unique_ptr&& data); - // Internal method of Message that is called when a new WebAssembly.Module - // object is encountered in the incoming value's structure. - // uint32_t AddWASMModule(v8::CompiledWasmModule&& mod); - // Internal method of Message that is called when a shared value is - // encountered for the first time in the incoming value's structure. - // void AdoptSharedValueConveyor(v8::SharedValueConveyor&& conveyor); - - // The host objects that will be transferred, as recorded by Serialize() - // (e.g. MessagePorts). - // Used for warning user about posting the target MessagePort to itself, - // which will as a side effect destroy the communication channel. - // const std::vector>& transferables() - // const { - // return transferables_; - // } - // bool has_transferables() const { - // return !transferables_.empty() || !array_buffers_.empty(); - // } +// What a worker posts: a value serialized on the sending isolate and read back +// on the receiving one. The mechanism is shared with structuredClone; only the +// host-object policy differs (see HostObjectPolicy). +using Message = tns::serialization::SerializedValue; - // void MemoryInfo(MemoryTracker* tracker) const override; - // - // SET_MEMORY_INFO_NAME(Message) - // SET_SELF_SIZE(Message) - private: - MallocedBuffer main_message_buf_; - // TODO(addaleax): Make this a std::variant to save storage size in the common - // case (which is that all of these vectors are empty) once that is available - // with C++17. - std::vector> array_buffers_; - std::vector> shared_array_buffers_; - // std::vector> transferables_; - // std::vector wasm_modules_; - // std::optional shared_value_conveyor_; -}; -}; // namespace worker +} // namespace worker } // namespace tns #endif /* Message_hpp */ diff --git a/NativeScript/runtime/Runtime.mm b/NativeScript/runtime/Runtime.mm index 4b9391ea..6ff6c072 100644 --- a/NativeScript/runtime/Runtime.mm +++ b/NativeScript/runtime/Runtime.mm @@ -18,6 +18,7 @@ #include "RuntimeConfig.h" #include "SimpleAllocator.h" #include "SpinLock.h" +#include "StructuredClone.h" #include "TSHelpers.h" #include "WeakRef.h" #include "Worker.h" @@ -362,6 +363,7 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { PromiseProxy::Init(context); Events::Init(context); ErrorEvents::Init(context); + StructuredClone::Init(context); Performance::Init(context); Console::Init(context); WeakRef::Init(context); diff --git a/NativeScript/runtime/StructuredClone.cpp b/NativeScript/runtime/StructuredClone.cpp new file mode 100644 index 00000000..7385b228 --- /dev/null +++ b/NativeScript/runtime/StructuredClone.cpp @@ -0,0 +1,61 @@ +#include "StructuredClone.h" + +#include "BuiltinLoader.h" +#include "Helpers.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& info) { + Isolate* isolate = info.GetIsolate(); + Local context = isolate->GetCurrentContext(); + Local value = + info.Length() > 0 ? info[0] : v8::Undefined(isolate).As(); + Local transferList = + info.Length() > 1 ? info[1] : v8::Undefined(isolate).As(); + + serialization::SerializedValue serialized; + if (serialized + .Serialize(isolate, context, value, transferList, + serialization::HostObjectPolicy::kReject) + .IsNothing()) { + return; + } + + Local result; + if (!serialized.Deserialize(isolate, context).ToLocal(&result)) { + return; + } + info.GetReturnValue().Set(result); +} + +} // namespace + +void StructuredClone::Init(Local context) { + Isolate* isolate = v8::Isolate::GetCurrent(); + + Local clone; + bool success = v8::Function::New(context, CloneCallback).ToLocal(&clone); + tns::Assert(success, isolate); + + Local binding = Object::New(isolate); + success = binding->Set(context, tns::ToV8String(isolate, "clone"), clone) + .FromMaybe(false); + tns::Assert(success, isolate); + + Local result; + success = + BuiltinLoader::RunBuiltin(context, BuiltinId::kStructuredClone, binding) + .ToLocal(&result); + tns::Assert(success, isolate); +} + +} // namespace tns diff --git a/NativeScript/runtime/StructuredClone.h b/NativeScript/runtime/StructuredClone.h new file mode 100644 index 00000000..071cfc46 --- /dev/null +++ b/NativeScript/runtime/StructuredClone.h @@ -0,0 +1,18 @@ +#ifndef StructuredClone_h +#define StructuredClone_h + +#include "Common.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 context); +}; + +} // namespace tns + +#endif /* StructuredClone_h */ diff --git a/NativeScript/runtime/StructuredSerialization.cpp b/NativeScript/runtime/StructuredSerialization.cpp new file mode 100644 index 00000000..a6c6602d --- /dev/null +++ b/NativeScript/runtime/StructuredSerialization.cpp @@ -0,0 +1,236 @@ +#include "StructuredSerialization.h" + +#include "Helpers.h" +#include "NativeScriptException.h" + +using namespace v8; + +namespace tns { +namespace serialization { + +void ThrowDataCloneError(Isolate* isolate, const std::string& message) { + NativeScriptException exception(isolate, message, "DataCloneError"); + exception.ReThrowToV8(isolate); +} + +namespace { + +class SerializerDelegate : public ValueSerializer::Delegate { + public: + SerializerDelegate(Isolate* isolate, HostObjectPolicy hostObjectPolicy, + std::vector>* sharedBuffers) + : isolate_(isolate), + hostObjectPolicy_(hostObjectPolicy), + sharedBuffers_(sharedBuffers) {} + + void ThrowDataCloneError(Local message) override { + serialization::ThrowDataCloneError(isolate_, + tns::ToString(isolate_, message)); + } + + Maybe WriteHostObject(Isolate* isolate, Local object) override { + if (hostObjectPolicy_ == HostObjectPolicy::kDegrade) { + // V8 has already written the kHostObject tag; writing no payload is what + // the zero-byte ReadHostObject below expects, and the value surfaces as + // an empty object. + return Just(true); + } + std::string name = tns::ToString(isolate, object->GetConstructorName()); + serialization::ThrowDataCloneError(isolate, + "#<" + name + "> could not be cloned."); + return Nothing(); + } + + // Shared memory is shared, not copied: the receiving isolate builds a new + // SharedArrayBuffer over this same backing store. + Maybe GetSharedArrayBufferId( + Isolate* isolate, Local sharedArrayBuffer) override { + std::shared_ptr backingStore = + sharedArrayBuffer->GetBackingStore(); + for (size_t i = 0; i < sharedBuffers_->size(); i++) { + if ((*sharedBuffers_)[i] == backingStore) { + return Just(static_cast(i)); + } + } + uint32_t id = static_cast(sharedBuffers_->size()); + sharedBuffers_->push_back(std::move(backingStore)); + return Just(id); + } + + // Overridden only to keep the DataCloneError name: with a delegate installed + // V8's default throws a plain Error straight onto the isolate. + bool AdoptSharedValueConveyor(Isolate* isolate, + SharedValueConveyor&& conveyor) override { + serialization::ThrowDataCloneError(isolate, + "shared value could not be cloned."); + return false; + } + + private: + Isolate* isolate_; + HostObjectPolicy hostObjectPolicy_; + std::vector>* sharedBuffers_; +}; + +class DeserializerDelegate : public ValueDeserializer::Delegate { + public: + explicit DeserializerDelegate( + const std::vector>* sharedBuffers) + : sharedBuffers_(sharedBuffers) {} + + // Counterpart of the kDegrade branch: consumes no bytes, so the stream stays + // balanced. Unreachable for a value written under kReject. + MaybeLocal ReadHostObject(Isolate* isolate) override { + return Object::New(isolate); + } + + MaybeLocal GetSharedArrayBufferFromId( + Isolate* isolate, uint32_t cloneId) override { + if (cloneId >= sharedBuffers_->size()) { + return MaybeLocal(); + } + return (*sharedBuffers_)[cloneId]; + } + + private: + const std::vector>* sharedBuffers_; +}; + +// Validates the transfer list and collects it in registration order. The +// detached and detachable checks are load-bearing rather than defensive: +// ArrayBuffer::Detach() aborts the process on a non-detachable buffer instead +// of reporting failure. +bool CollectTransferList(Isolate* isolate, Local context, + Local transferList, + std::vector>& transfers) { + if (transferList.IsEmpty() || transferList->IsUndefined() || + transferList->IsNull()) { + return true; + } + + if (!transferList->IsArray()) { + isolate->ThrowException(Exception::TypeError(tns::ToV8String( + isolate, "The transfer list must be an array of ArrayBuffers"))); + return false; + } + + Local list = transferList.As(); + uint32_t length = list->Length(); + for (uint32_t i = 0; i < length; i++) { + Local item; + if (!list->Get(context, i).ToLocal(&item)) { + return false; + } + if (!item->IsArrayBuffer()) { + ThrowDataCloneError(isolate, + "A value in the transfer list is not transferable"); + return false; + } + + Local buffer = item.As(); + for (const Local& existing : transfers) { + if (existing == buffer) { + ThrowDataCloneError( + isolate, "The transfer list contains the same ArrayBuffer twice"); + return false; + } + } + if (buffer->WasDetached() || !buffer->IsDetachable()) { + ThrowDataCloneError(isolate, + "An ArrayBuffer in the transfer list is detached and " + "cannot be transferred"); + return false; + } + + transfers.push_back(buffer); + } + return true; +} + +} // namespace + +Maybe SerializedValue::Serialize(Isolate* isolate, Local context, + Local input, + Local transferList, + HostObjectPolicy hostObjectPolicy) { + HandleScope handleScope(isolate); + Context::Scope contextScope(context); + tns::Assert(buffer_ == nullptr, isolate); + + std::vector> transfers; + if (!CollectTransferList(isolate, context, transferList, transfers)) { + return Nothing(); + } + + SerializerDelegate delegate(isolate, hostObjectPolicy, &sharedBuffers_); + ValueSerializer serializer(isolate, &delegate); + for (size_t i = 0; i < transfers.size(); i++) { + serializer.TransferArrayBuffer(static_cast(i), transfers[i]); + } + + serializer.WriteHeader(); + bool written = serializer.WriteValue(context, input).FromMaybe(false); + + // Release() hands ownership over whether or not the write succeeded, so the + // buffer is claimed either way rather than leaking with the serializer. + std::pair data = serializer.Release(); + std::unique_ptr owned(data.first); + if (!written) { + return Nothing(); + } + + // Only once the value is safely written does the memory change hands: claim + // each backing store before detaching, since detaching drops the buffer's own + // reference to it. + for (Local buffer : transfers) { + std::shared_ptr backingStore = buffer->GetBackingStore(); + // Detach rejects a null key only for a buffer carrying an + // [[ArrayBufferDetachKey]]: script cannot set one, this runtime never calls + // SetDetachKey, and the WebAssembly memory buffers that have one are + // already turned away as non-detachable above. Unreachable, then — but + // claiming success without moving the memory would hand the receiver an + // empty buffer, so the failure propagates carrying V8's TypeError, which + // names the key mismatch. + if (buffer->Detach(Local()).IsNothing()) { + return Nothing(); + } + transferredBuffers_.push_back(std::move(backingStore)); + } + + buffer_ = std::move(owned); + bufferSize_ = data.second; + return Just(true); +} + +MaybeLocal SerializedValue::Deserialize(Isolate* isolate, + Local context) { + Context::Scope contextScope(context); + EscapableHandleScope handleScope(isolate); + + std::vector> sharedBuffers; + for (const std::shared_ptr& backingStore : sharedBuffers_) { + sharedBuffers.push_back(SharedArrayBuffer::New(isolate, backingStore)); + } + + DeserializerDelegate delegate(&sharedBuffers); + ValueDeserializer deserializer(isolate, buffer_.get(), bufferSize_, + &delegate); + + for (size_t i = 0; i < transferredBuffers_.size(); i++) { + deserializer.TransferArrayBuffer( + static_cast(i), + ArrayBuffer::New(isolate, std::move(transferredBuffers_[i]))); + } + + if (deserializer.ReadHeader(context).IsNothing()) { + return MaybeLocal(); + } + Local result; + if (!deserializer.ReadValue(context).ToLocal(&result)) { + return MaybeLocal(); + } + return handleScope.Escape(result); +} + +} // namespace serialization +} // namespace tns diff --git a/NativeScript/runtime/StructuredSerialization.h b/NativeScript/runtime/StructuredSerialization.h new file mode 100644 index 00000000..860655e1 --- /dev/null +++ b/NativeScript/runtime/StructuredSerialization.h @@ -0,0 +1,80 @@ +#ifndef StructuredSerialization_h +#define StructuredSerialization_h + +#include +#include +#include +#include + +#include "Common.h" + +namespace tns { +namespace serialization { + +// What an entry point does with an object backed by native state — an ObjC +// wrapper, an interop pointer, a function reference. The two callers +// deliberately disagree, and this enum is the only place that disagreement is +// encoded. +enum class HostObjectPolicy { + // structuredClone: a DataCloneError, as the HTML spec requires. A clone whose + // native half was left behind would be a wrapper around nothing. + kReject, + // Worker postMessage: the value arrives as an empty object. This is what the + // runtime has always shipped and what the cross-runtime worker suite asserts; + // moving it to kReject is a breaking change both runtimes have to make + // together. + kDegrade, +}; + +// Throws the runtime's DataCloneError. There is no DOMException here, so it is +// an Error carrying that name — routed through NativeScriptException so the +// object is shaped like every other error the runtime raises. +void ThrowDataCloneError(v8::Isolate* isolate, const std::string& message); + +// A value serialized out of one isolate, plus the memory that travels with it. +// Serializing and deserializing are separate halves because a worker message is +// read back on a different isolate than it was written on, while +// structuredClone round-trips on a single one. +class SerializedValue { + public: + SerializedValue() = default; + SerializedValue(SerializedValue&&) = default; + SerializedValue& operator=(SerializedValue&&) = default; + SerializedValue(const SerializedValue&) = delete; + SerializedValue& operator=(const SerializedValue&) = delete; + + // Serializes `input`, moving out of this isolate every ArrayBuffer named by + // `transferList` (an Array, or undefined/null for none). Returns Nothing with + // an exception pending: a TypeError when the transfer list is not an Array, a + // DataCloneError for anything wrong with its entries or with the value. + v8::Maybe Serialize(v8::Isolate* isolate, + v8::Local context, + v8::Local input, + v8::Local transferList, + HostObjectPolicy hostObjectPolicy); + + // Reads the value back into `context`. Transferred buffers are consumed, so + // this runs once per serialized value. + v8::MaybeLocal Deserialize(v8::Isolate* isolate, + v8::Local context); + + private: + struct FreeDeleter { + void operator()(void* pointer) const { std::free(pointer); } + }; + + // The serializer grows this with realloc() through its delegate's default + // allocator, so it is free()d rather than deleted. + std::unique_ptr buffer_; + size_t bufferSize_ = 0; + // Backing stores moved out of the sending isolate. Each is re-wrapped in a + // fresh ArrayBuffer under the same transfer id on the receiving side. + std::vector> transferredBuffers_; + // Backing stores shared with — not moved from — the sending isolate. + std::vector> sharedBuffers_; +}; + +} // namespace serialization +} // namespace tns + +#endif /* StructuredSerialization_h */ diff --git a/NativeScript/runtime/Worker.h b/NativeScript/runtime/Worker.h index f1663336..c532dddd 100644 --- a/NativeScript/runtime/Worker.h +++ b/NativeScript/runtime/Worker.h @@ -17,8 +17,8 @@ class Worker { static void TerminateCallback(const v8::FunctionCallbackInfo& info); static void OnMessageCallback(v8::Isolate* isolate, v8::Local receiver, std::shared_ptr message); static void PostMessageToMainCallback(const v8::FunctionCallbackInfo& info); - static void CloseWorkerCallback(const v8::FunctionCallbackInfo& info); - static v8::Local Serialize(v8::Isolate* isolate, v8::Local value, v8::Local& error); + static void CloseWorkerCallback( + const v8::FunctionCallbackInfo& info); static void SetWorkerId(v8::Isolate* isolate, int workerId); static int GetWorkerId(v8::Isolate* isolate, v8::Local global); }; diff --git a/NativeScript/runtime/Worker.mm b/NativeScript/runtime/Worker.mm index f59d4357..dbb02997 100644 --- a/NativeScript/runtime/Worker.mm +++ b/NativeScript/runtime/Worker.mm @@ -271,7 +271,7 @@ throw NativeScriptException( throw NativeScriptException("Not enough arguments."); } - if (info.Length() > 1) { + if (info.Length() > 2) { throw NativeScriptException("Too many arguments passed."); } @@ -283,12 +283,13 @@ throw NativeScriptException( return; } - // Local error; - // Local result = Worker::Serialize(isolate, info[0], error); - // if (result.IsEmpty()) { - // isolate->ThrowException(error); - // return; - // } + // Resolved before anything is serialized: serializing a transfer list + // detaches the caller's buffers, so bailing out afterwards would destroy + // their contents without ever delivering the message. + auto runtime = static_cast(state->GetIsolate()->GetData(Constants::RUNTIME_SLOT)); + if (runtime == nullptr) { + return; + } auto context = Caches::Get(isolate)->GetContext(); auto message = std::make_shared(); @@ -300,13 +301,16 @@ throw NativeScriptException( success = obj->Set(context, tns::ToV8String(isolate, "data"), info[0]).FromMaybe(false); tns::Assert(success, isolate); - message->Serialize(isolate, context, obj); - // std::string message = tns::ToString(isolate, result); - - auto runtime = static_cast(state->GetIsolate()->GetData(Constants::RUNTIME_SLOT)); - if (runtime == nullptr) { + Local transferList = info.Length() > 1 ? info[1] : v8::Undefined(isolate).As(); + if (message + ->Serialize(isolate, context, obj, 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; } + tns::ExecuteOnRunLoop(runtime->RuntimeLoop(), [state, message]() { Isolate* isolate = state->GetIsolate(); v8::Locker locker(isolate); @@ -330,7 +334,7 @@ throw NativeScriptException( return; } - if (info.Length() > 1) { + if (info.Length() > 2) { throw NativeScriptException("Too many arguments passed."); return; } @@ -353,15 +357,16 @@ throw NativeScriptException( success = obj->Set(context, tns::ToV8String(isolate, "data"), info[0]).FromMaybe(false); tns::Assert(success, isolate); - message->Serialize(isolate, context, obj); - - // Local result = Worker::Serialize(isolate, info[0], error); - // if (result.IsEmpty()) { - // isolate->ThrowException(error); - // return; - // } + Local transferList = info.Length() > 1 ? info[1] : v8::Undefined(isolate).As(); + if (message + ->Serialize(isolate, context, obj, 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; + } - // std::string message = tns::ToString(isolate, result); worker->PostMessage(message); } catch (NativeScriptException& ex) { ex.ReThrowToV8(isolate); @@ -434,30 +439,6 @@ throw NativeScriptException( worker->Terminate(); } -Local Worker::Serialize(Isolate* isolate, Local value, Local& error) { - Local context = isolate->GetCurrentContext(); - Local objTemplate = ObjectTemplate::New(isolate); - - Local obj; - bool success = objTemplate->NewInstance(context).ToLocal(&obj); - tns::Assert(success, isolate); - - success = obj->Set(context, tns::ToV8String(isolate, "data"), value).FromMaybe(false); - tns::Assert(success, isolate); - - Local result; - TryCatch tc(isolate); - success = v8::JSON::Stringify(context, obj).ToLocal(&result); - if (!success && tc.HasCaught()) { - error = tc.Exception(); - return Local(); - } - - tns::Assert(success, isolate); - - return result.As(); -} - void Worker::SetWorkerId(Isolate* isolate, int workerId) { // Runs on the worker thread right after Runtime::Init(), whose Isolate::Scope // has already been unwound -- so this has to enter the isolate itself, and diff --git a/NativeScript/runtime/js/structured-clone.js b/NativeScript/runtime/js/structured-clone.js new file mode 100644 index 00000000..3b3243f4 --- /dev/null +++ b/NativeScript/runtime/js/structured-clone.js @@ -0,0 +1,110 @@ +"use strict"; + +// WHATWG structuredClone(value, { transfer }): the argument coercion and the +// WebIDL sequence handling for `transfer`; the clone itself is native +// (v8::ValueSerializer round-tripped in this isolate). +// +// Deviations from the HTML spec, both forced by the platform: +// - There is no DOMException here, so a clone failure throws an Error whose +// `name` is "DataCloneError" (same shape as the native-exception errors in +// docs/error-handling.md). `instanceof DOMException` checks cannot work. +// - Only ArrayBuffers are transferable. MessagePort, ImageBitmap and the +// native/interop wrapper objects have no serialization form in this runtime, +// so they are rejected rather than half-supported. + +const { clone } = binding; +const { + ArrayBufferPrototypeGetByteLength, + ArrayPrototypePush, + Error, + FunctionPrototypeCall, + SymbolIterator, + TypeError, +} = primordials; + +var g = globalThis; + +function dataCloneError(message) { + var e = new Error(message); + e.name = "DataCloneError"; + return e; +} + +// Brand check through the captured byteLength getter: it is the one thing only +// a real ArrayBuffer has, and it cannot be faked by a `Symbol.toStringTag` or a +// forged prototype. SharedArrayBuffer has its own getter and so fails here, +// which is what the spec wants — a SAB is not transferable. +function isArrayBuffer(value) { + if (value === null || typeof value !== "object") { + return false; + } + try { + ArrayBufferPrototypeGetByteLength(value); + return true; + } catch (notAnArrayBuffer) { + return false; + } +} + +// WebIDL `sequence` conversion: only an object with a callable +// @@iterator qualifies, which is why a string primitive is a TypeError even +// though strings are iterable. +function toTransferList(value) { + if (value === null || (typeof value !== "object" && typeof value !== "function")) { + throw new TypeError("structuredClone: transfer is not iterable"); + } + var method = value[SymbolIterator]; + if (typeof method !== "function") { + throw new TypeError("structuredClone: transfer is not iterable"); + } + + var iterator = FunctionPrototypeCall(method, value); + if (iterator === null || typeof iterator !== "object") { + throw new TypeError("structuredClone: transfer is not iterable"); + } + + // The iterator record captures `next` once, when it is created — re-reading + // it per step would expose a `next` that changes mid-iteration. + var next = iterator.next; + if (typeof next !== "function") { + throw new TypeError("structuredClone: transfer is not iterable"); + } + + var list = []; + for (;;) { + var step = FunctionPrototypeCall(next, iterator); + if (step === null || typeof step !== "object") { + throw new TypeError("structuredClone: transfer iterator returned a non-object"); + } + if (step.done) { + break; + } + var item = step.value; + if (!isArrayBuffer(item)) { + throw dataCloneError("structuredClone: value in transfer list is not transferable"); + } + ArrayPrototypePush(list, item); + } + return list; +} + +// `options` is defaulted rather than merely optional so that the function's +// reported arity is 1, as the IDL requires. +g.structuredClone = function structuredClone(value, options = undefined) { + if (arguments.length < 1) { + throw new TypeError("structuredClone: 1 argument required, but only 0 present"); + } + + var transfer; + if (options !== undefined && options !== null) { + if (typeof options !== "object" && typeof options !== "function") { + throw new TypeError("structuredClone: options is not an object"); + } + var requested = options.transfer; + if (requested !== undefined) { + transfer = toTransferList(requested); + } + } + + return clone(value, transfer); +}; diff --git a/TestRunner/app/tests/RuntimeImplementedAPIs.js b/TestRunner/app/tests/RuntimeImplementedAPIs.js index ea87ea2c..f30be4cf 100644 --- a/TestRunner/app/tests/RuntimeImplementedAPIs.js +++ b/TestRunner/app/tests/RuntimeImplementedAPIs.js @@ -60,3 +60,12 @@ describe("queueMicrotask", () => { }, 0); }); }); + +// 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", () => { + it("is implemented by this runtime", () => { + expect(typeof structuredClone).toBe("function"); + }); +}); diff --git a/docs/README.md b/docs/README.md index 3ed370a3..74eba116 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,8 @@ work must share, and the documented spec deviations. - [Error handling](error-handling.md) — global `error`/`unhandledrejection` events, `reportError`, catching native exceptions in JS (`error.nativeException`), forwarding JS throws to native (`interop.escapeException`), JS stacks on `NSException`, 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`. + ## Knowledge Notes on work that is done, kept because the reasoning is expensive to diff --git a/docs/structured-clone.md b/docs/structured-clone.md new file mode 100644 index 00000000..1ed42734 --- /dev/null +++ b/docs/structured-clone.md @@ -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 (ObjC wrappers, pointers, function references), 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 that predates the V8 port, 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 `NativeScript/runtime/StructuredSerialization.h` — and unifying the two on rejection is a breaking change that needs the Android 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"` — the same shape used for native exceptions (see [Error handling](error-handling.md)). 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`). diff --git a/tools/js2c-inputs.xcfilelist b/tools/js2c-inputs.xcfilelist index 9acaedec..c36ae4c7 100644 --- a/tools/js2c-inputs.xcfilelist +++ b/tools/js2c-inputs.xcfilelist @@ -12,5 +12,6 @@ $(SRCROOT)/NativeScript/runtime/js/ns-util.js $(SRCROOT)/NativeScript/runtime/js/performance.js $(SRCROOT)/NativeScript/runtime/js/promise-proxy.js $(SRCROOT)/NativeScript/runtime/js/require-factory.js +$(SRCROOT)/NativeScript/runtime/js/structured-clone.js $(SRCROOT)/NativeScript/runtime/js/ts-helpers.js $(SRCROOT)/NativeScript/runtime/js/weak-ref.js diff --git a/v8ios.xcodeproj/project.pbxproj b/v8ios.xcodeproj/project.pbxproj index c3ad68a1..91c741cd 100644 --- a/v8ios.xcodeproj/project.pbxproj +++ b/v8ios.xcodeproj/project.pbxproj @@ -15,7 +15,6 @@ 3C1850542A6DCB2D002ACC81 /* Timers.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3C1850522A6DCB2D002ACC81 /* Timers.cpp */; }; 3C1850552A6DCB2D002ACC81 /* Timers.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3C1850532A6DCB2D002ACC81 /* Timers.hpp */; }; 3C48F68D2F57905500C14231 /* json.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3C48F68B2F57905500C14231 /* json.hpp */; }; - 3C5333342B0E683100BE0C47 /* Message.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3C5333322B0E683100BE0C47 /* Message.cpp */; }; 3C5333352B0E683100BE0C47 /* Message.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3C5333332B0E683100BE0C47 /* Message.hpp */; }; 3C78BA5C2A0D600100C20A88 /* ModuleBinding.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3C78BA5A2A0D600100C20A88 /* ModuleBinding.cpp */; }; 3C78BA5D2A0D600100C20A88 /* ModuleBinding.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3C78BA5B2A0D600100C20A88 /* ModuleBinding.hpp */; }; @@ -295,6 +294,8 @@ 4A5C201A2E2B000100000007 /* RuntimeBuiltins.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000100000003 /* RuntimeBuiltins.cpp */; }; 4A5C201A2E2B000300000006 /* Performance.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000300000001 /* Performance.cpp */; }; 4A5C201A2E2B000200000006 /* NsBuiltinModules.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000200000001 /* NsBuiltinModules.cpp */; }; + 4A5C201A2E2B000400000003 /* StructuredClone.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000400000001 /* StructuredClone.cpp */; }; + 4A5C201A2E2B000500000003 /* StructuredSerialization.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000500000001 /* StructuredSerialization.cpp */; }; C2DDEB93229EAC8300345BFE /* ArgConverter.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB6A229EAC8200345BFE /* ArgConverter.mm */; }; C2DDEB94229EAC8300345BFE /* Console.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB6B229EAC8200345BFE /* Console.cpp */; }; C2DDEB95229EAC8300345BFE /* SetTimeout.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB6C229EAC8200345BFE /* SetTimeout.cpp */; }; @@ -467,7 +468,6 @@ 3C1850522A6DCB2D002ACC81 /* Timers.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Timers.cpp; sourceTree = ""; }; 3C1850532A6DCB2D002ACC81 /* Timers.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Timers.hpp; sourceTree = ""; }; 3C48F68B2F57905500C14231 /* json.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = json.hpp; sourceTree = ""; }; - 3C5333322B0E683100BE0C47 /* Message.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Message.cpp; sourceTree = ""; }; 3C5333332B0E683100BE0C47 /* Message.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Message.hpp; sourceTree = ""; }; 3C78BA5A2A0D600100C20A88 /* ModuleBinding.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ModuleBinding.cpp; sourceTree = ""; }; 3C78BA5B2A0D600100C20A88 /* ModuleBinding.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ModuleBinding.hpp; sourceTree = ""; }; @@ -816,6 +816,10 @@ 4A5C201A2E2B000100000003 /* RuntimeBuiltins.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = RuntimeBuiltins.cpp; path = generated/RuntimeBuiltins.cpp; sourceTree = ""; }; 4A5C201A2E2B000100000004 /* RuntimeBuiltins.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RuntimeBuiltins.h; path = generated/RuntimeBuiltins.h; sourceTree = ""; }; 4A5C201A2E2B000100000005 /* js */ = {isa = PBXFileReference; lastKnownFileType = folder; path = js; sourceTree = ""; }; + 4A5C201A2E2B000400000001 /* StructuredClone.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StructuredClone.cpp; sourceTree = ""; }; + 4A5C201A2E2B000400000002 /* StructuredClone.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StructuredClone.h; sourceTree = ""; }; + 4A5C201A2E2B000500000001 /* StructuredSerialization.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StructuredSerialization.cpp; sourceTree = ""; }; + 4A5C201A2E2B000500000002 /* StructuredSerialization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StructuredSerialization.h; sourceTree = ""; }; 4A5C201A2E2B000300000001 /* Performance.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Performance.cpp; sourceTree = ""; }; 4A5C201A2E2B000300000002 /* Performance.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Performance.h; sourceTree = ""; }; C2DDEB6A229EAC8200345BFE /* ArgConverter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ArgConverter.mm; sourceTree = ""; }; @@ -1521,6 +1525,10 @@ C2C8EE7922CF64E4001F8CEC /* SimpleAllocator.h */, C2C8EE7822CF64E4001F8CEC /* SimpleAllocator.cpp */, C2DDEB87229EAC8300345BFE /* StringHasher.h */, + 4A5C201A2E2B000400000002 /* StructuredClone.h */, + 4A5C201A2E2B000400000001 /* StructuredClone.cpp */, + 4A5C201A2E2B000500000002 /* StructuredSerialization.h */, + 4A5C201A2E2B000500000001 /* StructuredSerialization.cpp */, C2F4D0AC232F85E20008A2EB /* SymbolIterator.h */, C2F4D0AB232F85E20008A2EB /* SymbolIterator.mm */, C2DDEB7F229EAC8200345BFE /* SymbolLoader.h */, @@ -1555,7 +1563,6 @@ 3C78BA5B2A0D600100C20A88 /* ModuleBinding.hpp */, 3C1850522A6DCB2D002ACC81 /* Timers.cpp */, 3C1850532A6DCB2D002ACC81 /* Timers.hpp */, - 3C5333322B0E683100BE0C47 /* Message.cpp */, 3C5333332B0E683100BE0C47 /* Message.hpp */, ); path = runtime; @@ -2293,6 +2300,8 @@ C2DDEB92229EAC8300345BFE /* WeakRef.cpp in Sources */, 4A5C201A2E2B000100000006 /* BuiltinLoader.cpp in Sources */, 4A5C201A2E2B000200000006 /* NsBuiltinModules.cpp in Sources */, + 4A5C201A2E2B000400000003 /* StructuredClone.cpp in Sources */, + 4A5C201A2E2B000500000003 /* StructuredSerialization.cpp in Sources */, 4A5C201A2E2B000100000007 /* RuntimeBuiltins.cpp in Sources */, C2A5F86A2359AEB600074AFA /* ExtVector.cpp in Sources */, AA8C47B22E27114300649BF5 /* ModuleInternalCallbacks.mm in Sources */, @@ -2342,7 +2351,6 @@ F1F30E752B58FC74006A62C0 /* URLSearchParamsImpl.cpp in Sources */, F1F30E742B58FC74006A62C0 /* URLImpl.cpp in Sources */, C2DDEBA3229EAC8300345BFE /* ArrayAdapter.mm in Sources */, - 3C5333342B0E683100BE0C47 /* Message.cpp in Sources */, C2C8EE7422CE3266001F8CEC /* ConcurrentQueue.cpp in Sources */, C2DDEBA0229EAC8300345BFE /* Interop.mm in Sources */, C2D7E9D623F42C1100DB289C /* PromiseProxy.cpp in Sources */,