Skip to content

Commit 8689723

Browse files
committed
lib,src: implement WebAssembly Web API
Refs: #41749 Fixes: #21130
1 parent 6706be1 commit 8689723

12 files changed

+531
-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
@@ -1645,6 +1645,7 @@ E('ERR_VM_MODULE_NOT_MODULE',
16451645
'Provided module is not an instance of Module', Error);
16461646
E('ERR_VM_MODULE_STATUS', 'Module status %s', Error);
16471647
E('ERR_WASI_ALREADY_STARTED', 'WASI instance has already started', Error);
1648+
E('ERR_WEBASSEMBLY_RESPONSE', 'WebAssembly response %s', TypeError);
16481649
E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error);
16491650
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') =>
16501651
`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

+8
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,12 @@ 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()->get_per_env_options()
259+
->experimental_fetch) {
260+
isolate->SetWasmStreamingCallback(wasm_web_api::StartStreamingCompilation);
261+
}
262+
255263
if ((s.flags & SHOULD_NOT_SET_PROMISE_REJECTION_CALLBACK) == 0) {
256264
auto* promise_reject_cb = s.promise_reject_callback ?
257265
s.promise_reject_callback : PromiseRejectCallback;

src/env.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,9 @@ constexpr size_t kFsStatsBufferLength =
552552
V(tls_wrap_constructor_function, v8::Function) \
553553
V(trace_category_state_function, v8::Function) \
554554
V(udp_constructor_function, v8::Function) \
555-
V(url_constructor_function, v8::Function)
555+
V(url_constructor_function, v8::Function) \
556+
V(wasm_streaming_compilation_impl, v8::Function) \
557+
V(wasm_streaming_object_constructor, v8::Function) \
556558

557559
class Environment;
558560
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

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

test/parallel/test-bootstrap-modules.js

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const expectedModules = new Set([
4242
'Internal Binding util',
4343
'Internal Binding uv',
4444
'Internal Binding v8',
45+
'Internal Binding wasm_web_api',
4546
'Internal Binding worker',
4647
'NativeModule buffer',
4748
'NativeModule events',

test/parallel/test-fetch-disabled.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ assert.strictEqual(typeof globalThis.FormData, 'undefined');
88
assert.strictEqual(typeof globalThis.Headers, 'undefined');
99
assert.strictEqual(typeof globalThis.Request, 'undefined');
1010
assert.strictEqual(typeof globalThis.Response, 'undefined');
11+
12+
assert.strictEqual(typeof WebAssembly.compileStreaming, 'undefined');
13+
assert.strictEqual(typeof WebAssembly.instantiateStreaming, 'undefined');

0 commit comments

Comments
 (0)