Skip to content

Commit b0f7c4c

Browse files
tniessentargos
authored andcommitted
lib,src: implement WebAssembly Web API
Refs: #41749 Fixes: #21130 PR-URL: #42701 Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent c6c1dc5 commit b0f7c4c

File tree

161 files changed

+8056
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

161 files changed

+8056
-2
lines changed

doc/api/errors.md

+11
Original file line numberDiff line numberDiff line change
@@ -2890,6 +2890,17 @@ The WASI instance has already started.
28902890

28912891
The WASI instance has not been started.
28922892

2893+
<a id="ERR_WEBASSEMBLY_RESPONSE"></a>
2894+
2895+
### `ERR_WEBASSEMBLY_RESPONSE`
2896+
2897+
<!-- YAML
2898+
added: REPLACEME
2899+
-->
2900+
2901+
The `Response` that has been passed to `WebAssembly.compileStreaming` or to
2902+
`WebAssembly.instantiateStreaming` is not a valid WebAssembly response.
2903+
28932904
<a id="ERR_WORKER_INIT_FAILED"></a>
28942905

28952906
### `ERR_WORKER_INIT_FAILED`

lib/internal/bootstrap/pre_execution.js

+44-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const {
55
ObjectDefineProperties,
66
ObjectDefineProperty,
77
ObjectGetOwnPropertyDescriptor,
8+
PromiseResolve,
89
SafeMap,
910
SafeWeakMap,
1011
StringPrototypeStartsWith,
@@ -24,7 +25,11 @@ const {
2425
} = require('internal/util');
2526

2627
const { Buffer } = require('buffer');
27-
const { ERR_MANIFEST_ASSERT_INTEGRITY } = require('internal/errors').codes;
28+
const {
29+
ERR_INVALID_ARG_TYPE,
30+
ERR_MANIFEST_ASSERT_INTEGRITY,
31+
ERR_WEBASSEMBLY_RESPONSE,
32+
} = require('internal/errors').codes;
2833
const assert = require('internal/assert');
2934

3035
function prepareMainThreadExecution(expandArgv1 = false,
@@ -215,6 +220,44 @@ function setupFetch() {
215220
Request: lazyInterface('Request'),
216221
Response: lazyInterface('Response'),
217222
});
223+
224+
// The WebAssembly Web API: https://webassembly.github.io/spec/web-api
225+
internalBinding('wasm_web_api').setImplementation((streamState, source) => {
226+
(async () => {
227+
const response = await PromiseResolve(source);
228+
if (!(response instanceof lazyUndici().Response)) {
229+
throw new ERR_INVALID_ARG_TYPE(
230+
'source', ['Response', 'Promise resolving to Response'], response);
231+
}
232+
233+
const contentType = response.headers.get('Content-Type');
234+
if (contentType !== 'application/wasm') {
235+
throw new ERR_WEBASSEMBLY_RESPONSE(
236+
`has unsupported MIME type '${contentType}'`);
237+
}
238+
239+
if (!response.ok) {
240+
throw new ERR_WEBASSEMBLY_RESPONSE(
241+
`has status code ${response.status}`);
242+
}
243+
244+
if (response.bodyUsed !== false) {
245+
throw new ERR_WEBASSEMBLY_RESPONSE('body has already been used');
246+
}
247+
248+
// Pass all data from the response body to the WebAssembly compiler.
249+
for await (const chunk of response.body) {
250+
streamState.push(chunk);
251+
}
252+
})().then(() => {
253+
// No error occurred. Tell the implementation that the stream has ended.
254+
streamState.finish();
255+
}, (err) => {
256+
// An error occurred, either because the given object was not a valid
257+
// and usable Response or because a network error occurred.
258+
streamState.abort(err);
259+
});
260+
});
218261
}
219262

220263
// TODO(aduh95): move this to internal/bootstrap/browser when the CLI flag is

lib/internal/errors.js

+1
Original file line numberDiff line numberDiff line change
@@ -1659,6 +1659,7 @@ E('ERR_VM_MODULE_NOT_MODULE',
16591659
'Provided module is not an instance of Module', Error);
16601660
E('ERR_VM_MODULE_STATUS', 'Module status %s', Error);
16611661
E('ERR_WASI_ALREADY_STARTED', 'WASI instance has already started', Error);
1662+
E('ERR_WEBASSEMBLY_RESPONSE', 'WebAssembly response %s', TypeError);
16621663
E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error);
16631664
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') =>
16641665
`Initiated Worker with ${msg}: ${ArrayPrototypeJoin(errors, ', ')}`,

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@
543543
'src/node_util.cc',
544544
'src/node_v8.cc',
545545
'src/node_wasi.cc',
546+
'src/node_wasm_web_api.cc',
546547
'src/node_watchdog.cc',
547548
'src/node_worker.cc',
548549
'src/node_zlib.cc',

src/api/environment.cc

+9
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
#include "node_errors.h"
44
#include "node_internals.h"
55
#include "node_native_module_env.h"
6+
#include "node_options-inl.h"
67
#include "node_platform.h"
78
#include "node_v8_platform-inl.h"
9+
#include "node_wasm_web_api.h"
810
#include "uv.h"
911

1012
#if HAVE_INSPECTOR
@@ -252,6 +254,13 @@ void SetIsolateMiscHandlers(v8::Isolate* isolate, const IsolateSettings& s) {
252254
s.allow_wasm_code_generation_callback : AllowWasmCodeGenerationCallback;
253255
isolate->SetAllowWasmCodeGenerationCallback(allow_wasm_codegen_cb);
254256

257+
Mutex::ScopedLock lock(node::per_process::cli_options_mutex);
258+
if (per_process::cli_options->get_per_isolate_options()
259+
->get_per_env_options()
260+
->experimental_fetch) {
261+
isolate->SetWasmStreamingCallback(wasm_web_api::StartStreamingCompilation);
262+
}
263+
255264
if ((s.flags & SHOULD_NOT_SET_PROMISE_REJECTION_CALLBACK) == 0) {
256265
auto* promise_reject_cb = s.promise_reject_callback ?
257266
s.promise_reject_callback : PromiseRejectCallback;

src/env.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,9 @@ constexpr size_t kFsStatsBufferLength =
550550
V(tls_wrap_constructor_function, v8::Function) \
551551
V(trace_category_state_function, v8::Function) \
552552
V(udp_constructor_function, v8::Function) \
553-
V(url_constructor_function, v8::Function)
553+
V(url_constructor_function, v8::Function) \
554+
V(wasm_streaming_compilation_impl, v8::Function) \
555+
V(wasm_streaming_object_constructor, v8::Function)
554556

555557
class Environment;
556558
struct AllocatedBuffer;

src/node_binding.cc

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
V(uv) \
8888
V(v8) \
8989
V(wasi) \
90+
V(wasm_web_api) \
9091
V(watchdog) \
9192
V(worker) \
9293
V(zlib)

src/node_wasm_web_api.cc

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#include "node_wasm_web_api.h"
2+
3+
#include "memory_tracker-inl.h"
4+
#include "node_errors.h"
5+
6+
namespace node {
7+
namespace wasm_web_api {
8+
9+
using v8::ArrayBuffer;
10+
using v8::ArrayBufferView;
11+
using v8::Context;
12+
using v8::Function;
13+
using v8::FunctionCallbackInfo;
14+
using v8::FunctionTemplate;
15+
using v8::Local;
16+
using v8::MaybeLocal;
17+
using v8::Object;
18+
using v8::Value;
19+
using v8::WasmStreaming;
20+
21+
Local<Function> WasmStreamingObject::Initialize(Environment* env) {
22+
Local<Function> templ = env->wasm_streaming_object_constructor();
23+
if (!templ.IsEmpty()) {
24+
return templ;
25+
}
26+
27+
Local<FunctionTemplate> t = env->NewFunctionTemplate(New);
28+
t->Inherit(BaseObject::GetConstructorTemplate(env));
29+
t->InstanceTemplate()->SetInternalFieldCount(
30+
WasmStreamingObject::kInternalFieldCount);
31+
32+
env->SetProtoMethod(t, "push", Push);
33+
env->SetProtoMethod(t, "finish", Finish);
34+
env->SetProtoMethod(t, "abort", Abort);
35+
36+
auto function = t->GetFunction(env->context()).ToLocalChecked();
37+
env->set_wasm_streaming_object_constructor(function);
38+
return function;
39+
}
40+
41+
void WasmStreamingObject::RegisterExternalReferences(
42+
ExternalReferenceRegistry* registry) {
43+
registry->Register(Push);
44+
registry->Register(Finish);
45+
registry->Register(Abort);
46+
}
47+
48+
void WasmStreamingObject::MemoryInfo(MemoryTracker* tracker) const {
49+
// v8::WasmStreaming is opaque. We assume that the size of the WebAssembly
50+
// module that is being compiled is roughly what V8 allocates (as in, off by
51+
// only a small factor).
52+
tracker->TrackFieldWithSize("streaming", wasm_size_);
53+
}
54+
55+
MaybeLocal<Object> WasmStreamingObject::Create(
56+
Environment* env, std::shared_ptr<WasmStreaming> streaming) {
57+
Local<Function> ctor = Initialize(env);
58+
Local<Object> obj;
59+
if (!ctor->NewInstance(env->context(), 0, nullptr).ToLocal(&obj)) {
60+
return MaybeLocal<Object>();
61+
}
62+
63+
CHECK(streaming);
64+
65+
WasmStreamingObject* ptr = Unwrap<WasmStreamingObject>(obj);
66+
CHECK_NOT_NULL(ptr);
67+
ptr->streaming_ = streaming;
68+
ptr->wasm_size_ = 0;
69+
return obj;
70+
}
71+
72+
void WasmStreamingObject::New(const FunctionCallbackInfo<Value>& args) {
73+
CHECK(args.IsConstructCall());
74+
Environment* env = Environment::GetCurrent(args);
75+
new WasmStreamingObject(env, args.This());
76+
}
77+
78+
void WasmStreamingObject::Push(const FunctionCallbackInfo<Value>& args) {
79+
WasmStreamingObject* obj;
80+
ASSIGN_OR_RETURN_UNWRAP(&obj, args.Holder());
81+
CHECK(obj->streaming_);
82+
83+
CHECK_EQ(args.Length(), 1);
84+
Local<Value> chunk = args[0];
85+
86+
// The start of the memory section backing the ArrayBuffer(View), the offset
87+
// of the ArrayBuffer(View) within the memory section, and its size in bytes.
88+
const void* bytes;
89+
size_t offset;
90+
size_t size;
91+
92+
if (LIKELY(chunk->IsArrayBufferView())) {
93+
Local<ArrayBufferView> view = chunk.As<ArrayBufferView>();
94+
bytes = view->Buffer()->GetBackingStore()->Data();
95+
offset = view->ByteOffset();
96+
size = view->ByteLength();
97+
} else if (LIKELY(chunk->IsArrayBuffer())) {
98+
Local<ArrayBuffer> buffer = chunk.As<ArrayBuffer>();
99+
bytes = buffer->GetBackingStore()->Data();
100+
offset = 0;
101+
size = buffer->ByteLength();
102+
} else {
103+
return node::THROW_ERR_INVALID_ARG_TYPE(
104+
Environment::GetCurrent(args),
105+
"chunk must be an ArrayBufferView or an ArrayBuffer");
106+
}
107+
108+
// Forward the data to V8. Internally, V8 will make a copy.
109+
obj->streaming_->OnBytesReceived(static_cast<const uint8_t*>(bytes) + offset,
110+
size);
111+
obj->wasm_size_ += size;
112+
}
113+
114+
void WasmStreamingObject::Finish(const FunctionCallbackInfo<Value>& args) {
115+
WasmStreamingObject* obj;
116+
ASSIGN_OR_RETURN_UNWRAP(&obj, args.Holder());
117+
CHECK(obj->streaming_);
118+
119+
CHECK_EQ(args.Length(), 0);
120+
obj->streaming_->Finish();
121+
}
122+
123+
void WasmStreamingObject::Abort(const FunctionCallbackInfo<Value>& args) {
124+
WasmStreamingObject* obj;
125+
ASSIGN_OR_RETURN_UNWRAP(&obj, args.Holder());
126+
CHECK(obj->streaming_);
127+
128+
CHECK_EQ(args.Length(), 1);
129+
obj->streaming_->Abort(args[0]);
130+
}
131+
132+
void StartStreamingCompilation(const FunctionCallbackInfo<Value>& info) {
133+
// V8 passes an instance of v8::WasmStreaming to this callback, which we can
134+
// use to pass the WebAssembly module bytes to V8 as we receive them.
135+
// Unfortunately, our fetch() implementation is a JavaScript dependency, so it
136+
// is difficult to implement the required logic here. Instead, we create a
137+
// a WasmStreamingObject that encapsulates v8::WasmStreaming and that we can
138+
// pass to the JavaScript implementation. The JavaScript implementation can
139+
// then push() bytes from the Response and eventually either finish() or
140+
// abort() the operation.
141+
142+
// Create the wrapper object.
143+
std::shared_ptr<WasmStreaming> streaming =
144+
WasmStreaming::Unpack(info.GetIsolate(), info.Data());
145+
Environment* env = Environment::GetCurrent(info);
146+
Local<Object> obj;
147+
if (!WasmStreamingObject::Create(env, streaming).ToLocal(&obj)) {
148+
// A JavaScript exception is pending. Let V8 deal with it.
149+
return;
150+
}
151+
152+
// V8 always passes one argument to this callback.
153+
CHECK_EQ(info.Length(), 1);
154+
155+
// Prepare the JavaScript implementation for invocation. We will pass the
156+
// WasmStreamingObject as the first argument, followed by the argument that we
157+
// received from V8, i.e., the first argument passed to compileStreaming (or
158+
// instantiateStreaming).
159+
Local<Function> impl = env->wasm_streaming_compilation_impl();
160+
CHECK(!impl.IsEmpty());
161+
Local<Value> args[] = {obj, info[0]};
162+
163+
// Hand control to the JavaScript implementation. It should never throw an
164+
// error, but if it does, we leave it to the calling V8 code to handle that
165+
// gracefully. Otherwise, we assert that the JavaScript function does not
166+
// return anything.
167+
MaybeLocal<Value> maybe_ret =
168+
impl->Call(env->context(), info.This(), 2, args);
169+
Local<Value> ret;
170+
CHECK_IMPLIES(maybe_ret.ToLocal(&ret), ret->IsUndefined());
171+
}
172+
173+
// Called once by JavaScript during initialization.
174+
void SetImplementation(const FunctionCallbackInfo<Value>& info) {
175+
Environment* env = Environment::GetCurrent(info);
176+
env->set_wasm_streaming_compilation_impl(info[0].As<Function>());
177+
}
178+
179+
void Initialize(Local<Object> target,
180+
Local<Value>,
181+
Local<Context> context,
182+
void*) {
183+
Environment* env = Environment::GetCurrent(context);
184+
env->SetMethod(target, "setImplementation", SetImplementation);
185+
}
186+
187+
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
188+
registry->Register(SetImplementation);
189+
}
190+
191+
} // namespace wasm_web_api
192+
} // namespace node
193+
194+
NODE_MODULE_CONTEXT_AWARE_INTERNAL(wasm_web_api, node::wasm_web_api::Initialize)
195+
NODE_MODULE_EXTERNAL_REFERENCE(wasm_web_api,
196+
node::wasm_web_api::RegisterExternalReferences)

src/node_wasm_web_api.h

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#ifndef SRC_NODE_WASM_WEB_API_H_
2+
#define SRC_NODE_WASM_WEB_API_H_
3+
4+
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
5+
6+
#include "base_object-inl.h"
7+
#include "v8.h"
8+
9+
namespace node {
10+
namespace wasm_web_api {
11+
12+
// Wrapper for interacting with a v8::WasmStreaming instance from JavaScript.
13+
class WasmStreamingObject final : public BaseObject {
14+
public:
15+
static v8::Local<v8::Function> Initialize(Environment* env);
16+
17+
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
18+
19+
void MemoryInfo(MemoryTracker* tracker) const override;
20+
SET_MEMORY_INFO_NAME(WasmStreamingObject)
21+
SET_SELF_SIZE(WasmStreamingObject)
22+
23+
static v8::MaybeLocal<v8::Object> Create(
24+
Environment* env, std::shared_ptr<v8::WasmStreaming> streaming);
25+
26+
private:
27+
WasmStreamingObject(Environment* env, v8::Local<v8::Object> object)
28+
: BaseObject(env, object) {
29+
MakeWeak();
30+
}
31+
32+
~WasmStreamingObject() override {}
33+
34+
private:
35+
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
36+
static void Push(const v8::FunctionCallbackInfo<v8::Value>& args);
37+
static void Finish(const v8::FunctionCallbackInfo<v8::Value>& args);
38+
static void Abort(const v8::FunctionCallbackInfo<v8::Value>& args);
39+
40+
std::shared_ptr<v8::WasmStreaming> streaming_;
41+
size_t wasm_size_;
42+
};
43+
44+
// This is a v8::WasmStreamingCallback implementation that must be passed to
45+
// v8::Isolate::SetWasmStreamingCallback when setting up the isolate in order to
46+
// enable the WebAssembly.(compile|instantiate)Streaming APIs.
47+
void StartStreamingCompilation(const v8::FunctionCallbackInfo<v8::Value>& args);
48+
49+
} // namespace wasm_web_api
50+
} // namespace node
51+
52+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
53+
54+
#endif // SRC_NODE_WASM_WEB_API_H_

0 commit comments

Comments
 (0)