From 1fde98bb4fa5cab0d060994768ebd055ce6fbf2c Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 26 Jan 2017 21:38:11 -0800 Subject: [PATCH] v8: expose new V8 serialization API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose the new serialization API that was added in V8 5.5 to userland. The JS API is virtually a direct copy of what V8 provides on the C++ level. This is useful Node as a possible replacement for some internals that currently use JSON, like IPC, but is likely to be useful to general userland code as well. PR-URL: https://github.com/nodejs/node/pull/11048 Reviewed-By: Michaël Zasso Reviewed-By: Ben Noordhuis --- doc/api/v8.md | 251 +++++++++ lib/v8.js | 119 +++++ node.gyp | 1 + src/env.h | 4 + src/node_serdes.cc | 483 ++++++++++++++++++ .../test-v8-serdes-sharedarraybuffer.js | 27 + test/parallel/test-v8-serdes.js | 120 +++++ 7 files changed, 1005 insertions(+) create mode 100644 src/node_serdes.cc create mode 100644 test/parallel/test-v8-serdes-sharedarraybuffer.js create mode 100644 test/parallel/test-v8-serdes.js diff --git a/doc/api/v8.md b/doc/api/v8.md index 173d0abeef..8d5d428e56 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -157,3 +157,254 @@ setTimeout(function() { v8.setFlagsFromString('--notrace_gc'); }, 60e3); [`vm.Script`]: vm.html#vm_new_vm_script_code_options [here]: https://github.com/thlorenz/v8-flags/blob/master/flags-0.11.md [`GetHeapSpaceStatistics`]: https://v8docs.nodesource.com/node-5.0/d5/dda/classv8_1_1_isolate.html#ac673576f24fdc7a33378f8f57e1d13a4 + +## Serialization API + +> Stability: 1 - Experimental + +The serialization API provides means of serializing JavaScript values in a way +that is compatible with the [HTML structured clone algorithm][]. +The format is backward-compatible (i.e. safe to store to disk). + +*Note*: This API is under development, and changes (including incompatible +changes to the API or wire format) may occur until this warning is removed. + +### v8.serialize(value) + + +* Returns: {Buffer} + +Uses a [`DefaultSerializer`][] to serialize `value` into a buffer. + +### v8.deserialize(buffer) + + +* `buffer` {Buffer|Uint8Array} A buffer returned by [`serialize()`][]. + +Uses a [`DefaultDeserializer`][] with default options to read a JS value +from a buffer. + +### class: v8.Serializer + + +#### new Serializer() +Creates a new `Serializer` object. + +#### serializer.writeHeader() + +Writes out a header, which includes the serialization format version. + +#### serializer.writeValue(value) + +Serializes a JavaScript value and adds the serialized representation to the +internal buffer. + +This throws an error if `value` cannot be serialized. + +#### serializer.releaseBuffer() + +Returns the stored internal buffer. This serializer should not be used once +the buffer is released. Calling this method results in undefined behavior +if a previous write has failed. + +#### serializer.transferArrayBuffer(id, arrayBuffer) + +* `id` {integer} A 32-bit unsigned integer. +* `arrayBuffer` {ArrayBuffer} An `ArrayBuffer` instance. + +Marks an `ArrayBuffer` as havings its contents transferred out of band. +Pass the corresponding `ArrayBuffer` in the deserializing context to +[`deserializer.transferArrayBuffer()`][]. + +#### serializer.writeUint32(value) + +* `value` {integer} + +Write a raw 32-bit unsigned integer. +For use inside of a custom [`serializer._writeHostObject()`][]. + +#### serializer.writeUint64(hi, lo) + +* `hi` {integer} +* `lo` {integer} + +Write a raw 64-bit unsigned integer, split into high and low 32-bit parts. +For use inside of a custom [`serializer._writeHostObject()`][]. + +#### serializer.writeDouble(value) + +* `value` {number} + +Write a JS `number` value. +For use inside of a custom [`serializer._writeHostObject()`][]. + +#### serializer.writeRawBytes(buffer) + +* `buffer` {Buffer|Uint8Array} + +Write raw bytes into the serializer’s internal buffer. The deserializer +will require a way to compute the length of the buffer. +For use inside of a custom [`serializer._writeHostObject()`][]. + +#### serializer.\_writeHostObject(object) + +* `object` {Object} + +This method is called to write some kind of host object, i.e. an object created +by native C++ bindings. If it is not possible to serialize `object`, a suitable +exception should be thrown. + +This method is not present on the `Serializer` class itself but can be provided +by subclasses. + +#### serializer.\_getDataCloneError(message) + +* `message` {string} + +This method is called to generate error objects that will be thrown when an +object can not be cloned. + +This method defaults to the [`Error`][] constructor and can be be overridden on +subclasses. + +#### serializer.\_getSharedArrayBufferId(sharedArrayBuffer) + +* `sharedArrayBuffer` {SharedArrayBuffer} + +This method is called when the serializer is going to serialize a +`SharedArrayBuffer` object. It must return an unsigned 32-bit integer ID for +the object, using the same ID if this `SharedArrayBuffer` has already been +serialized. When deserializing, this ID will be passed to +[`deserializer.transferArrayBuffer()`][]. + +If the object cannot be serialized, an exception should be thrown. + +This method is not present on the `Serializer` class itself but can be provided +by subclasses. + +#### serializer.\_setTreatArrayBufferViewsAsHostObjects(flag) + +* `flag` {boolean} + +Indicate whether to treat `TypedArray` and `DataView` objects as +host objects, i.e. pass them to [`serializer._writeHostObject`][]. + +The default is not to treat those objects as host objects. + +### class: v8.Deserializer + + +#### new Deserializer(buffer) + +* `buffer` {Buffer|Uint8Array} A buffer returned by [`serializer.releaseBuffer()`][]. + +Creates a new `Deserializer` object. + +#### deserializer.readHeader() + +Reads and validates a header (including the format version). +May, for example, reject an invalid or unsupported wire format. In that case, +an `Error` is thrown. + +#### deserializer.readValue() + +Deserializes a JavaScript value from the buffer and returns it. + +#### deserializer.transferArrayBuffer(id, arrayBuffer) + +* `id` {integer} A 32-bit unsigned integer. +* `arrayBuffer` {ArrayBuffer|SharedArrayBuffer} An `ArrayBuffer` instance. + +Marks an `ArrayBuffer` as havings its contents transferred out of band. +Pass the corresponding `ArrayBuffer` in the serializing context to +[`serializer.transferArrayBuffer()`][] (or return the `id` from +[`serializer._getSharedArrayBufferId()`][] in the case of `SharedArrayBuffer`s). + +#### deserializer.getWireFormatVersion() + +* Returns: {integer} + +Reads the underlying wire format version. Likely mostly to be useful to +legacy code reading old wire format versions. May not be called before +`.readHeader()`. + +#### deserializer.readUint32() + +* Returns: {integer} + +Read a raw 32-bit unsigned integer and return it. +For use inside of a custom [`deserializer._readHostObject()`][]. + +#### deserializer.readUint64() + +* Returns: {Array} + +Read a raw 64-bit unsigned integer and return it as an array `[hi, lo]` +with two 32-bit unsigned integer entries. +For use inside of a custom [`deserializer._readHostObject()`][]. + +#### deserializer.readDouble() + +* Returns: {number} + +Read a JS `number` value. +For use inside of a custom [`deserializer._readHostObject()`][]. + +#### deserializer.readRawBytes(length) + +* Returns: {Buffer} + +Read raw bytes from the deserializer’s internal buffer. The `length` parameter +must correspond to the length of the buffer that was passed to +[`serializer.writeRawBytes()`][]. +For use inside of a custom [`deserializer._readHostObject()`][]. + +#### deserializer.\_readHostObject() + +This method is called to read some kind of host object, i.e. an object that is +created by native C++ bindings. If it is not possible to deserialize the data, +a suitable exception should be thrown. + +This method is not present on the `Deserializer` class itself but can be +provided by subclasses. + +### class: v8.DefaultSerializer + + +A subclass of [`Serializer`][] that serializes `TypedArray` +(in particular [`Buffer`][]) and `DataView` objects as host objects, and only +stores the part of their underlying `ArrayBuffer`s that they are referring to. + +### class: v8.DefaultDeserializer + + +A subclass of [`Deserializer`][] corresponding to the format written by +[`DefaultSerializer`][]. + +[`Buffer`]: buffer.html +[`Error`]: errors.html#errors_class_error +[`deserializer.transferArrayBuffer()`]: #v8_deserializer_transferarraybuffer_id_arraybuffer +[`deserializer._readHostObject()`]: #v8_deserializer_readhostobject +[`serializer.transferArrayBuffer()`]: #v8_serializer_transferarraybuffer_id_arraybuffer +[`serializer.releaseBuffer()`]: #v8_serializer_releasebuffer +[`serializer.writeRawBytes()`]: #v8_serializer_writerawbytes_buffer +[`serializer._writeHostObject()`]: #v8_serializer_writehostobject_object +[`serializer._getSharedArrayBufferId()`]: #v8_serializer_getsharedarraybufferid_sharedarraybuffer +[`Serializer`]: #v8_class_v8_serializer +[`Deserializer`]: #v8_class_v8_deserializer +[`DefaultSerializer`]: #v8_class_v8_defaultserializer +[`DefaultDeserializer`]: #v8_class_v8_defaultdeserializer +[`serialize()`]: #v8_v8_serialize_value +[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm diff --git a/lib/v8.js b/lib/v8.js index a11baacdc9..7790f9b334 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -14,7 +14,14 @@ 'use strict'; +const Buffer = require('buffer').Buffer; + const v8binding = process.binding('v8'); +const serdesBinding = process.binding('serdes'); +const bufferBinding = process.binding('buffer'); + +const { objectToString } = require('internal/util'); +const { FastBuffer } = require('internal/buffer'); // Properties for heap statistics buffer extraction. const heapStatisticsBuffer = @@ -80,3 +87,115 @@ exports.getHeapSpaceStatistics = function() { return heapSpaceStatistics; }; + +/* V8 serialization API */ + +const Serializer = exports.Serializer = serdesBinding.Serializer; +const Deserializer = exports.Deserializer = serdesBinding.Deserializer; + +/* JS methods for the base objects */ +Serializer.prototype._getDataCloneError = Error; + +Deserializer.prototype.readRawBytes = function(length) { + const offset = this._readRawBytes(length); + // `this.buffer` can be a Buffer or a plain Uint8Array, so just calling + // `.slice()` doesn't work. + return new FastBuffer(this.buffer.buffer, + this.buffer.byteOffset + offset, + length); +}; + +/* Keep track of how to handle different ArrayBufferViews. + * The default Serializer for Node does not use the V8 methods for serializing + * those objects because Node's `Buffer` objects use pooled allocation in many + * cases, and their underlying `ArrayBuffer`s would show up in the + * serialization. Because a) those may contain sensitive data and the user + * may not be aware of that and b) they are often much larger than the `Buffer` + * itself, custom serialization is applied. */ +const arrayBufferViewTypes = [Int8Array, Uint8Array, Uint8ClampedArray, + Int16Array, Uint16Array, Int32Array, Uint32Array, + Float32Array, Float64Array, DataView]; + +const arrayBufferViewTypeToIndex = new Map(); + +{ + const dummy = new ArrayBuffer(); + for (const [i, ctor] of arrayBufferViewTypes.entries()) { + const tag = objectToString(new ctor(dummy)); + arrayBufferViewTypeToIndex.set(tag, i); + } +} + +const bufferConstructorIndex = arrayBufferViewTypes.push(Buffer) - 1; + +class DefaultSerializer extends Serializer { + constructor() { + super(); + + this._setTreatArrayBufferViewsAsHostObjects(true); + } + + _writeHostObject(abView) { + let i = 0; + if (abView.constructor === Buffer) { + i = bufferConstructorIndex; + } else { + const tag = objectToString(abView); + i = arrayBufferViewTypeToIndex.get(tag); + + if (i === undefined) { + throw this._getDataCloneError(`Unknown host object type: ${tag}`); + } + } + this.writeUint32(i); + this.writeUint32(abView.byteLength); + this.writeRawBytes(new Uint8Array(abView.buffer, + abView.byteOffset, + abView.byteLength)); + } +} + +exports.DefaultSerializer = DefaultSerializer; + +class DefaultDeserializer extends Deserializer { + constructor(buffer) { + super(buffer); + } + + _readHostObject() { + const typeIndex = this.readUint32(); + const ctor = arrayBufferViewTypes[typeIndex]; + const byteLength = this.readUint32(); + const byteOffset = this._readRawBytes(byteLength); + const BYTES_PER_ELEMENT = ctor.BYTES_PER_ELEMENT || 1; + + const offset = this.buffer.byteOffset + byteOffset; + if (offset % BYTES_PER_ELEMENT === 0) { + return new ctor(this.buffer.buffer, + offset, + byteLength / BYTES_PER_ELEMENT); + } else { + // Copy to an aligned buffer first. + const copy = Buffer.allocUnsafe(byteLength); + bufferBinding.copy(this.buffer, copy, 0, offset, offset + byteLength); + return new ctor(copy.buffer, + copy.byteOffset, + byteLength / BYTES_PER_ELEMENT); + } + } +} + +exports.DefaultDeserializer = DefaultDeserializer; + +exports.serialize = function serialize(value) { + const ser = new DefaultSerializer(); + ser.writeHeader(); + ser.writeValue(value); + return ser.releaseBuffer(); +}; + +exports.deserialize = function deserialize(buffer) { + const der = new DefaultDeserializer(buffer); + der.readHeader(); + return der.readValue(); +}; diff --git a/node.gyp b/node.gyp index b08bda0dad..597df9d126 100644 --- a/node.gyp +++ b/node.gyp @@ -177,6 +177,7 @@ 'src/node_main.cc', 'src/node_os.cc', 'src/node_revert.cc', + 'src/node_serdes.cc', 'src/node_url.cc', 'src/node_util.cc', 'src/node_v8.cc', diff --git a/src/env.h b/src/env.h index abb0e6d0e5..6fd1051a21 100644 --- a/src/env.h +++ b/src/env.h @@ -127,6 +127,8 @@ namespace node { V(fingerprint_string, "fingerprint") \ V(flags_string, "flags") \ V(get_string, "get") \ + V(get_data_clone_error_string, "_getDataCloneError") \ + V(get_shared_array_buffer_id_string, "_getSharedArrayBufferId") \ V(gid_string, "gid") \ V(handle_string, "handle") \ V(homedir_string, "homedir") \ @@ -189,6 +191,7 @@ namespace node { V(priority_string, "priority") \ V(produce_cached_data_string, "produceCachedData") \ V(raw_string, "raw") \ + V(read_host_object_string, "_readHostObject") \ V(readable_string, "readable") \ V(received_shutdown_string, "receivedShutdown") \ V(refresh_string, "refresh") \ @@ -237,6 +240,7 @@ namespace node { V(windows_verbatim_arguments_string, "windowsVerbatimArguments") \ V(wrap_string, "wrap") \ V(writable_string, "writable") \ + V(write_host_object_string, "_writeHostObject") \ V(write_queue_size_string, "writeQueueSize") \ V(x_forwarded_string, "x-forwarded-for") \ V(zero_return_string, "ZERO_RETURN") \ diff --git a/src/node_serdes.cc b/src/node_serdes.cc new file mode 100644 index 0000000000..144de1695e --- /dev/null +++ b/src/node_serdes.cc @@ -0,0 +1,483 @@ +#include "node.h" +#include "node_buffer.h" +#include "base-object.h" +#include "base-object-inl.h" +#include "env.h" +#include "env-inl.h" +#include "v8.h" + +namespace node { + +using v8::Array; +using v8::ArrayBuffer; +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Integer; +using v8::Isolate; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::SharedArrayBuffer; +using v8::String; +using v8::Value; +using v8::ValueDeserializer; +using v8::ValueSerializer; + +class SerializerContext : public BaseObject, + public ValueSerializer::Delegate { + public: + SerializerContext(Environment* env, + Local wrap); + + ~SerializerContext() {} + + void ThrowDataCloneError(Local message) override; + Maybe WriteHostObject(Isolate* isolate, Local object) override; + Maybe GetSharedArrayBufferId( + Isolate* isolate, Local shared_array_buffer) override; + + static void SetTreatArrayBufferViewsAsHostObjects( + const FunctionCallbackInfo& args); + + static void New(const FunctionCallbackInfo& args); + static void WriteHeader(const FunctionCallbackInfo& args); + static void WriteValue(const FunctionCallbackInfo& args); + static void ReleaseBuffer(const FunctionCallbackInfo& args); + static void TransferArrayBuffer(const FunctionCallbackInfo& args); + static void WriteUint32(const FunctionCallbackInfo& args); + static void WriteUint64(const FunctionCallbackInfo& args); + static void WriteDouble(const FunctionCallbackInfo& args); + static void WriteRawBytes(const FunctionCallbackInfo& args); + private: + ValueSerializer serializer_; +}; + +class DeserializerContext : public BaseObject, + public ValueDeserializer::Delegate { + public: + DeserializerContext(Environment* env, + Local wrap, + Local buffer); + + ~DeserializerContext() {} + + MaybeLocal ReadHostObject(Isolate* isolate) override; + + static void New(const FunctionCallbackInfo& args); + static void ReadHeader(const FunctionCallbackInfo& args); + static void ReadValue(const FunctionCallbackInfo& args); + static void TransferArrayBuffer(const FunctionCallbackInfo& args); + static void GetWireFormatVersion(const FunctionCallbackInfo& args); + static void ReadUint32(const FunctionCallbackInfo& args); + static void ReadUint64(const FunctionCallbackInfo& args); + static void ReadDouble(const FunctionCallbackInfo& args); + static void ReadRawBytes(const FunctionCallbackInfo& args); + private: + const uint8_t* data_; + const size_t length_; + + ValueDeserializer deserializer_; +}; + +SerializerContext::SerializerContext(Environment* env, Local wrap) + : BaseObject(env, wrap), + serializer_(env->isolate(), this) { + MakeWeak(this); +} + +void SerializerContext::ThrowDataCloneError(Local message) { + Local args[1] = { message }; + Local get_data_clone_error = + object()->Get(env()->context(), + env()->get_data_clone_error_string()) + .ToLocalChecked(); + + CHECK(get_data_clone_error->IsFunction()); + MaybeLocal error = + get_data_clone_error.As()->Call(env()->context(), + object(), + arraysize(args), + args); + + if (error.IsEmpty()) return; + + env()->isolate()->ThrowException(error.ToLocalChecked()); +} + +Maybe SerializerContext::GetSharedArrayBufferId( + Isolate* isolate, Local shared_array_buffer) { + Local args[1] = { shared_array_buffer }; + Local get_shared_array_buffer_id = + object()->Get(env()->context(), + env()->get_shared_array_buffer_id_string()) + .ToLocalChecked(); + + if (!get_shared_array_buffer_id->IsFunction()) { + return ValueSerializer::Delegate::GetSharedArrayBufferId( + isolate, shared_array_buffer); + } + + MaybeLocal id = + get_shared_array_buffer_id.As()->Call(env()->context(), + object(), + arraysize(args), + args); + + if (id.IsEmpty()) return Nothing(); + + return id.ToLocalChecked()->Uint32Value(env()->context()); +} + +Maybe SerializerContext::WriteHostObject(Isolate* isolate, + Local input) { + MaybeLocal ret; + Local args[1] = { input }; + + Local write_host_object = + object()->Get(env()->context(), + env()->write_host_object_string()).ToLocalChecked(); + + if (!write_host_object->IsFunction()) { + return ValueSerializer::Delegate::WriteHostObject(isolate, input); + } + + ret = write_host_object.As()->Call(env()->context(), + object(), + arraysize(args), + args); + + if (ret.IsEmpty()) + return Nothing(); + + return Just(true); +} + +void SerializerContext::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + new SerializerContext(env, args.This()); +} + +void SerializerContext::WriteHeader(const FunctionCallbackInfo& args) { + SerializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + ctx->serializer_.WriteHeader(); +} + +void SerializerContext::WriteValue(const FunctionCallbackInfo& args) { + SerializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + Maybe ret = + ctx->serializer_.WriteValue(ctx->env()->context(), args[0]); + + if (ret.IsJust()) args.GetReturnValue().Set(ret.FromJust()); +} + +void SerializerContext::SetTreatArrayBufferViewsAsHostObjects( + const FunctionCallbackInfo& args) { + SerializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + Maybe value = args[0]->BooleanValue(ctx->env()->context()); + if (value.IsNothing()) return; + ctx->serializer_.SetTreatArrayBufferViewsAsHostObjects(value.FromJust()); +} + +void SerializerContext::ReleaseBuffer(const FunctionCallbackInfo& args) { + SerializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + std::pair ret = ctx->serializer_.Release(); + auto buf = Buffer::New(ctx->env(), + reinterpret_cast(ret.first), + ret.second); + + if (!buf.IsEmpty()) { + args.GetReturnValue().Set(buf.ToLocalChecked()); + } +} + +void SerializerContext::TransferArrayBuffer( + const FunctionCallbackInfo& args) { + SerializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + Maybe id = args[0]->Uint32Value(ctx->env()->context()); + if (id.IsNothing()) return; + + if (!args[1]->IsArrayBuffer()) + return ctx->env()->ThrowTypeError("arrayBuffer must be an ArrayBuffer"); + + Local ab = args[1].As(); + ctx->serializer_.TransferArrayBuffer(id.FromJust(), ab); + return; +} + +void SerializerContext::WriteUint32(const FunctionCallbackInfo& args) { + SerializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + Maybe value = args[0]->Uint32Value(ctx->env()->context()); + if (value.IsNothing()) return; + + ctx->serializer_.WriteUint32(value.FromJust()); +} + +void SerializerContext::WriteUint64(const FunctionCallbackInfo& args) { + SerializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + Maybe arg0 = args[0]->Uint32Value(ctx->env()->context()); + Maybe arg1 = args[1]->Uint32Value(ctx->env()->context()); + if (arg0.IsNothing() || arg1.IsNothing()) + return; + + uint64_t hi = arg0.FromJust(); + uint64_t lo = arg1.FromJust(); + ctx->serializer_.WriteUint64((hi << 32) | lo); +} + +void SerializerContext::WriteDouble(const FunctionCallbackInfo& args) { + SerializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + Maybe value = args[0]->NumberValue(ctx->env()->context()); + if (value.IsNothing()) return; + + ctx->serializer_.WriteDouble(value.FromJust()); +} + +void SerializerContext::WriteRawBytes(const FunctionCallbackInfo& args) { + SerializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + if (!args[0]->IsUint8Array()) { + return ctx->env()->ThrowTypeError("source must be a Uint8Array"); + } + + ctx->serializer_.WriteRawBytes(Buffer::Data(args[0]), + Buffer::Length(args[0])); +} + +DeserializerContext::DeserializerContext(Environment* env, + Local wrap, + Local buffer) + : BaseObject(env, wrap), + data_(reinterpret_cast(Buffer::Data(buffer))), + length_(Buffer::Length(buffer)), + deserializer_(env->isolate(), data_, length_, this) { + object()->Set(env->context(), env->buffer_string(), buffer); + + MakeWeak(this); +} + +MaybeLocal DeserializerContext::ReadHostObject(Isolate* isolate) { + Local read_host_object = + object()->Get(env()->context(), + env()->read_host_object_string()).ToLocalChecked(); + + if (!read_host_object->IsFunction()) { + return ValueDeserializer::Delegate::ReadHostObject(isolate); + } + + MaybeLocal ret = + read_host_object.As()->Call(env()->context(), + object(), + 0, + nullptr); + + if (ret.IsEmpty()) + return MaybeLocal(); + + Local return_value = ret.ToLocalChecked(); + if (!return_value->IsObject()) { + env()->ThrowTypeError("readHostObject must return an object"); + return MaybeLocal(); + } + + return return_value.As(); +} + +void DeserializerContext::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + if (!args[0]->IsUint8Array()) { + return env->ThrowTypeError("buffer must be a Uint8Array"); + } + + new DeserializerContext(env, args.This(), args[0]); +} + +void DeserializerContext::ReadHeader(const FunctionCallbackInfo& args) { + DeserializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + Maybe ret = ctx->deserializer_.ReadHeader(ctx->env()->context()); + + if (ret.IsJust()) args.GetReturnValue().Set(ret.FromJust()); +} + +void DeserializerContext::ReadValue(const FunctionCallbackInfo& args) { + DeserializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + MaybeLocal ret = ctx->deserializer_.ReadValue(ctx->env()->context()); + + if (!ret.IsEmpty()) args.GetReturnValue().Set(ret.ToLocalChecked()); +} + +void DeserializerContext::TransferArrayBuffer( + const FunctionCallbackInfo& args) { + DeserializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + Maybe id = args[0]->Uint32Value(ctx->env()->context()); + if (id.IsNothing()) return; + + if (args[1]->IsArrayBuffer()) { + Local ab = args[1].As(); + ctx->deserializer_.TransferArrayBuffer(id.FromJust(), ab); + return; + } + + if (args[1]->IsSharedArrayBuffer()) { + Local sab = args[1].As(); + ctx->deserializer_.TransferSharedArrayBuffer(id.FromJust(), sab); + return; + } + + return ctx->env()->ThrowTypeError("arrayBuffer must be an ArrayBuffer or " + "SharedArrayBuffer"); +} + +void DeserializerContext::GetWireFormatVersion( + const FunctionCallbackInfo& args) { + DeserializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + args.GetReturnValue().Set(ctx->deserializer_.GetWireFormatVersion()); +} + +void DeserializerContext::ReadUint32(const FunctionCallbackInfo& args) { + DeserializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + uint32_t value; + bool ok = ctx->deserializer_.ReadUint32(&value); + if (!ok) return ctx->env()->ThrowError("ReadUint32() failed"); + return args.GetReturnValue().Set(value); +} + +void DeserializerContext::ReadUint64(const FunctionCallbackInfo& args) { + DeserializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + uint64_t value; + bool ok = ctx->deserializer_.ReadUint64(&value); + if (!ok) return ctx->env()->ThrowError("ReadUint64() failed"); + + uint32_t hi = static_cast(value >> 32); + uint32_t lo = static_cast(value); + + Isolate* isolate = ctx->env()->isolate(); + Local context = ctx->env()->context(); + + Local ret = Array::New(isolate, 2); + ret->Set(context, 0, Integer::NewFromUnsigned(isolate, hi)); + ret->Set(context, 1, Integer::NewFromUnsigned(isolate, lo)); + return args.GetReturnValue().Set(ret); +} + +void DeserializerContext::ReadDouble(const FunctionCallbackInfo& args) { + DeserializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + double value; + bool ok = ctx->deserializer_.ReadDouble(&value); + if (!ok) return ctx->env()->ThrowError("ReadDouble() failed"); + return args.GetReturnValue().Set(value); +} + +void DeserializerContext::ReadRawBytes( + const FunctionCallbackInfo& args) { + DeserializerContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + Maybe length_arg = args[0]->IntegerValue(ctx->env()->context()); + if (length_arg.IsNothing()) return; + size_t length = length_arg.FromJust(); + + const void* data; + bool ok = ctx->deserializer_.ReadRawBytes(length, &data); + if (!ok) return ctx->env()->ThrowError("ReadRawBytes() failed"); + + const uint8_t* position = reinterpret_cast(data); + CHECK_GE(position, ctx->data_); + CHECK_LE(position + length, ctx->data_ + ctx->length_); + + const uint32_t offset = position - ctx->data_; + CHECK_EQ(ctx->data_ + offset, position); + + args.GetReturnValue().Set(offset); +} + +void InitializeSerdesBindings(Local target, + Local unused, + Local context) { + Environment* env = Environment::GetCurrent(context); + Local ser = + env->NewFunctionTemplate(SerializerContext::New); + + ser->InstanceTemplate()->SetInternalFieldCount(1); + + env->SetProtoMethod(ser, "writeHeader", SerializerContext::WriteHeader); + env->SetProtoMethod(ser, "writeValue", SerializerContext::WriteValue); + env->SetProtoMethod(ser, "releaseBuffer", SerializerContext::ReleaseBuffer); + env->SetProtoMethod(ser, + "transferArrayBuffer", + SerializerContext::TransferArrayBuffer); + env->SetProtoMethod(ser, "writeUint32", SerializerContext::WriteUint32); + env->SetProtoMethod(ser, "writeUint64", SerializerContext::WriteUint64); + env->SetProtoMethod(ser, "writeDouble", SerializerContext::WriteDouble); + env->SetProtoMethod(ser, "writeRawBytes", SerializerContext::WriteRawBytes); + env->SetProtoMethod(ser, + "_setTreatArrayBufferViewsAsHostObjects", + SerializerContext::SetTreatArrayBufferViewsAsHostObjects); + + ser->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Serializer")); + target->Set(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "Serializer"), + ser->GetFunction(env->context()).ToLocalChecked()).FromJust(); + + Local des = + env->NewFunctionTemplate(DeserializerContext::New); + + des->InstanceTemplate()->SetInternalFieldCount(1); + + env->SetProtoMethod(des, "readHeader", DeserializerContext::ReadHeader); + env->SetProtoMethod(des, "readValue", DeserializerContext::ReadValue); + env->SetProtoMethod(des, + "getWireFormatVersion", + DeserializerContext::GetWireFormatVersion); + env->SetProtoMethod(des, + "transferArrayBuffer", + DeserializerContext::TransferArrayBuffer); + env->SetProtoMethod(des, "readUint32", DeserializerContext::ReadUint32); + env->SetProtoMethod(des, "readUint64", DeserializerContext::ReadUint64); + env->SetProtoMethod(des, "readDouble", DeserializerContext::ReadDouble); + env->SetProtoMethod(des, "_readRawBytes", DeserializerContext::ReadRawBytes); + + des->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Deserializer")); + target->Set(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "Deserializer"), + des->GetFunction(env->context()).ToLocalChecked()).FromJust(); +} + +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_BUILTIN(serdes, node::InitializeSerdesBindings) diff --git a/test/parallel/test-v8-serdes-sharedarraybuffer.js b/test/parallel/test-v8-serdes-sharedarraybuffer.js new file mode 100644 index 0000000000..9dea5fe8b1 --- /dev/null +++ b/test/parallel/test-v8-serdes-sharedarraybuffer.js @@ -0,0 +1,27 @@ +/*global SharedArrayBuffer*/ +'use strict'; +// Flags: --harmony-sharedarraybuffer + +const common = require('../common'); +const assert = require('assert'); +const v8 = require('v8'); + +{ + const sab = new SharedArrayBuffer(64); + const uint8array = new Uint8Array(sab); + const ID = 42; + + const ser = new v8.Serializer(); + ser._getSharedArrayBufferId = common.mustCall(() => ID); + ser.writeHeader(); + + ser.writeValue(uint8array); + + const des = new v8.Deserializer(ser.releaseBuffer()); + des.readHeader(); + des.transferArrayBuffer(ID, sab); + + const value = des.readValue(); + assert.strictEqual(value.buffer, sab); + assert.notStrictEqual(value, uint8array); +} diff --git a/test/parallel/test-v8-serdes.js b/test/parallel/test-v8-serdes.js new file mode 100644 index 0000000000..84037b6f8c --- /dev/null +++ b/test/parallel/test-v8-serdes.js @@ -0,0 +1,120 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const v8 = require('v8'); + +const circular = {}; +circular.circular = circular; + +const objects = [ + { foo: 'bar' }, + { bar: 'baz' }, + new Uint8Array([1, 2, 3, 4]), + new Uint32Array([1, 2, 3, 4]), + Buffer.from([1, 2, 3, 4]), + undefined, + null, + 42, + circular +]; + +{ + const ser = new v8.DefaultSerializer(); + ser.writeHeader(); + for (const obj of objects) { + ser.writeValue(obj); + } + + const des = new v8.DefaultDeserializer(ser.releaseBuffer()); + des.readHeader(); + + for (const obj of objects) { + assert.deepStrictEqual(des.readValue(), obj); + } +} + +{ + for (const obj of objects) { + assert.deepStrictEqual(v8.deserialize(v8.serialize(obj)), obj); + } +} + +{ + const ser = new v8.DefaultSerializer(); + ser._getDataCloneError = common.mustCall((message) => { + assert.strictEqual(message, '[object Object] could not be cloned.'); + return new Error('foobar'); + }); + + ser.writeHeader(); + + assert.throws(() => { + ser.writeValue(new Proxy({}, {})); + }, /foobar/); +} + +{ + const ser = new v8.DefaultSerializer(); + ser._writeHostObject = common.mustCall((object) => { + assert.strictEqual(object, process.stdin._handle); + const buf = Buffer.from('stdin'); + + ser.writeUint32(buf.length); + ser.writeRawBytes(buf); + + ser.writeUint64(1, 2); + ser.writeDouble(-0.25); + }); + + ser.writeHeader(); + ser.writeValue({ val: process.stdin._handle }); + + const des = new v8.DefaultDeserializer(ser.releaseBuffer()); + des._readHostObject = common.mustCall(() => { + const length = des.readUint32(); + const buf = des.readRawBytes(length); + + assert.strictEqual(buf.toString(), 'stdin'); + + assert.deepStrictEqual(des.readUint64(), [1, 2]); + assert.strictEqual(des.readDouble(), -0.25); + return process.stdin._handle; + }); + + des.readHeader(); + + assert.strictEqual(des.readValue().val, process.stdin._handle); +} + +{ + const ser = new v8.DefaultSerializer(); + ser._writeHostObject = common.mustCall((object) => { + throw new Error('foobar'); + }); + + ser.writeHeader(); + assert.throws(() => { + ser.writeValue({ val: process.stdin._handle }); + }, /foobar/); +} + +{ + assert.throws(() => v8.serialize(process.stdin._handle), + /^Error: Unknown host object type: \[object .*\]$/); +} + +{ + const buf = Buffer.from('ff0d6f2203666f6f5e007b01', 'hex'); + + const des = new v8.DefaultDeserializer(buf); + des.readHeader(); + + const ser = new v8.DefaultSerializer(); + ser.writeHeader(); + + ser.writeValue(des.readValue()); + + assert.deepStrictEqual(buf, ser.releaseBuffer()); + assert.strictEqual(des.getWireFormatVersion(), 0x0d); +}