Sfoglia il codice sorgente

LibWeb: Add Wasm Web-API streaming compilation and instantiation

This requires fixing up the "parameter is a promise" handling in
the IDL generator.
Andrew Kaster 8 mesi fa
parent
commit
36feebb1e7

+ 6 - 9
Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp

@@ -669,15 +669,12 @@ static void generate_to_cpp(SourceGenerator& generator, ParameterType& parameter
     } else if (parameter.type->name() == "Promise") {
         // https://webidl.spec.whatwg.org/#js-promise
         scoped_generator.append(R"~~~(
-    if (!@js_name@@js_suffix@.is_cell() || !is<JS::PromiseCapability>(@js_name@@js_suffix@.as_cell())) {
-        // 1. Let promiseCapability be ? NewPromiseCapability(%Promise%).
-        auto promise_capability = TRY(JS::new_promise_capability(vm, realm.intrinsics().promise_constructor()));
-        // 2. Perform ? Call(promiseCapability.[[Resolve]], undefined, « V »).
-        TRY(JS::call(vm, *promise_capability->resolve(), JS::js_undefined(), @js_name@@js_suffix@));
-        // 3. Return promiseCapability.
-        @js_name@@js_suffix@ = promise_capability;
-    }
-    auto @cpp_name@ = JS::make_handle(static_cast<JS::PromiseCapability&>(@js_name@@js_suffix@.as_cell()));
+    // 1. Let promiseCapability be ? NewPromiseCapability(%Promise%).
+    auto promise_capability = TRY(JS::new_promise_capability(vm, realm.intrinsics().promise_constructor()));
+    // 2. Perform ? Call(promiseCapability.[[Resolve]], undefined, « V »).
+    TRY(JS::call(vm, *promise_capability->resolve(), JS::js_undefined(), @js_name@@js_suffix@));
+    // 3. Return promiseCapability.
+    auto @cpp_name@ = JS::make_handle(promise_capability);
 )~~~");
     } else if (parameter.type->name() == "object") {
         if (parameter.type->is_nullable()) {

BIN
Tests/LibWeb/Text/data/greeter.wasm


+ 5 - 0
Tests/LibWeb/Text/expected/Wasm/WebAssembly-instantiate-streaming.txt

@@ -0,0 +1,5 @@
+WebAssembly.instantiateStreaming
+Hello from wasm!!!!!!
+WebAssembly.compileStreaming
+Hello from wasm!!!!!!
+Sanity check

+ 102 - 0
Tests/LibWeb/Text/input/Wasm/WebAssembly-instantiate-streaming.html

@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<script src="../include.js"></script>
+<script>
+    asyncTest(async (done) => {
+        const WASM_FILE = "../../data/greeter.wasm";
+
+        let wasm;
+
+        const lTextDecoder = typeof TextDecoder === 'undefined' ? (0, module.require)('util').TextDecoder : TextDecoder;
+
+        let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true });
+
+        cachedTextDecoder.decode();
+
+        function getUint8Memory0() {
+            return  new Uint8Array(wasm.memory.buffer);
+        }
+
+        function getStringFromWasm0(ptr, len) {
+            ptr = ptr >>> 0;
+            return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
+        }
+
+        const exports = {
+            "./wasm_test_bg.js": {
+                __wbg_println_95d984b86202de7b(arg0, arg1) {
+                    println(getStringFromWasm0(arg0, arg1));
+                },
+                greet() {
+                    wasm.greet();
+                },
+            },
+        };
+
+        function runTest(module) {
+            wasm = module.instance?.exports ?? module.exports;
+            try {
+                wasm.greet();
+            } catch (e) {
+                println(`FAILED: ${e.message}`);
+            }
+        }
+
+        println("WebAssembly.instantiateStreaming");
+        let module = await WebAssembly.instantiateStreaming(fetch(WASM_FILE), exports).catch(e => {
+            println(`FAILED: ${e.message}`);
+            return Promise.resolve();
+        });
+        if (module) {
+            runTest(module);
+        }
+
+        println("WebAssembly.compileStreaming");
+        let compiled = await WebAssembly.compileStreaming(fetch(WASM_FILE)).catch(e => {
+            println(`FAILED: ${e.message}`);
+            return Promise.resolve();
+        });
+        if (compiled) {
+            let module = await WebAssembly.instantiate(compiled, exports).catch(e => {
+                println(`FAILED: ${e.message}`);
+                return Promise.resolve();
+            });
+            runTest(module);
+        }
+
+        // Sanity check after running meat of the test
+        println("Sanity check");
+        const arrayBuffer = new Uint8Array([
+            0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x09, 0x02, 0x60, 0x02, 0x7f, 0x7f, 0x00,
+            0x60, 0x00, 0x00, 0x02, 0x34, 0x01, 0x11, 0x2e, 0x2f, 0x77, 0x61, 0x73, 0x6d, 0x5f, 0x74, 0x65,
+            0x73, 0x74, 0x5f, 0x62, 0x67, 0x2e, 0x6a, 0x73, 0x1e, 0x5f, 0x5f, 0x77, 0x62, 0x67, 0x5f, 0x70,
+            0x72, 0x69, 0x6e, 0x74, 0x6c, 0x6e, 0x5f, 0x39, 0x35, 0x64, 0x39, 0x38, 0x34, 0x62, 0x38, 0x36,
+            0x32, 0x30, 0x32, 0x64, 0x65, 0x37, 0x62, 0x00, 0x00, 0x03, 0x02, 0x01, 0x01, 0x05, 0x03, 0x01,
+            0x00, 0x11, 0x07, 0x12, 0x02, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x05, 0x67,
+            0x72, 0x65, 0x65, 0x74, 0x00, 0x01, 0x0a, 0x0d, 0x01, 0x0b, 0x00, 0x41, 0x80, 0x80, 0xc0, 0x00,
+            0x41, 0x15, 0x10, 0x00, 0x0b, 0x0b, 0x1e, 0x01, 0x00, 0x41, 0x80, 0x80, 0xc0, 0x00, 0x0b, 0x15,
+            0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x77, 0x61, 0x73, 0x6d, 0x21,
+            0x21, 0x21, 0x21, 0x21, 0x21, 0x00, 0x7b, 0x09, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x65, 0x72,
+            0x73, 0x02, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x01, 0x04, 0x52, 0x75, 0x73,
+            0x74, 0x00, 0x0c, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x2d, 0x62, 0x79, 0x03,
+            0x05, 0x72, 0x75, 0x73, 0x74, 0x63, 0x1d, 0x31, 0x2e, 0x37, 0x33, 0x2e, 0x30, 0x20, 0x28, 0x63,
+            0x63, 0x36, 0x36, 0x61, 0x64, 0x34, 0x36, 0x38, 0x20, 0x32, 0x30, 0x32, 0x33, 0x2d, 0x31, 0x30,
+            0x2d, 0x30, 0x33, 0x29, 0x06, 0x77, 0x61, 0x6c, 0x72, 0x75, 0x73, 0x06, 0x30, 0x2e, 0x31, 0x39,
+            0x2e, 0x30, 0x0c, 0x77, 0x61, 0x73, 0x6d, 0x2d, 0x62, 0x69, 0x6e, 0x64, 0x67, 0x65, 0x6e, 0x12,
+            0x30, 0x2e, 0x32, 0x2e, 0x38, 0x38, 0x20, 0x28, 0x30, 0x62, 0x35, 0x66, 0x30, 0x65, 0x65, 0x63,
+            0x32, 0x29, 0x00, 0x2c, 0x0f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x66, 0x65, 0x61, 0x74,
+            0x75, 0x72, 0x65, 0x73, 0x02, 0x2b, 0x0f, 0x6d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x2d, 0x67,
+            0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x73, 0x2b, 0x08, 0x73, 0x69, 0x67, 0x6e, 0x2d, 0x65, 0x78, 0x74
+        ]).buffer;
+        const fetchedBuffer = new Uint8Array(await (await fetch(WASM_FILE)).arrayBuffer()).buffer;
+        if (arrayBuffer.byteLength !== fetchedBuffer.byteLength) {
+            println(`FAILED: Mismatch in byte length: ${arrayBuffer.byteLength} !== ${fetchedBuffer.byteLength}`);
+        }
+        for (let i = 0; i < arrayBuffer.byteLength; i++) {
+            if (new Uint8Array(arrayBuffer)[i] !== new Uint8Array(fetchedBuffer)[i]) {
+                println(`FAILED: Mismatch at byte ${i}`);
+            }
+        }
+
+        done();
+    });
+</script>

+ 129 - 0
Userland/Libraries/LibWeb/WebAssembly/WebAssembly.cpp

@@ -19,6 +19,8 @@
 #include <LibJS/Runtime/TypedArray.h>
 #include <LibJS/Runtime/VM.h>
 #include <LibWasm/AbstractMachine/Validator.h>
+#include <LibWeb/Bindings/ResponsePrototype.h>
+#include <LibWeb/Fetch/Response.h>
 #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
 #include <LibWeb/Platform/EventLoopPlugin.h>
 #include <LibWeb/WebAssembly/Instance.h>
@@ -35,6 +37,7 @@ namespace Web::WebAssembly {
 static JS::NonnullGCPtr<WebIDL::Promise> asynchronously_compile_webassembly_module(JS::VM&, ByteBuffer, HTML::Task::Source = HTML::Task::Source::Unspecified);
 static JS::NonnullGCPtr<WebIDL::Promise> instantiate_promise_of_module(JS::VM&, JS::NonnullGCPtr<WebIDL::Promise>, JS::GCPtr<JS::Object> import_object);
 static JS::NonnullGCPtr<WebIDL::Promise> asynchronously_instantiate_webassembly_module(JS::VM&, JS::NonnullGCPtr<Module>, JS::GCPtr<JS::Object> import_object);
+static JS::NonnullGCPtr<WebIDL::Promise> compile_potential_webassembly_response(JS::VM&, JS::NonnullGCPtr<WebIDL::Promise>);
 
 namespace Detail {
 
@@ -101,6 +104,13 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> compile(JS::VM& vm, JS::H
     return asynchronously_compile_webassembly_module(vm, stable_bytes.release_value());
 }
 
+// https://webassembly.github.io/spec/web-api/index.html#dom-webassembly-compilestreaming
+WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> compile_streaming(JS::VM& vm, JS::Handle<WebIDL::Promise> source)
+{
+    //  The compileStreaming(source) method, when invoked, returns the result of compiling a potential WebAssembly response with source.
+    return compile_potential_webassembly_response(vm, *source);
+}
+
 // https://webassembly.github.io/spec/js-api/#dom-webassembly-instantiate
 WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> instantiate(JS::VM& vm, JS::Handle<WebIDL::BufferSource>& bytes, Optional<JS::Handle<JS::Object>>& import_object_handle)
 {
@@ -130,6 +140,19 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> instantiate(JS::VM& vm, M
     return asynchronously_instantiate_webassembly_module(vm, module, imports);
 }
 
+// https://webassembly.github.io/spec/web-api/index.html#dom-webassembly-instantiatestreaming
+WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> instantiate_streaming(JS::VM& vm, JS::Handle<WebIDL::Promise> source, Optional<JS::Handle<JS::Object>>& import_object)
+{
+    // The instantiateStreaming(source, importObject) method, when invoked, performs the following steps:
+
+    // 1. Let promiseOfModule be the result of compiling a potential WebAssembly response with source.
+    auto promise_of_module = compile_potential_webassembly_response(vm, *source);
+
+    // 2. Return the result of instantiating the promise of a module promiseOfModule with imports importObject.
+    auto imports = JS::GCPtr { import_object.has_value() ? import_object.value().ptr() : nullptr };
+    return instantiate_promise_of_module(vm, promise_of_module, imports);
+}
+
 namespace Detail {
 
 JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS::VM& vm, Wasm::Module const& module, JS::GCPtr<JS::Object> import_object)
@@ -624,4 +647,110 @@ JS::NonnullGCPtr<WebIDL::Promise> instantiate_promise_of_module(JS::VM& vm, JS::
     return promise;
 }
 
+// https://webassembly.github.io/spec/web-api/index.html#compile-a-potential-webassembly-response
+JS::NonnullGCPtr<WebIDL::Promise> compile_potential_webassembly_response(JS::VM& vm, JS::NonnullGCPtr<WebIDL::Promise> source)
+{
+    auto& realm = *vm.current_realm();
+
+    // Note: This algorithm accepts a Response object, or a promise for one, and compiles and instantiates the resulting bytes of the response.
+    //       This compilation can be performed in the background and in a streaming manner.
+    //       If the Response is not CORS-same-origin, does not represent an ok status, or does not match the `application/wasm` MIME type,
+    //       the returned promise will be rejected with a TypeError; if compilation or instantiation fails,
+    //       the returned promise will be rejected with a CompileError or other relevant error type, depending on the cause of failure.
+
+    // 1. Let returnValue be a new promise
+    auto return_value = WebIDL::create_promise(realm);
+
+    // 2. Upon fulfillment of source with value unwrappedSource:
+    auto fulfillment_steps = JS::create_heap_function(vm.heap(), [&vm, return_value](JS::Value unwrapped_source) -> WebIDL::ExceptionOr<JS::Value> {
+        auto& realm = HTML::relevant_realm(*return_value->promise());
+
+        // 1. Let response be unwrappedSource’s response.
+        if (!unwrapped_source.is_object() || !is<Fetch::Response>(unwrapped_source.as_object())) {
+            WebIDL::reject_promise(realm, return_value, *vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "Response").value());
+            return JS::js_undefined();
+        }
+        auto& response_object = static_cast<Fetch::Response&>(unwrapped_source.as_object());
+        auto response = response_object.response();
+
+        // 2. Let mimeType be the result of getting `Content-Type` from response’s header list.
+        // 3. If mimeType is null, reject returnValue with a TypeError and abort these substeps.
+        // 4. Remove all HTTP tab or space byte from the start and end of mimeType.
+        // 5. If mimeType is not a byte-case-insensitive match for `application/wasm`, reject returnValue with a TypeError and abort these substeps.
+        // Note: extra parameters are not allowed, including the empty `application/wasm;`.
+        // FIXME: Validate these extra constraints that are not checked by extract_mime_type()
+        if (auto mime = response->header_list()->extract_mime_type(); !mime.has_value() || mime.value().essence() != "application/wasm"sv) {
+            WebIDL::reject_promise(realm, return_value, *vm.throw_completion<JS::TypeError>("Response does not match the application/wasm MIME type"sv).value());
+            return JS::js_undefined();
+        }
+
+        // 6. If response is not CORS-same-origin, reject returnValue with a TypeError and abort these substeps.
+        // https://html.spec.whatwg.org/#cors-same-origin
+        auto type = response_object.type();
+        if (type != Bindings::ResponseType::Basic && type != Bindings::ResponseType::Cors && type != Bindings::ResponseType::Default) {
+            WebIDL::reject_promise(realm, return_value, *vm.throw_completion<JS::TypeError>("Response is not CORS-same-origin"sv).value());
+            return JS::js_undefined();
+        }
+
+        // 7. If response’s status is not an ok status, reject returnValue with a TypeError and abort these substeps.
+        if (!response_object.ok()) {
+            WebIDL::reject_promise(realm, return_value, *vm.throw_completion<JS::TypeError>("Response does not represent an ok status"sv).value());
+            return JS::js_undefined();
+        }
+
+        // 8. Consume response’s body as an ArrayBuffer, and let bodyPromise be the result.
+        auto body_promise_or_error = response_object.array_buffer();
+        if (body_promise_or_error.is_error()) {
+            auto throw_completion = Bindings::dom_exception_to_throw_completion(realm.vm(), body_promise_or_error.release_error());
+            WebIDL::reject_promise(realm, return_value, *throw_completion.value());
+            return JS::js_undefined();
+        }
+        auto body_promise = body_promise_or_error.release_value();
+
+        // 9. Upon fulfillment of bodyPromise with value bodyArrayBuffer:
+        auto body_fulfillment_steps = JS::create_heap_function(vm.heap(), [&vm, return_value](JS::Value body_array_buffer) -> WebIDL::ExceptionOr<JS::Value> {
+            // 1. Let stableBytes be a copy of the bytes held by the buffer bodyArrayBuffer.
+            VERIFY(body_array_buffer.is_object());
+            auto stable_bytes = WebIDL::get_buffer_source_copy(body_array_buffer.as_object());
+            if (stable_bytes.is_error()) {
+                VERIFY(stable_bytes.error().code() == ENOMEM);
+                WebIDL::reject_promise(HTML::relevant_realm(*return_value->promise()), return_value, *vm.throw_completion<JS::InternalError>(vm.error_message(JS::VM::ErrorMessage::OutOfMemory)).value());
+                return JS::js_undefined();
+            }
+
+            // 2. Asynchronously compile the WebAssembly module stableBytes using the networking task source and resolve returnValue with the result.
+            auto result = asynchronously_compile_webassembly_module(vm, stable_bytes.release_value(), HTML::Task::Source::Networking);
+
+            // Need to manually convert WebIDL promise to an ECMAScript value here to resolve
+            WebIDL::resolve_promise(HTML::relevant_realm(*return_value->promise()), return_value, result->promise());
+
+            return JS::js_undefined();
+        });
+
+        // 10. Upon rejection of bodyPromise with reason reason:
+        auto body_rejection_steps = JS::create_heap_function(vm.heap(), [return_value](JS::Value reason) -> WebIDL::ExceptionOr<JS::Value> {
+            // 1. Reject returnValue with reason.
+            WebIDL::reject_promise(HTML::relevant_realm(*return_value->promise()), return_value, reason);
+            return JS::js_undefined();
+        });
+
+        WebIDL::react_to_promise(body_promise, body_fulfillment_steps, body_rejection_steps);
+
+        return JS::js_undefined();
+    });
+
+    // 3. Upon rejection of source with reason reason:
+    auto rejection_steps = JS::create_heap_function(vm.heap(), [return_value](JS::Value reason) -> WebIDL::ExceptionOr<JS::Value> {
+        // 1. Reject returnValue with reason.
+        WebIDL::reject_promise(HTML::relevant_realm(*return_value->promise()), return_value, reason);
+
+        return JS::js_undefined();
+    });
+
+    WebIDL::react_to_promise(source, fulfillment_steps, rejection_steps);
+
+    // 4. Return returnValue.
+    return return_value;
+}
+
 }

+ 2 - 0
Userland/Libraries/LibWeb/WebAssembly/WebAssembly.h

@@ -23,9 +23,11 @@ void finalize(JS::Object&);
 
 bool validate(JS::VM&, JS::Handle<WebIDL::BufferSource>& bytes);
 WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> compile(JS::VM&, JS::Handle<WebIDL::BufferSource>& bytes);
+WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> compile_streaming(JS::VM&, JS::Handle<WebIDL::Promise> source);
 
 WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> instantiate(JS::VM&, JS::Handle<WebIDL::BufferSource>& bytes, Optional<JS::Handle<JS::Object>>& import_object);
 WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> instantiate(JS::VM&, Module const& module_object, Optional<JS::Handle<JS::Object>>& import_object);
+WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> instantiate_streaming(JS::VM&, JS::Handle<WebIDL::Promise> source, Optional<JS::Handle<JS::Object>>& import_object);
 
 namespace Detail {
 struct CompiledWebAssemblyModule : public RefCounted<CompiledWebAssemblyModule> {

+ 6 - 0
Userland/Libraries/LibWeb/WebAssembly/WebAssembly.idl

@@ -1,5 +1,6 @@
 #import <WebAssembly/Instance.idl>
 #import <WebAssembly/Module.idl>
+#import <Fetch/Response.idl>
 
 dictionary WebAssemblyInstantiatedSource {
     required Module module;
@@ -7,11 +8,16 @@ dictionary WebAssemblyInstantiatedSource {
 };
 
 // https://webassembly.github.io/spec/js-api/#webassembly-namespace
+// https://webassembly.github.io/spec/web-api/index.html#streaming-modules
 [Exposed=*, WithGCVisitor, WithFinalizer]
 namespace WebAssembly {
+    // FIXME: Streaming APIs are supposed to be only exposed to Window, Worker
+
     boolean validate(BufferSource bytes);
     Promise<Module> compile(BufferSource bytes);
+    Promise<Module> compileStreaming(Promise<Response> source);
 
     Promise<WebAssemblyInstantiatedSource> instantiate(BufferSource bytes, optional object importObject);
+    Promise<WebAssemblyInstantiatedSource> instantiateStreaming(Promise<Response> source, optional object importObject);
     Promise<Instance> instantiate(Module moduleObject, optional object importObject);
 };