Skip to content

Commit

Permalink
lib,src: implement WebAssembly Web API
Browse files Browse the repository at this point in the history
Refs: #41749
Fixes: #21130
  • Loading branch information
tniessen committed Apr 12, 2022
1 parent 6706be1 commit f76bda6
Show file tree
Hide file tree
Showing 12 changed files with 530 additions and 2 deletions.
11 changes: 11 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2890,6 +2890,17 @@ The WASI instance has already started.

The WASI instance has not been started.

<a id="ERR_WEBASSEMBLY_RESPONSE"></a>

### `ERR_WEBASSEMBLY_RESPONSE`

<!-- YAML
added: REPLACEME
-->

The `Response` that has been passed to `WebAssembly.compileStreaming` or to
`WebAssembly.instantiateStreaming` is not a valid WebAssembly response.

<a id="ERR_WORKER_INIT_FAILED"></a>

### `ERR_WORKER_INIT_FAILED`
Expand Down
45 changes: 44 additions & 1 deletion lib/internal/bootstrap/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
ObjectDefineProperties,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
PromiseResolve,
SafeMap,
SafeWeakMap,
StringPrototypeStartsWith,
Expand All @@ -24,7 +25,11 @@ const {
} = require('internal/util');

const { Buffer } = require('buffer');
const { ERR_MANIFEST_ASSERT_INTEGRITY } = require('internal/errors').codes;
const {
ERR_INVALID_ARG_TYPE,
ERR_MANIFEST_ASSERT_INTEGRITY,
ERR_WEBASSEMBLY_RESPONSE,
} = require('internal/errors').codes;
const assert = require('internal/assert');

function prepareMainThreadExecution(expandArgv1 = false,
Expand Down Expand Up @@ -215,6 +220,44 @@ function setupFetch() {
Request: lazyInterface('Request'),
Response: lazyInterface('Response'),
});

// The WebAssembly Web API: https://webassembly.github.io/spec/web-api
internalBinding('wasm_web_api').setImplementation((streamState, source) => {
(async () => {
const response = await PromiseResolve(source);
if (!(response instanceof lazyUndici().Response)) {
throw new ERR_INVALID_ARG_TYPE(
'source', ['Response', 'Promise resolving to Response'], response);
}

const contentType = response.headers.get('Content-Type');
if (contentType !== 'application/wasm') {
throw new ERR_WEBASSEMBLY_RESPONSE(
`has unsupported MIME type '${contentType}'`);
}

if (!response.ok) {
throw new ERR_WEBASSEMBLY_RESPONSE(
`has status code ${response.status}`);
}

if (response.bodyUsed !== false) {
throw new ERR_WEBASSEMBLY_RESPONSE('body has already been used');
}

// Pass all data from the response body to the WebAssembly compiler.
for await (const chunk of response.body) {
streamState.push(chunk);
}
})().then(() => {
// No error occurred. Tell the implementation that the stream has ended.
streamState.finish();
}, (err) => {
// An error occurred, either because the given object was not a valid
// and usable Response or because a network error occurred.
streamState.abort(err);
});
});
}

// TODO(aduh95): move this to internal/bootstrap/browser when the CLI flag is
Expand Down
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,7 @@ E('ERR_VM_MODULE_NOT_MODULE',
'Provided module is not an instance of Module', Error);
E('ERR_VM_MODULE_STATUS', 'Module status %s', Error);
E('ERR_WASI_ALREADY_STARTED', 'WASI instance has already started', Error);
E('ERR_WEBASSEMBLY_RESPONSE', 'WebAssembly response %s', TypeError);
E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error);
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') =>
`Initiated Worker with ${msg}: ${ArrayPrototypeJoin(errors, ', ')}`,
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@
'src/node_util.cc',
'src/node_v8.cc',
'src/node_wasi.cc',
'src/node_wasm_web_api.cc',
'src/node_watchdog.cc',
'src/node_worker.cc',
'src/node_zlib.cc',
Expand Down
8 changes: 8 additions & 0 deletions src/api/environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
#include "node_errors.h"
#include "node_internals.h"
#include "node_native_module_env.h"
#include "node_options-inl.h"
#include "node_platform.h"
#include "node_v8_platform-inl.h"
#include "node_wasm_web_api.h"
#include "uv.h"

#if HAVE_INSPECTOR
Expand Down Expand Up @@ -252,6 +254,12 @@ void SetIsolateMiscHandlers(v8::Isolate* isolate, const IsolateSettings& s) {
s.allow_wasm_code_generation_callback : AllowWasmCodeGenerationCallback;
isolate->SetAllowWasmCodeGenerationCallback(allow_wasm_codegen_cb);

Mutex::ScopedLock lock(node::per_process::cli_options_mutex);
if (per_process::cli_options->get_per_isolate_options()->get_per_env_options()
->experimental_fetch) {
isolate->SetWasmStreamingCallback(wasm_web_api::StartStreamingCompilation);
}

if ((s.flags & SHOULD_NOT_SET_PROMISE_REJECTION_CALLBACK) == 0) {
auto* promise_reject_cb = s.promise_reject_callback ?
s.promise_reject_callback : PromiseRejectCallback;
Expand Down
4 changes: 3 additions & 1 deletion src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,9 @@ constexpr size_t kFsStatsBufferLength =
V(tls_wrap_constructor_function, v8::Function) \
V(trace_category_state_function, v8::Function) \
V(udp_constructor_function, v8::Function) \
V(url_constructor_function, v8::Function)
V(url_constructor_function, v8::Function) \
V(wasm_streaming_compilation_impl, v8::Function) \
V(wasm_streaming_object_constructor, v8::Function) \

class Environment;
struct AllocatedBuffer;
Expand Down
1 change: 1 addition & 0 deletions src/node_binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
V(uv) \
V(v8) \
V(wasi) \
V(wasm_web_api) \
V(watchdog) \
V(worker) \
V(zlib)
Expand Down
178 changes: 178 additions & 0 deletions src/node_wasm_web_api.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#include "node_wasm_web_api.h"

#include "node_errors.h"

namespace node {
namespace wasm_web_api {

v8::Local<v8::Function> WasmStreamingObject::Initialize(Environment* env) {
v8::Local<v8::Function> templ = env->wasm_streaming_object_constructor();
if (!templ.IsEmpty()) {
return templ;
}

v8::Local<v8::FunctionTemplate> t = env->NewFunctionTemplate(New);
t->Inherit(BaseObject::GetConstructorTemplate(env));
t->InstanceTemplate()->SetInternalFieldCount(
WasmStreamingObject::kInternalFieldCount);

env->SetProtoMethod(t, "push", Push);
env->SetProtoMethod(t, "finish", Finish);
env->SetProtoMethod(t, "abort", Abort);

auto function = t->GetFunction(env->context()).ToLocalChecked();
env->set_wasm_streaming_object_constructor(function);
return function;
}

void WasmStreamingObject::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(Push);
registry->Register(Finish);
registry->Register(Abort);
}

v8::MaybeLocal<v8::Object> WasmStreamingObject::Create(
Environment* env, std::shared_ptr<v8::WasmStreaming> streaming) {
v8::Local<v8::Function> ctor = Initialize(env);
v8::Local<v8::Object> obj;
if (!ctor->NewInstance(env->context(), 0, nullptr).ToLocal(&obj)) {
return v8::MaybeLocal<v8::Object>();
}

CHECK(streaming);

WasmStreamingObject* ptr = Unwrap<WasmStreamingObject>(obj);
CHECK_NOT_NULL(ptr);
ptr->streaming_ = streaming;
return obj;
}

void WasmStreamingObject::New(const v8::FunctionCallbackInfo<v8::Value>& args) {
CHECK(args.IsConstructCall());
Environment* env = Environment::GetCurrent(args);
new WasmStreamingObject(env, args.This());
}

void WasmStreamingObject::Push(
const v8::FunctionCallbackInfo<v8::Value>& args) {
WasmStreamingObject* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, args.Holder());
CHECK(obj->streaming_);

CHECK_EQ(args.Length(), 1);
v8::Local<v8::Value> chunk = args[0];

// The start of the memory section backing the ArrayBuffer(View), the offset
// of the ArrayBuffer(View) within the memory section, and its size in bytes.
const void* bytes;
size_t offset;
size_t size;

if (LIKELY(chunk->IsArrayBufferView())) {
v8::Local<v8::ArrayBufferView> view = chunk.As<v8::ArrayBufferView>();
bytes = view->Buffer()->GetBackingStore()->Data();
offset = view->ByteOffset();
size = view->ByteLength();
} else if (LIKELY(chunk->IsArrayBuffer())) {
v8::Local<v8::ArrayBuffer> buffer = chunk.As<v8::ArrayBuffer>();
bytes = buffer->GetBackingStore()->Data();
offset = 0;
size = buffer->ByteLength();
} else {
return node::THROW_ERR_INVALID_ARG_TYPE(
Environment::GetCurrent(args),
"chunk must be an ArrayBufferView or an ArrayBuffer");
}

// Forward the data to V8. Internally, V8 will make a copy.
obj->streaming_->OnBytesReceived(
static_cast<const uint8_t*>(bytes) + offset, size);
}

void WasmStreamingObject::Finish(
const v8::FunctionCallbackInfo<v8::Value>& args) {
WasmStreamingObject* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, args.Holder());
CHECK(obj->streaming_);

CHECK_EQ(args.Length(), 0);
obj->streaming_->Finish();
}

void WasmStreamingObject::Abort(
const v8::FunctionCallbackInfo<v8::Value>& args) {
WasmStreamingObject* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, args.Holder());
CHECK(obj->streaming_);

CHECK_EQ(args.Length(), 1);
obj->streaming_->Abort(args[0]);
}

void StartStreamingCompilation(
const v8::FunctionCallbackInfo<v8::Value>& info) {
// V8 passes an instance of v8::WasmStreaming to this callback, which we can
// use to pass the WebAssembly module bytes to V8 as we receive them.
// Unfortunately, our fetch() implementation is a JavaScript dependency, so it
// is difficult to implement the required logic here. Instead, we create a
// a WasmStreamingObject that encapsulates v8::WasmStreaming and that we can
// pass to the JavaScript implementation. The JavaScript implementation can
// then push() bytes from the Response and eventually either finish() or
// abort() the operation.

// Create the wrapper object.
std::shared_ptr<v8::WasmStreaming> streaming =
v8::WasmStreaming::Unpack(info.GetIsolate(), info.Data());
Environment* env = Environment::GetCurrent(info);
v8::Local<v8::Object> obj;
if (!WasmStreamingObject::Create(env, streaming).ToLocal(&obj)) {
// A JavaScript exception is pending. Let V8 deal with it.
return;
}

// V8 always passes one argument to this callback.
CHECK_EQ(info.Length(), 1);

// Prepare the JavaScript implementation for invocation. We will pass the
// WasmStreamingObject as the first argument, followed by the argument that we
// received from V8, i.e., the first argument passed to compileStreaming (or
// instantiateStreaming).
v8::Local<v8::Function> impl = env->wasm_streaming_compilation_impl();
CHECK(!impl.IsEmpty());
v8::Local<v8::Value> args[] = { obj, info[0] };

// Hand control to the JavaScript implementation. It should never throw an
// error, but if it does, we leave it to the calling V8 code to handle that
// gracefully. Otherwise, we assert that the JavaScript function does not
// return anything.
v8::MaybeLocal<v8::Value> maybe_ret =
impl->Call(env->context(), info.This(), 2, args);
v8::Local<v8::Value> ret;
CHECK_IMPLIES(maybe_ret.ToLocal(&ret), ret->IsUndefined());
}

// Called once by JavaScript during initialization.
void SetImplementation(const v8::FunctionCallbackInfo<v8::Value>& info) {
Environment* env = Environment::GetCurrent(info);
env->set_wasm_streaming_compilation_impl(info[0].As<v8::Function>());
}

void Initialize(v8::Local<v8::Object> target,
v8::Local<v8::Value>,
v8::Local<v8::Context> context,
void*) {
Environment* env = Environment::GetCurrent(context);
env->SetMethod(target, "setImplementation", SetImplementation);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(SetImplementation);
}

} // namespace wasm_web_api
} // namespace node

NODE_MODULE_CONTEXT_AWARE_INTERNAL(wasm_web_api, node::wasm_web_api::Initialize)
NODE_MODULE_EXTERNAL_REFERENCE(wasm_web_api,
node::wasm_web_api::RegisterExternalReferences)
54 changes: 54 additions & 0 deletions src/node_wasm_web_api.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#ifndef SRC_NODE_WASM_WEB_API_H_
#define SRC_NODE_WASM_WEB_API_H_

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include "base_object-inl.h"
#include "v8.h"

namespace node {
namespace wasm_web_api {

// Wrapper for interacting with a v8::WasmStreaming instance from JavaScript.
class WasmStreamingObject final : public BaseObject {
public:
static v8::Local<v8::Function> Initialize(Environment* env);

static void RegisterExternalReferences(ExternalReferenceRegistry* registry);

void MemoryInfo(MemoryTracker* tracker) const override {}
SET_MEMORY_INFO_NAME(WasmStreamingObject)
SET_SELF_SIZE(WasmStreamingObject)

static v8::MaybeLocal<v8::Object> Create(
Environment* env, std::shared_ptr<v8::WasmStreaming> streaming);

private:
WasmStreamingObject(Environment* env, v8::Local<v8::Object> object)
: BaseObject(env, object) {
MakeWeak();
}

~WasmStreamingObject() override {}

private:
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Push(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Finish(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Abort(const v8::FunctionCallbackInfo<v8::Value>& args);

std::shared_ptr<v8::WasmStreaming> streaming_;
};

// This is a v8::WasmStreamingCallback implementation that must be passed to
// v8::Isolate::SetWasmStreamingCallback when setting up the isolate in order to
// enable the WebAssembly.(compile|instantiate)Streaming APIs.
void StartStreamingCompilation(
const v8::FunctionCallbackInfo<v8::Value>& args);

} // namespace wasm_web_api
} // namespace node

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#endif // SRC_NODE_WASM_WEB_API_H_
1 change: 1 addition & 0 deletions test/parallel/test-bootstrap-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const expectedModules = new Set([
'Internal Binding util',
'Internal Binding uv',
'Internal Binding v8',
'Internal Binding wasm_web_api',
'Internal Binding worker',
'NativeModule buffer',
'NativeModule events',
Expand Down
3 changes: 3 additions & 0 deletions test/parallel/test-fetch-disabled.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ assert.strictEqual(typeof globalThis.FormData, 'undefined');
assert.strictEqual(typeof globalThis.Headers, 'undefined');
assert.strictEqual(typeof globalThis.Request, 'undefined');
assert.strictEqual(typeof globalThis.Response, 'undefined');

assert.strictEqual(typeof WebAssembly.compileStreaming, 'undefined');
assert.strictEqual(typeof WebAssembly.instantiateStreaming, 'undefined');
Loading

0 comments on commit f76bda6

Please sign in to comment.