/* * Copyright (c) 2021, Ali Mohammad Pur * * SPDX-License-Identifier: BSD-2-Clause */ #include "WebAssemblyInstanceObject.h" #include "WebAssemblyMemoryPrototype.h" #include "WebAssemblyModuleConstructor.h" #include "WebAssemblyModuleObject.h" #include "WebAssemblyModulePrototype.h" #include "WebAssemblyTableObject.h" #include "WebAssemblyTablePrototype.h" #include #include #include #include #include #include #include #include #include #include namespace Web::Bindings { WebAssemblyObject::WebAssemblyObject(JS::GlobalObject& global_object) : Object(*global_object.object_prototype()) { s_abstract_machine.enable_instruction_count_limit(); } void WebAssemblyObject::initialize(JS::GlobalObject& global_object) { Object::initialize(global_object); u8 attr = JS::Attribute::Configurable | JS::Attribute::Writable | JS::Attribute::Enumerable; define_old_native_function("validate", validate, 1, attr); define_old_native_function("compile", compile, 1, attr); define_old_native_function("instantiate", instantiate, 1, attr); auto& vm = global_object.vm(); auto& window = static_cast(global_object); auto& memory_constructor = window.ensure_web_constructor("WebAssembly.Memory"); memory_constructor.define_direct_property(vm.names.name, js_string(vm, "WebAssembly.Memory"), JS::Attribute::Configurable); auto& memory_prototype = window.ensure_web_prototype("WebAssemblyMemoryPrototype"); memory_prototype.define_direct_property(vm.names.constructor, &memory_constructor, JS::Attribute::Writable | JS::Attribute::Configurable); define_direct_property("Memory", &memory_constructor, JS::Attribute::Writable | JS::Attribute::Configurable); auto& instance_constructor = window.ensure_web_constructor("WebAssembly.Instance"); instance_constructor.define_direct_property(vm.names.name, js_string(vm, "WebAssembly.Instance"), JS::Attribute::Configurable); auto& instance_prototype = window.ensure_web_prototype("WebAssemblyInstancePrototype"); instance_prototype.define_direct_property(vm.names.constructor, &instance_constructor, JS::Attribute::Writable | JS::Attribute::Configurable); define_direct_property("Instance", &instance_constructor, JS::Attribute::Writable | JS::Attribute::Configurable); auto& module_constructor = window.ensure_web_constructor("WebAssembly.Module"); module_constructor.define_direct_property(vm.names.name, js_string(vm, "WebAssembly.Module"), JS::Attribute::Configurable); auto& module_prototype = window.ensure_web_prototype("WebAssemblyModulePrototype"); module_prototype.define_direct_property(vm.names.constructor, &module_constructor, JS::Attribute::Writable | JS::Attribute::Configurable); define_direct_property("Module", &module_constructor, JS::Attribute::Writable | JS::Attribute::Configurable); auto& table_constructor = window.ensure_web_constructor("WebAssembly.Table"); table_constructor.define_direct_property(vm.names.name, js_string(vm, "WebAssembly.Table"), JS::Attribute::Configurable); auto& table_prototype = window.ensure_web_prototype("WebAssemblyTablePrototype"); table_prototype.define_direct_property(vm.names.constructor, &table_constructor, JS::Attribute::Writable | JS::Attribute::Configurable); define_direct_property("Table", &table_constructor, JS::Attribute::Writable | JS::Attribute::Configurable); } NonnullOwnPtrVector WebAssemblyObject::s_compiled_modules; NonnullOwnPtrVector WebAssemblyObject::s_instantiated_modules; Vector WebAssemblyObject::s_module_caches; WebAssemblyObject::GlobalModuleCache WebAssemblyObject::s_global_cache; Wasm::AbstractMachine WebAssemblyObject::s_abstract_machine; void WebAssemblyObject::visit_edges(Visitor& visitor) { Base::visit_edges(visitor); for (auto& entry : s_global_cache.function_instances) visitor.visit(entry.value); for (auto& module_cache : s_module_caches) { for (auto& entry : module_cache.function_instances) visitor.visit(entry.value); for (auto& entry : module_cache.memory_instances) visitor.visit(entry.value); } } JS_DEFINE_OLD_NATIVE_FUNCTION(WebAssemblyObject::validate) { // FIXME: Implement this once module validation is implemented in LibWasm. dbgln("Hit WebAssemblyObject::validate() stub!"); return JS::Value { true }; } Result parse_module(JS::GlobalObject& global_object, JS::Object* buffer_object) { ReadonlyBytes data; if (is(buffer_object)) { auto& buffer = static_cast(*buffer_object); data = buffer.buffer(); } else if (is(buffer_object)) { auto& buffer = static_cast(*buffer_object); data = buffer.viewed_array_buffer()->buffer().span().slice(buffer.byte_offset(), buffer.byte_length()); } else if (is(buffer_object)) { auto& buffer = static_cast(*buffer_object); data = buffer.viewed_array_buffer()->buffer().span().slice(buffer.byte_offset(), buffer.byte_length()); } else { auto error = JS::TypeError::create(global_object, "Not a BufferSource"); return JS::Value { error }; } InputMemoryStream stream { data }; auto module_result = Wasm::Module::parse(stream); ScopeGuard drain_errors { [&] { stream.handle_any_error(); } }; if (module_result.is_error()) { // FIXME: Throw CompileError instead. auto error = JS::TypeError::create(global_object, Wasm::parse_error_to_string(module_result.error())); return JS::Value { error }; } WebAssemblyObject::s_compiled_modules.append(make(module_result.release_value())); return WebAssemblyObject::s_compiled_modules.size() - 1; } JS_DEFINE_OLD_NATIVE_FUNCTION(WebAssemblyObject::compile) { // FIXME: This shouldn't block! auto buffer_or_error = vm.argument(0).to_object(global_object); JS::Value rejection_value; if (buffer_or_error.is_error()) { rejection_value = buffer_or_error.throw_completion().value(); vm.clear_exception(); vm.stop_unwind(); } auto promise = JS::Promise::create(global_object); if (!rejection_value.is_empty()) { promise->reject(rejection_value); return promise; } auto* buffer = buffer_or_error.release_value(); auto result = parse_module(global_object, buffer); if (result.is_error()) promise->reject(result.error()); else promise->fulfill(vm.heap().allocate(global_object, global_object, result.value())); return promise; } Result WebAssemblyObject::instantiate_module(Wasm::Module const& module, JS::VM& vm, JS::GlobalObject& global_object) { Wasm::Linker linker { module }; HashMap resolved_imports; auto import_argument = vm.argument(1); if (!import_argument.is_undefined()) { auto import_object_or_error = import_argument.to_object(global_object); if (import_object_or_error.is_error()) { vm.clear_exception(); vm.stop_unwind(); return import_object_or_error.throw_completion().value(); } [[maybe_unused]] auto* import_object = import_object_or_error.release_value(); dbgln("Trying to resolve stuff because import object was specified"); for (const Wasm::Linker::Name& import_name : linker.unresolved_imports()) { dbgln("Trying to resolve {}::{}", import_name.module, import_name.name); auto value_or_error = import_object->get(import_name.module); if (value_or_error.is_error()) break; auto value = value_or_error.release_value(); auto object_or_error = value.to_object(global_object); if (object_or_error.is_error()) break; auto* object = object_or_error.release_value(); auto import_or_error = object->get(import_name.name); if (import_or_error.is_error()) break; auto import_ = import_or_error.release_value(); import_name.type.visit( [&](Wasm::TypeIndex index) { dbgln("Trying to resolve a function {}::{}, type index {}", import_name.module, import_name.name, index.value()); auto& type = module.type(index); // FIXME: IsCallable() if (!import_.is_function()) return; auto& function = import_.as_function(); // FIXME: If this is a function created by create_native_function(), // just extract its address and resolve to that. Wasm::HostFunction host_function { [&](auto&, auto& arguments) -> Wasm::Result { JS::MarkedValueList argument_values { vm.heap() }; for (auto& entry : arguments) argument_values.append(to_js_value(entry, global_object)); auto result_or_error = vm.call(function, JS::js_undefined(), move(argument_values)); if (result_or_error.is_error()) { vm.clear_exception(); return Wasm::Trap(); } if (type.results().is_empty()) return Wasm::Result { Vector {} }; if (type.results().size() == 1) { auto value = to_webassembly_value(result_or_error.release_value(), type.results().first(), global_object); if (!value.has_value()) return Wasm::Trap {}; return Wasm::Result { Vector { value.release_value() } }; } // FIXME: Multiple returns TODO(); }, type }; auto address = s_abstract_machine.store().allocate(move(host_function)); dbgln("Resolved to {}", address->value()); // FIXME: LinkError instead. VERIFY(address.has_value()); resolved_imports.set(import_name, Wasm::ExternValue { Wasm::FunctionAddress { *address } }); }, [&](Wasm::GlobalType const& type) { Optional address; // https://webassembly.github.io/spec/js-api/#read-the-imports step 5.1 if (import_.is_number() || import_.is_bigint()) { if (import_.is_number() && type.type().kind() == Wasm::ValueType::I64) { // FIXME: Throw a LinkError instead. vm.throw_exception(global_object, "LinkError: Import resolution attempted to cast a Number to a BigInteger"); return; } if (import_.is_bigint() && type.type().kind() != Wasm::ValueType::I64) { // FIXME: Throw a LinkError instead. vm.throw_exception(global_object, "LinkError: Import resolution attempted to cast a BigInteger to a Number"); return; } auto cast_value = to_webassembly_value(import_, type.type(), global_object); if (!cast_value.has_value()) return; address = s_abstract_machine.store().allocate({ type.type(), false }, cast_value.release_value()); } else { // FIXME: https://webassembly.github.io/spec/js-api/#read-the-imports step 5.2 // if v implements Global // let globaladdr be v.[[Global]] // FIXME: Throw a LinkError instead vm.throw_exception(global_object, "LinkError: Invalid value for global type"); return; } resolved_imports.set(import_name, Wasm::ExternValue { *address }); }, [&](Wasm::MemoryType const&) { if (!import_.is_object() || !is(import_.as_object())) { // FIXME: Throw a LinkError instead vm.throw_exception(global_object, "LinkError: Expected an instance of WebAssembly.Memory for a memory import"); return; } auto address = static_cast(import_.as_object()).address(); resolved_imports.set(import_name, Wasm::ExternValue { address }); }, [&](Wasm::TableType const&) { if (!import_.is_object() || !is(import_.as_object())) { // FIXME: Throw a LinkError instead vm.throw_exception(global_object, "LinkError: Expected an instance of WebAssembly.Table for a table import"); return; } auto address = static_cast(import_.as_object()).address(); resolved_imports.set(import_name, Wasm::ExternValue { address }); }, [&](const auto&) { // FIXME: Implement these. dbgln("Unimplemented import of non-function attempted"); vm.throw_exception(global_object, "LinkError: Not Implemented"); }); if (vm.exception()) break; } if (auto exception = vm.exception()) { vm.clear_exception(); return exception->value(); } } linker.link(resolved_imports); auto link_result = linker.finish(); if (link_result.is_error()) { // FIXME: Throw a LinkError. StringBuilder builder; builder.append("LinkError: Missing "); builder.join(' ', link_result.error().missing_imports); return JS::Value(JS::TypeError::create(global_object, builder.build())); } auto instance_result = s_abstract_machine.instantiate(module, link_result.release_value()); if (instance_result.is_error()) { // FIXME: Throw a LinkError instead. return JS::Value(JS::TypeError::create(global_object, instance_result.error().error)); } s_instantiated_modules.append(instance_result.release_value()); s_module_caches.empend(); return s_instantiated_modules.size() - 1; } JS_DEFINE_OLD_NATIVE_FUNCTION(WebAssemblyObject::instantiate) { // FIXME: This shouldn't block! auto buffer_or_error = vm.argument(0).to_object(global_object); auto promise = JS::Promise::create(global_object); bool should_return_module = false; if (buffer_or_error.is_error()) { auto rejection_value = buffer_or_error.throw_completion().value(); vm.clear_exception(); vm.stop_unwind(); promise->reject(rejection_value); return promise; } auto* buffer = buffer_or_error.release_value(); const Wasm::Module* module { nullptr }; if (is(buffer) || is(buffer)) { auto result = parse_module(global_object, buffer); if (result.is_error()) { promise->reject(result.error()); return promise; } module = &WebAssemblyObject::s_compiled_modules.at(result.value()).module; should_return_module = true; } else if (is(buffer)) { module = &static_cast(buffer)->module(); } else { auto error = JS::TypeError::create(global_object, String::formatted("{} is not an ArrayBuffer or a Module", buffer->class_name())); promise->reject(error); return promise; } VERIFY(module); auto result = instantiate_module(*module, vm, global_object); if (result.is_error()) { promise->reject(result.release_error()); } else { auto instance_object = vm.heap().allocate(global_object, global_object, result.value()); if (should_return_module) { auto object = JS::Object::create(global_object, nullptr); object->define_direct_property("module", vm.heap().allocate(global_object, global_object, s_compiled_modules.size() - 1), JS::default_attributes); object->define_direct_property("instance", instance_object, JS::default_attributes); promise->fulfill(object); } else { promise->fulfill(instance_object); } } return promise; } JS::Value to_js_value(Wasm::Value& wasm_value, JS::GlobalObject& global_object) { switch (wasm_value.type().kind()) { case Wasm::ValueType::I64: return global_object.heap().allocate(global_object, ::Crypto::SignedBigInteger::create_from(wasm_value.to().value())); case Wasm::ValueType::I32: return JS::Value(wasm_value.to().value()); case Wasm::ValueType::F64: return JS::Value(wasm_value.to().value()); case Wasm::ValueType::F32: return JS::Value(static_cast(wasm_value.to().value())); case Wasm::ValueType::FunctionReference: // FIXME: What's the name of a function reference that isn't exported? return create_native_function(wasm_value.to().value().address, "FIXME_IHaveNoIdeaWhatThisShouldBeCalled", global_object); case Wasm::ValueType::NullFunctionReference: return JS::js_null(); case Wasm::ValueType::ExternReference: case Wasm::ValueType::NullExternReference: TODO(); } VERIFY_NOT_REACHED(); } Optional to_webassembly_value(JS::Value value, const Wasm::ValueType& type, JS::GlobalObject& global_object) { static ::Crypto::SignedBigInteger two_64 = "1"_sbigint.shift_left(64); auto& vm = global_object.vm(); switch (type.kind()) { case Wasm::ValueType::I64: { auto bigint = TRY_OR_DISCARD(value.to_bigint(global_object)); auto value = bigint->big_integer().divided_by(two_64).remainder; VERIFY(value.unsigned_value().trimmed_length() <= 2); i64 integer = static_cast(value.unsigned_value().to_u64()); if (value.is_negative()) integer = -integer; return Wasm::Value { integer }; } case Wasm::ValueType::I32: { auto _i32 = TRY_OR_DISCARD(value.to_i32(global_object)); return Wasm::Value { static_cast(_i32) }; } case Wasm::ValueType::F64: { auto number = TRY_OR_DISCARD(value.to_double(global_object)); return Wasm::Value { static_cast(number) }; } case Wasm::ValueType::F32: { auto number = TRY_OR_DISCARD(value.to_double(global_object)); return Wasm::Value { static_cast(number) }; } case Wasm::ValueType::FunctionReference: case Wasm::ValueType::NullFunctionReference: { if (value.is_null()) return Wasm::Value { Wasm::ValueType(Wasm::ValueType::NullExternReference), 0ull }; if (value.is_function()) { auto& function = value.as_function(); for (auto& entry : WebAssemblyObject::s_global_cache.function_instances) { if (entry.value == &function) return Wasm::Value { Wasm::Reference { Wasm::Reference::Func { entry.key } } }; } } vm.throw_exception(global_object, JS::ErrorType::NotAnObjectOfType, "Exported function"); return {}; } case Wasm::ValueType::ExternReference: case Wasm::ValueType::NullExternReference: TODO(); } VERIFY_NOT_REACHED(); } JS::NativeFunction* create_native_function(Wasm::FunctionAddress address, String name, JS::GlobalObject& global_object) { Optional type; WebAssemblyObject::s_abstract_machine.store().get(address)->visit([&](const auto& value) { type = value.type(); }); if (auto entry = WebAssemblyObject::s_global_cache.function_instances.get(address); entry.has_value()) return *entry; auto function = JS::NativeFunction::create( global_object, name, [address, type = type.release_value()](JS::VM& vm, JS::GlobalObject& global_object) -> JS::ThrowCompletionOr { Vector values; values.ensure_capacity(type.parameters().size()); // Grab as many values as needed and convert them. size_t index = 0; for (auto& type : type.parameters()) { auto result = to_webassembly_value(vm.argument(index++), type, global_object); if (result.has_value()) values.append(result.release_value()); else return JS::throw_completion(vm.exception()->value()); } auto result = WebAssemblyObject::s_abstract_machine.invoke(address, move(values)); // FIXME: Use the convoluted mapping of errors defined in the spec. if (result.is_trap()) return vm.throw_completion(global_object, String::formatted("Wasm execution trapped (WIP): {}", result.trap().reason)); if (result.values().is_empty()) return JS::js_undefined(); if (result.values().size() == 1) return to_js_value(result.values().first(), global_object); Vector result_values; for (auto& entry : result.values()) result_values.append(to_js_value(entry, global_object)); return JS::Value(JS::Array::create_from(global_object, result_values)); }); WebAssemblyObject::s_global_cache.function_instances.set(address, function); return function; } WebAssemblyMemoryObject::WebAssemblyMemoryObject(JS::GlobalObject& global_object, Wasm::MemoryAddress address) : Object(static_cast(global_object).ensure_web_prototype("WebAssemblyMemoryPrototype")) , m_address(address) { } }