Skip to content

Commit d1f372f

Browse files
addaleaxtargos
authored andcommitted
worker: add SharedArrayBuffer sharing
Logic is added to the `MessagePort` mechanism that attaches hidden objects to those instances when they are transferred that track their lifetime and maintain a reference count, to make sure that memory is freed at the appropriate times. Thanks to Stephen Belanger for reviewing this change in its original PR. Refs: ayojs/ayo#106 PR-URL: #20876 Reviewed-By: Gireesh Punathil <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Shingo Inoue <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Tiancheng "Timothy" Gu <[email protected]> Reviewed-By: John-David Dalton <[email protected]> Reviewed-By: Gus Caplan <[email protected]>
1 parent f447acd commit d1f372f

File tree

8 files changed

+274
-9
lines changed

8 files changed

+274
-9
lines changed

doc/api/worker.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,16 @@ to stringify.
8585

8686
`transferList` may be a list of `ArrayBuffer` and `MessagePort` objects.
8787
After transferring, they will not be usable on the sending side of the channel
88-
anymore (even if they are not contained in `value`).
88+
anymore (even if they are not contained in `value`). Unlike with
89+
[child processes][], transferring handles such as network sockets is currently
90+
not supported.
91+
92+
If `value` contains [`SharedArrayBuffer`][] instances, those will be accessible
93+
from either thread. They cannot be listed in `transferList`.
8994

9095
`value` may still contain `ArrayBuffer` instances that are not in
9196
`transferList`; in that case, the underlying memory is copied rather than moved.
9297

93-
For more information on the serialization and deserialization mechanisms
94-
behind this API, see the [serialization API of the `v8` module][v8.serdes].
95-
9698
Because the object cloning uses the structured clone algorithm,
9799
non-enumerable properties, property accessors, and object prototypes are
98100
not preserved. In particular, [`Buffer`][] objects will be read as
@@ -101,6 +103,9 @@ plain [`Uint8Array`][]s on the receiving side.
101103
The message object will be cloned immediately, and can be modified after
102104
posting without having side effects.
103105

106+
For more information on the serialization and deserialization mechanisms
107+
behind this API, see the [serialization API of the `v8` module][v8.serdes].
108+
104109
### port.ref()
105110
<!-- YAML
106111
added: REPLACEME
@@ -137,10 +142,12 @@ be `ref()`ed and `unref()`ed automatically depending on whether
137142
listeners for the event exist.
138143

139144
[`Buffer`]: buffer.html
145+
[child processes]: child_process.html
140146
[`EventEmitter`]: events.html
141147
[`MessagePort`]: #worker_class_messageport
142148
[`port.postMessage()`]: #worker_port_postmessage_value_transferlist
143149
[v8.serdes]: v8.html#v8_serialization_api
150+
[`SharedArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
144151
[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
145152
[browser `MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
146153
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@
352352
'src/node_i18n.cc',
353353
'src/pipe_wrap.cc',
354354
'src/process_wrap.cc',
355+
'src/sharedarraybuffer_metadata.cc',
355356
'src/signal_wrap.cc',
356357
'src/spawn_sync.cc',
357358
'src/string_bytes.cc',
@@ -411,6 +412,7 @@
411412
'src/udp_wrap.h',
412413
'src/req_wrap.h',
413414
'src/req_wrap-inl.h',
415+
'src/sharedarraybuffer_metadata.h',
414416
'src/string_bytes.h',
415417
'src/string_decoder.h',
416418
'src/string_decoder-inl.h',

src/env.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ struct PackageConfig {
106106
V(decorated_private_symbol, "node:decorated") \
107107
V(napi_env, "node:napi:env") \
108108
V(napi_wrapper, "node:napi:wrapper") \
109+
V(sab_lifetimepartner_symbol, "node:sharedArrayBufferLifetimePartner") \
109110

110111
// Symbols are per-isolate primitives but Environment proxies them
111112
// for the sake of convenience.
@@ -338,6 +339,7 @@ struct PackageConfig {
338339
V(promise_wrap_template, v8::ObjectTemplate) \
339340
V(push_values_to_array_function, v8::Function) \
340341
V(randombytes_constructor_template, v8::ObjectTemplate) \
342+
V(sab_lifetimepartner_constructor_template, v8::FunctionTemplate) \
341343
V(script_context_constructor_template, v8::FunctionTemplate) \
342344
V(script_data_constructor_function, v8::Function) \
343345
V(secure_context_constructor_template, v8::FunctionTemplate) \

src/node_errors.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ namespace node {
3131
V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, TypeError) \
3232
V(ERR_MISSING_MODULE, Error) \
3333
V(ERR_STRING_TOO_LONG, Error) \
34+
V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, TypeError) \
3435

3536
#define V(code, type) \
3637
inline v8::Local<v8::Value> code(v8::Isolate* isolate, \
@@ -60,7 +61,9 @@ namespace node {
6061
V(ERR_INVALID_TRANSFER_OBJECT, "Found invalid object in transferList") \
6162
V(ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate memory") \
6263
V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, \
63-
"MessagePort was found in message but not listed in transferList")
64+
"MessagePort was found in message but not listed in transferList") \
65+
V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, \
66+
"Cannot serialize externalized SharedArrayBuffer") \
6467

6568
#define V(code, message) \
6669
inline v8::Local<v8::Value> code(v8::Isolate* isolate) { \

src/node_messaging.cc

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ using v8::Maybe;
2424
using v8::MaybeLocal;
2525
using v8::Nothing;
2626
using v8::Object;
27+
using v8::SharedArrayBuffer;
2728
using v8::String;
2829
using v8::Value;
2930
using v8::ValueDeserializer;
@@ -43,8 +44,13 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
4344
public:
4445
DeserializerDelegate(Message* m,
4546
Environment* env,
46-
const std::vector<MessagePort*>& message_ports)
47-
: env_(env), msg_(m), message_ports_(message_ports) {}
47+
const std::vector<MessagePort*>& message_ports,
48+
const std::vector<Local<SharedArrayBuffer>>&
49+
shared_array_buffers)
50+
: env_(env),
51+
msg_(m),
52+
message_ports_(message_ports),
53+
shared_array_buffers_(shared_array_buffers) {}
4854

4955
MaybeLocal<Object> ReadHostObject(Isolate* isolate) override {
5056
// Currently, only MessagePort hosts objects are supported, so identifying
@@ -56,12 +62,19 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
5662
return message_ports_[id]->object();
5763
};
5864

65+
MaybeLocal<SharedArrayBuffer> GetSharedArrayBufferFromId(
66+
Isolate* isolate, uint32_t clone_id) override {
67+
CHECK_LE(clone_id, shared_array_buffers_.size());
68+
return shared_array_buffers_[clone_id];
69+
}
70+
5971
ValueDeserializer* deserializer = nullptr;
6072

6173
private:
6274
Environment* env_;
6375
Message* msg_;
6476
const std::vector<MessagePort*>& message_ports_;
77+
const std::vector<Local<SharedArrayBuffer>>& shared_array_buffers_;
6578
};
6679

6780
} // anonymous namespace
@@ -87,7 +100,18 @@ MaybeLocal<Value> Message::Deserialize(Environment* env,
87100
}
88101
message_ports_.clear();
89102

90-
DeserializerDelegate delegate(this, env, ports);
103+
std::vector<Local<SharedArrayBuffer>> shared_array_buffers;
104+
// Attach all transfered SharedArrayBuffers to their new Isolate.
105+
for (uint32_t i = 0; i < shared_array_buffers_.size(); ++i) {
106+
Local<SharedArrayBuffer> sab;
107+
if (!shared_array_buffers_[i]->GetSharedArrayBuffer(env, context)
108+
.ToLocal(&sab))
109+
return MaybeLocal<Value>();
110+
shared_array_buffers.push_back(sab);
111+
}
112+
shared_array_buffers_.clear();
113+
114+
DeserializerDelegate delegate(this, env, ports, shared_array_buffers);
91115
ValueDeserializer deserializer(
92116
env->isolate(),
93117
reinterpret_cast<const uint8_t*>(main_message_buf_.data),
@@ -112,6 +136,11 @@ MaybeLocal<Value> Message::Deserialize(Environment* env,
112136
deserializer.ReadValue(context).FromMaybe(Local<Value>()));
113137
}
114138

139+
void Message::AddSharedArrayBuffer(
140+
SharedArrayBufferMetadataReference reference) {
141+
shared_array_buffers_.push_back(reference);
142+
}
143+
115144
void Message::AddMessagePort(std::unique_ptr<MessagePortData>&& data) {
116145
message_ports_.emplace_back(std::move(data));
117146
}
@@ -139,6 +168,27 @@ class SerializerDelegate : public ValueSerializer::Delegate {
139168
return Nothing<bool>();
140169
}
141170

171+
Maybe<uint32_t> GetSharedArrayBufferId(
172+
Isolate* isolate,
173+
Local<SharedArrayBuffer> shared_array_buffer) override {
174+
uint32_t i;
175+
for (i = 0; i < seen_shared_array_buffers_.size(); ++i) {
176+
if (seen_shared_array_buffers_[i] == shared_array_buffer)
177+
return Just(i);
178+
}
179+
180+
auto reference = SharedArrayBufferMetadata::ForSharedArrayBuffer(
181+
env_,
182+
context_,
183+
shared_array_buffer);
184+
if (!reference) {
185+
return Nothing<uint32_t>();
186+
}
187+
seen_shared_array_buffers_.push_back(shared_array_buffer);
188+
msg_->AddSharedArrayBuffer(reference);
189+
return Just(i);
190+
}
191+
142192
void Finish() {
143193
// Only close the MessagePort handles and actually transfer them
144194
// once we know that serialization succeeded.
@@ -166,6 +216,7 @@ class SerializerDelegate : public ValueSerializer::Delegate {
166216
Environment* env_;
167217
Local<Context> context_;
168218
Message* msg_;
219+
std::vector<Local<SharedArrayBuffer>> seen_shared_array_buffers_;
169220
std::vector<MessagePort*> ports_;
170221

171222
friend class worker::Message;

src/node_messaging.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
#include "env.h"
77
#include "node_mutex.h"
8+
#include "sharedarraybuffer_metadata.h"
89
#include <list>
9-
#include <memory>
1010

1111
namespace node {
1212
namespace worker {
@@ -37,13 +37,17 @@ class Message {
3737
v8::Local<v8::Value> input,
3838
v8::Local<v8::Value> transfer_list);
3939

40+
// Internal method of Message that is called when a new SharedArrayBuffer
41+
// object is encountered in the incoming value's structure.
42+
void AddSharedArrayBuffer(SharedArrayBufferMetadataReference ref);
4043
// Internal method of Message that is called once serialization finishes
4144
// and that transfers ownership of `data` to this message.
4245
void AddMessagePort(std::unique_ptr<MessagePortData>&& data);
4346

4447
private:
4548
MallocedBuffer<char> main_message_buf_;
4649
std::vector<MallocedBuffer<char>> array_buffer_contents_;
50+
std::vector<SharedArrayBufferMetadataReference> shared_array_buffers_;
4751
std::vector<std::unique_ptr<MessagePortData>> message_ports_;
4852

4953
friend class MessagePort;

src/sharedarraybuffer_metadata.cc

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#include "sharedarraybuffer_metadata.h"
2+
#include "base_object.h"
3+
#include "base_object-inl.h"
4+
#include "node_errors.h"
5+
6+
using v8::Context;
7+
using v8::Function;
8+
using v8::FunctionTemplate;
9+
using v8::Local;
10+
using v8::Maybe;
11+
using v8::MaybeLocal;
12+
using v8::Nothing;
13+
using v8::Object;
14+
using v8::SharedArrayBuffer;
15+
using v8::Value;
16+
17+
namespace node {
18+
namespace worker {
19+
20+
namespace {
21+
22+
// Yield a JS constructor for SABLifetimePartner objects in the form of a
23+
// standard API object, that has a single field for containing the raw
24+
// SABLiftimePartner* pointer.
25+
Local<Function> GetSABLifetimePartnerConstructor(
26+
Environment* env, Local<Context> context) {
27+
Local<FunctionTemplate> templ;
28+
templ = env->sab_lifetimepartner_constructor_template();
29+
if (!templ.IsEmpty())
30+
return templ->GetFunction(context).ToLocalChecked();
31+
32+
templ = BaseObject::MakeLazilyInitializedJSTemplate(env);
33+
templ->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(),
34+
"SABLifetimePartner"));
35+
env->set_sab_lifetimepartner_constructor_template(templ);
36+
37+
return GetSABLifetimePartnerConstructor(env, context);
38+
}
39+
40+
class SABLifetimePartner : public BaseObject {
41+
public:
42+
SABLifetimePartner(Environment* env,
43+
Local<Object> obj,
44+
SharedArrayBufferMetadataReference r)
45+
: BaseObject(env, obj),
46+
reference(r) {
47+
MakeWeak();
48+
}
49+
50+
SharedArrayBufferMetadataReference reference;
51+
};
52+
53+
} // anonymous namespace
54+
55+
SharedArrayBufferMetadataReference
56+
SharedArrayBufferMetadata::ForSharedArrayBuffer(
57+
Environment* env,
58+
Local<Context> context,
59+
Local<SharedArrayBuffer> source) {
60+
Local<Value> lifetime_partner;
61+
62+
if (!source->GetPrivate(context,
63+
env->sab_lifetimepartner_symbol())
64+
.ToLocal(&lifetime_partner)) {
65+
return nullptr;
66+
}
67+
68+
if (lifetime_partner->IsObject() &&
69+
env->sab_lifetimepartner_constructor_template()
70+
->HasInstance(lifetime_partner)) {
71+
CHECK(source->IsExternal());
72+
SABLifetimePartner* partner =
73+
Unwrap<SABLifetimePartner>(lifetime_partner.As<Object>());
74+
CHECK_NE(partner, nullptr);
75+
return partner->reference;
76+
}
77+
78+
if (source->IsExternal()) {
79+
// If this is an external SharedArrayBuffer but we do not see a lifetime
80+
// partner object, it was not us who externalized it. In that case, there
81+
// is no way to serialize it, because it's unclear how the memory
82+
// is actually owned.
83+
THROW_ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER(env);
84+
return nullptr;
85+
}
86+
87+
SharedArrayBuffer::Contents contents = source->Externalize();
88+
SharedArrayBufferMetadataReference r(new SharedArrayBufferMetadata(
89+
contents.Data(), contents.ByteLength()));
90+
if (r->AssignToSharedArrayBuffer(env, context, source).IsNothing())
91+
return nullptr;
92+
return r;
93+
}
94+
95+
Maybe<bool> SharedArrayBufferMetadata::AssignToSharedArrayBuffer(
96+
Environment* env, Local<Context> context,
97+
Local<SharedArrayBuffer> target) {
98+
CHECK(target->IsExternal());
99+
Local<Function> ctor = GetSABLifetimePartnerConstructor(env, context);
100+
Local<Object> obj;
101+
if (!ctor->NewInstance(context).ToLocal(&obj))
102+
return Nothing<bool>();
103+
104+
new SABLifetimePartner(env, obj, shared_from_this());
105+
return target->SetPrivate(context,
106+
env->sab_lifetimepartner_symbol(),
107+
obj);
108+
}
109+
110+
SharedArrayBufferMetadata::SharedArrayBufferMetadata(void* data, size_t size)
111+
: data(data), size(size) { }
112+
113+
SharedArrayBufferMetadata::~SharedArrayBufferMetadata() {
114+
free(data);
115+
}
116+
117+
MaybeLocal<SharedArrayBuffer> SharedArrayBufferMetadata::GetSharedArrayBuffer(
118+
Environment* env, Local<Context> context) {
119+
Local<SharedArrayBuffer> obj =
120+
SharedArrayBuffer::New(env->isolate(), data, size);
121+
122+
if (AssignToSharedArrayBuffer(env, context, obj).IsNothing())
123+
return MaybeLocal<SharedArrayBuffer>();
124+
125+
return obj;
126+
}
127+
128+
} // namespace worker
129+
} // namespace node

0 commit comments

Comments
 (0)