Skip to content

Commit 81ff9d9

Browse files
committed
src: implement structuredClone in native
Simplify the implementation by implementing it directly in C++. For now we still piggyback on the MessagePort implementation, this can be simplified further by avoiding MessagePorts at all in the future.
1 parent 97d2219 commit 81ff9d9

File tree

9 files changed

+264
-90
lines changed

9 files changed

+264
-90
lines changed

benchmark/misc/structured-clone.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const assert = require('assert');
5+
6+
const bench = common.createBenchmark(main, {
7+
type: ['string', 'object', 'arraybuffer'],
8+
n: [1e4],
9+
});
10+
11+
function main({ n, type }) {
12+
const data = [];
13+
14+
switch (type) {
15+
case 'string':
16+
for (let i = 0; i < n; ++i) {
17+
data.push(new Date().toISOString());
18+
}
19+
break;
20+
case 'object':
21+
for (let i = 0; i < n; ++i) {
22+
data.push({ ...process.config });
23+
}
24+
break;
25+
case 'arraybuffer':
26+
for (let i = 0; i < n; ++i) {
27+
data.push(new ArrayBuffer(10));
28+
}
29+
break;
30+
default:
31+
throw new Error('Unsupported payload type');
32+
}
33+
34+
const run = type === 'arraybuffer' ? (i) => {
35+
data[i] = structuredClone(data[i], { transfer: [ data[i] ] });
36+
} : (i) => {
37+
data[i] = structuredClone(data[i]);
38+
};
39+
40+
bench.start();
41+
for (let i = 0; i < n; ++i) {
42+
run(i);
43+
}
44+
bench.end(n);
45+
assert.strictEqual(data.length, n);
46+
}

lib/internal/bootstrap/web/exposed-window-or-worker.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,8 @@ const {
3131
} = require('internal/process/task_queues');
3232
defineOperation(globalThis, 'queueMicrotask', queueMicrotask);
3333

34-
defineLazyProperties(
35-
globalThis,
36-
'internal/structured_clone',
37-
['structuredClone'],
38-
);
34+
const { structuredClone } = internalBinding('messaging');
35+
defineOperation(globalThis, 'structuredClone', structuredClone);
3936
defineLazyProperties(globalThis, 'buffer', ['atob', 'btoa']);
4037

4138
// https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts

lib/internal/perf/usertiming.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const {
3131
},
3232
} = require('internal/errors');
3333

34-
const { structuredClone } = require('internal/structured_clone');
34+
const { structuredClone } = internalBinding('messaging');
3535
const {
3636
lazyDOMException,
3737
kEnumerableProperty,

lib/internal/structured_clone.js

Lines changed: 0 additions & 29 deletions
This file was deleted.

lib/internal/webstreams/readablestream.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,7 @@ const {
8585
kControllerErrorFunction,
8686
} = require('internal/streams/utils');
8787

88-
const {
89-
structuredClone,
90-
} = require('internal/structured_clone');
88+
const { structuredClone } = internalBinding('messaging');
9189

9290
const {
9391
ArrayBufferViewGetBuffer,

src/base_object_types.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace node {
1212
#define SERIALIZABLE_BINDING_TYPES(V) \
1313
V(encoding_binding_data, encoding_binding::BindingData) \
1414
V(fs_binding_data, fs::BindingData) \
15+
V(messaging_binding_data, messaging::BindingData) \
1516
V(mksnapshot_binding_data, mksnapshot::BindingData) \
1617
V(v8_binding_data, v8_utils::BindingData) \
1718
V(blob_binding_data, BlobBindingData) \

src/node_messaging.cc

Lines changed: 174 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,47 @@ static Maybe<bool> ReadIterable(Environment* env,
10031003
return Just(true);
10041004
}
10051005

1006+
bool GetTransferList(Environment* env,
1007+
Local<Context> context,
1008+
Local<Value> transfer_list_v,
1009+
TransferList* transfer_list_out) {
1010+
if (transfer_list_v->IsNullOrUndefined()) {
1011+
// Browsers ignore null or undefined, and otherwise accept an array or an
1012+
// options object.
1013+
return true;
1014+
}
1015+
1016+
if (!transfer_list_v->IsObject()) {
1017+
THROW_ERR_INVALID_ARG_TYPE(
1018+
env, "Optional transferList argument must be an iterable");
1019+
return false;
1020+
}
1021+
1022+
bool was_iterable;
1023+
if (!ReadIterable(env, context, *transfer_list_out, transfer_list_v)
1024+
.To(&was_iterable))
1025+
return false;
1026+
if (!was_iterable) {
1027+
Local<Value> transfer_option;
1028+
if (!transfer_list_v.As<Object>()
1029+
->Get(context, env->transfer_string())
1030+
.ToLocal(&transfer_option))
1031+
return false;
1032+
if (!transfer_option->IsUndefined()) {
1033+
if (!ReadIterable(env, context, *transfer_list_out, transfer_option)
1034+
.To(&was_iterable))
1035+
return false;
1036+
if (!was_iterable) {
1037+
THROW_ERR_INVALID_ARG_TYPE(
1038+
env, "Optional options.transfer argument must be an iterable");
1039+
return false;
1040+
}
1041+
}
1042+
}
1043+
1044+
return true;
1045+
}
1046+
10061047
void MessagePort::PostMessage(const FunctionCallbackInfo<Value>& args) {
10071048
Environment* env = Environment::GetCurrent(args);
10081049
Local<Object> obj = args.This();
@@ -1013,33 +1054,10 @@ void MessagePort::PostMessage(const FunctionCallbackInfo<Value>& args) {
10131054
"MessagePort.postMessage");
10141055
}
10151056

1016-
if (!args[1]->IsNullOrUndefined() && !args[1]->IsObject()) {
1017-
// Browsers ignore null or undefined, and otherwise accept an array or an
1018-
// options object.
1019-
return THROW_ERR_INVALID_ARG_TYPE(env,
1020-
"Optional transferList argument must be an iterable");
1021-
}
1022-
10231057
TransferList transfer_list;
1024-
if (args[1]->IsObject()) {
1025-
bool was_iterable;
1026-
if (!ReadIterable(env, context, transfer_list, args[1]).To(&was_iterable))
1027-
return;
1028-
if (!was_iterable) {
1029-
Local<Value> transfer_option;
1030-
if (!args[1].As<Object>()->Get(context, env->transfer_string())
1031-
.ToLocal(&transfer_option)) return;
1032-
if (!transfer_option->IsUndefined()) {
1033-
if (!ReadIterable(env, context, transfer_list, transfer_option)
1034-
.To(&was_iterable)) return;
1035-
if (!was_iterable) {
1036-
return THROW_ERR_INVALID_ARG_TYPE(env,
1037-
"Optional options.transfer argument must be an iterable");
1038-
}
1039-
}
1040-
}
1058+
if (!GetTransferList(env, context, args[1], &transfer_list)) {
1059+
return;
10411060
}
1042-
10431061
MessagePort* port = Unwrap<MessagePort>(args.This());
10441062
// Even if the backing MessagePort object has already been deleted, we still
10451063
// want to serialize the message to ensure spec-compliant behavior w.r.t.
@@ -1531,6 +1549,56 @@ static void SetDeserializerCreateObjectFunction(
15311549
env->set_messaging_deserialize_create_object(args[0].As<Function>());
15321550
}
15331551

1552+
static void StructuredClone(const FunctionCallbackInfo<Value>& args) {
1553+
Isolate* isolate = args.GetIsolate();
1554+
Local<Context> context = isolate->GetCurrentContext();
1555+
Realm* realm = Realm::GetCurrent(context);
1556+
Environment* env = realm->env();
1557+
1558+
if (args.Length() == 0) {
1559+
return THROW_ERR_MISSING_ARGS(env, "The value argument must be specified");
1560+
}
1561+
1562+
Local<Value> value = args[0];
1563+
1564+
TransferList transfer_list;
1565+
if (!args[1]->IsNullOrUndefined()) {
1566+
if (!args[1]->IsObject()) {
1567+
return THROW_ERR_INVALID_ARG_TYPE(
1568+
env, "The options argument must be either an object or undefined");
1569+
}
1570+
Local<Object> options = args[1].As<Object>();
1571+
Local<Value> transfer_list_v;
1572+
if (!options->Get(context, FIXED_ONE_BYTE_STRING(isolate, "transfer"))
1573+
.ToLocal(&transfer_list_v)) {
1574+
return;
1575+
}
1576+
1577+
if (!GetTransferList(env, context, transfer_list_v, &transfer_list)) {
1578+
return;
1579+
}
1580+
}
1581+
1582+
// TODO(joyeecheung): refactor and use V8 serialization/deserialization
1583+
// directly instead of going through message ports.
1584+
BindingData* binding_data = realm->GetBindingData<BindingData>(context);
1585+
MessagePort* port1;
1586+
MessagePort* port2;
1587+
std::tie(port1, port2) =
1588+
binding_data->GetOrCreatePortsForStructuredClone(context);
1589+
if (port1 == nullptr || port2 == nullptr) {
1590+
return;
1591+
}
1592+
1593+
Maybe<bool> res = port1->PostMessage(env, context, value, transfer_list);
1594+
if (res.IsNothing()) {
1595+
return;
1596+
}
1597+
MaybeLocal<Value> payload = port2->ReceiveMessage(
1598+
context, MessagePort::MessageProcessingMode::kForceReadMessages);
1599+
if (!payload.IsEmpty()) args.GetReturnValue().Set(payload.ToLocalChecked());
1600+
}
1601+
15341602
static void MessageChannel(const FunctionCallbackInfo<Value>& args) {
15351603
Environment* env = Environment::GetCurrent(args);
15361604
if (!args.IsConstructCall()) {
@@ -1569,6 +1637,83 @@ static void BroadcastChannel(const FunctionCallbackInfo<Value>& args) {
15691637
}
15701638
}
15711639

1640+
void BindingData::MemoryInfo(MemoryTracker* tracker) const {
1641+
tracker->TrackField("port1", port1_);
1642+
tracker->TrackField("port2", port2_);
1643+
}
1644+
1645+
BindingData::BindingData(Realm* realm, v8::Local<v8::Object> object)
1646+
: SnapshotableObject(realm, object, type_int) {}
1647+
1648+
std::pair<MessagePort*, MessagePort*>
1649+
BindingData::GetOrCreatePortsForStructuredClone(Local<Context> context) {
1650+
if (port1_ != nullptr) {
1651+
DCHECK_NOT_NULL(port2_);
1652+
return std::make_pair(port1_, port2_);
1653+
}
1654+
1655+
port1_ = MessagePort::New(env(), context);
1656+
1657+
if (port1_ != nullptr) {
1658+
port2_ = MessagePort::New(env(), context);
1659+
}
1660+
1661+
if (port1_ == nullptr || port2_ == nullptr) {
1662+
ThrowDataCloneException(context,
1663+
FIXED_ONE_BYTE_STRING(context->GetIsolate(),
1664+
"Cannot create MessagePort"));
1665+
if (port1_ != nullptr) {
1666+
port1_->Close();
1667+
port1_ = nullptr;
1668+
}
1669+
}
1670+
1671+
uv_unref(port1_->GetHandle());
1672+
uv_unref(port2_->GetHandle());
1673+
MessagePort::Entangle(port1_, port2_);
1674+
1675+
return std::make_pair(port1_, port2_);
1676+
}
1677+
1678+
bool BindingData::PrepareForSerialization(v8::Local<v8::Context> context,
1679+
v8::SnapshotCreator* creator) {
1680+
// We'll just re-initialize them when structuredClone is called again.
1681+
// TODO(joyeecheung): currently this is not enough to clean up the ports
1682+
// because their shutdown is async. Either add a special path to shut
1683+
// them down synchronously, or make it possible for the the snapshot
1684+
// process to deal with async shutdown, or just don't use ports and
1685+
// serialize/deserialize the data directly. Until then, structuredClone
1686+
// is not supported in custom snapshots.
1687+
if (port1_ != nullptr) {
1688+
DCHECK_NOT_NULL(port2_);
1689+
port1_->Close();
1690+
port1_ = nullptr;
1691+
port2_->Close();
1692+
port2_ = nullptr;
1693+
}
1694+
// Return true because we need to maintain the reference to the binding from
1695+
// JS land.
1696+
return true;
1697+
}
1698+
1699+
InternalFieldInfoBase* BindingData::Serialize(int index) {
1700+
DCHECK_IS_SNAPSHOT_SLOT(index);
1701+
InternalFieldInfo* info =
1702+
InternalFieldInfoBase::New<InternalFieldInfo>(type());
1703+
return info;
1704+
}
1705+
1706+
void BindingData::Deserialize(v8::Local<v8::Context> context,
1707+
v8::Local<v8::Object> holder,
1708+
int index,
1709+
InternalFieldInfoBase* info) {
1710+
DCHECK_IS_SNAPSHOT_SLOT(index);
1711+
v8::HandleScope scope(context->GetIsolate());
1712+
Realm* realm = Realm::GetCurrent(context);
1713+
BindingData* binding = realm->AddBindingData<BindingData>(holder);
1714+
CHECK_NOT_NULL(binding);
1715+
}
1716+
15721717
static void CreatePerIsolateProperties(IsolateData* isolate_data,
15731718
Local<ObjectTemplate> target) {
15741719
Isolate* isolate = isolate_data->isolate();
@@ -1608,18 +1753,21 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
16081753
"setDeserializerCreateObjectFunction",
16091754
SetDeserializerCreateObjectFunction);
16101755
SetMethod(isolate, target, "broadcastChannel", BroadcastChannel);
1756+
SetMethod(isolate, target, "structuredClone", StructuredClone);
16111757
}
16121758

16131759
static void CreatePerContextProperties(Local<Object> target,
16141760
Local<Value> unused,
16151761
Local<Context> context,
16161762
void* priv) {
1763+
Realm* realm = Realm::GetCurrent(context);
16171764
Isolate* isolate = context->GetIsolate();
16181765
Local<Function> domexception = GetDOMException(context).ToLocalChecked();
16191766
target
16201767
->Set(
16211768
context, FIXED_ONE_BYTE_STRING(isolate, "DOMException"), domexception)
16221769
.Check();
1770+
realm->AddBindingData<BindingData>(target);
16231771
}
16241772

16251773
static void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
@@ -1634,6 +1782,7 @@ static void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
16341782
registry->Register(MessagePort::ReceiveMessage);
16351783
registry->Register(MessagePort::MoveToContext);
16361784
registry->Register(SetDeserializerCreateObjectFunction);
1785+
registry->Register(StructuredClone);
16371786
}
16381787

16391788
} // namespace messaging

0 commit comments

Comments
 (0)