ladybird/Libraries/LibWeb/WebAssembly/WebAssemblyModule.cpp
Glenn Skrzypczak e5dab9e1c7 LibWeb: Support WASM modules
This adds support for importing WASM modules in JavaScript and vice
versa.
2026-04-03 21:21:09 +02:00

525 lines
28 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2025, Glenn Skrzypczak <glenn.skrzypczak@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/ModuleEnvironment.h>
#include <LibJS/Runtime/ModuleRequest.h>
#include <LibWasm/AbstractMachine/AbstractMachine.h>
#include <LibWasm/AbstractMachine/Validator.h>
#include <LibWeb/WebAssembly/Global.h>
#include <LibWeb/WebAssembly/Instance.h>
#include <LibWeb/WebAssembly/Memory.h>
#include <LibWeb/WebAssembly/Module.h>
#include <LibWeb/WebAssembly/Table.h>
#include <LibWeb/WebAssembly/WebAssembly.h>
#include <LibWeb/WebAssembly/WebAssemblyModule.h>
namespace Web::WebAssembly {
GC_DEFINE_ALLOCATOR(WebAssemblyModule);
WebAssemblyModule::WebAssemblyModule(JS::Realm& realm, StringView filename, WebAssembly::Module& module_source,
JS::Script::HostDefined* host_defined, Vector<JS::ModuleRequest> requested_modules)
: CyclicModule(realm, filename, false, move(requested_modules), host_defined)
, m_module_source(module_source)
{
}
WebAssemblyModule::~WebAssemblyModule() = default;
void WebAssemblyModule::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_instance);
visitor.visit(m_module_source);
visitor.visit(m_module_record);
}
// https://webassembly.github.io/esm-integration/js-api/index.html#parse-a-webassembly-module
JS::ThrowCompletionOr<GC::Ref<WebAssemblyModule>> WebAssemblyModule::parse(ByteBuffer bytes, JS::Realm& realm, StringView filename, JS::Script::HostDefined* host_defined)
{
auto& vm = realm.vm();
// 1. Let stableBytes be a copy of the bytes held by the buffer bytes.
auto stable_bytes
= MUST(ByteBuffer::create_uninitialized(bytes.size()));
bytes.bytes().copy_to(stable_bytes);
// 2. Compile the WebAssembly module stableBytes and store the result as module.
// 3. If module is error, throw a CompileError exception.
// NOTE: When integrating with the JS String Builtins proposal, builtinSetNames should be passed in the following
// step as « "js-string" » and importedStringModule as null.
auto module = TRY(Detail::compile_a_webassembly_module(vm, stable_bytes));
// 4. Construct a WebAssembly module object from module and bytes, and let module be the result.
auto module_object = realm.create<WebAssembly::Module>(realm, module);
// 5. Let requestedModules be a set.
HashTable<ByteString> requested_modules;
// 6. For each (moduleName, name, type) in module_imports(module.[[Module]]),
auto const& imports = module_object->compiled_module()->module->import_section().imports();
for (auto const& entry : imports) {
// 1. If moduleName starts with the prefix "wasm-js:",
if (entry.module().starts_with("wasm-js:"sv)) {
// 1. Throw a LinkError exception.
return vm.throw_completion<LinkError>("Import with invalid module name"sv);
}
// 2. If name starts with the prefix "wasm:" or "wasm-js:",
if (entry.name().starts_with("wasm:"sv) || entry.name().starts_with("wasm-js:"sv)) {
// 1. Throw a LinkError exception.
return vm.throw_completion<LinkError>("Import with invalid name"sv);
}
// NOTE: The following step only applies when integrating with the JS String Builtins proposal.
// FIXME: 3. If Find a builtin with (moduleName, name, type) and builtins module.[[BuiltinSets]] is not null,
// then continue.
// 4. Append moduleName to requestedModules.
requested_modules.set(entry.module());
}
// 7. For each (name, type) in module_exports(module.[[Module]])
auto const& exports = module_object->compiled_module()->module->export_section().entries();
for (auto const& entry : exports) {
// 1. If name starts with the prefix "wasm:" or "wasm-js:",
if (entry.name().starts_with("wasm:"sv) || entry.name().starts_with("wasm-js:"sv)) {
// 1. Throw a LinkError exception.
return vm.throw_completion<LinkError>("Export with invalid name"sv);
}
}
// 8. Let moduleRecord be { [[Instance]]: ~empty~, [[Realm]]: realm, [[Environment]]: ~empty~,
// [[Namespace]]: ~empty~, [[ModuleSource]]: module, [[HostDefined]]: hostDefined,
// [[Status]]: "new", [[EvaluationError]]: undefined, [[DFSIndex]]: undefined,
// [[DFSAncestorIndex]]: undefined, [[RequestedModules]]: requestedModules,
// [[LoadedModules]]: « », [[CycleRoot]]: ~empty~, [[HasTLA]]: false,
// [[AsyncEvaluation]]: false, [[TopLevelCapability]]: ~empty~ [[AsyncParentModules]]: « »,
// [[PendingAsyncDependencies]]: ~empty~, }.
AK::Vector<JS::ModuleRequest> module_requests;
for (auto const& module_name : requested_modules) {
module_requests.append(JS::ModuleRequest { Utf16FlyString::from_utf8(module_name), {} });
}
auto module_record = realm.create<WebAssemblyModule>(realm, filename, module_object, host_defined, module_requests);
// 9. Set module.[[ModuleRecord]] to moduleRecord.
module_record->m_module_record = module_record;
// 10. Return moduleRecord.
return module_record;
}
// https://webassembly.github.io/esm-integration/js-api/index.html#export-name-list
Vector<Utf16FlyString> WebAssemblyModule::export_name_list()
{
// AD-HOC: Return cached export name list if available
if (m_cached_export_name_list.has_value())
return m_cached_export_name_list.value();
// 1. Let module be records [[ModuleSource]] internal slot.
auto module = m_module_source;
// 2. Let exports be an empty list.
Vector<Utf16FlyString> exports;
// 3. For each(name, type) in module_exports(module.[[Module]])
auto module_exports = module->compiled_module()->module->export_section().entries();
for (auto const& entry : module_exports) {
// 1. Append name to the end of exports.
exports.append(Utf16FlyString::from_utf8(entry.name()));
}
// AD-HOC: Cache exports
m_cached_export_name_list = exports;
// 4. Return exports.
return exports;
}
// https://webassembly.github.io/esm-integration/js-api/index.html#get-exported-names
Vector<Utf16FlyString> WebAssemblyModule::get_exported_names(JS::VM&, HashTable<Module const*>&)
{
// 1. Let record be this WebAssembly Module Record.
auto* record = this;
// 2. Return the export name list of record.
return record->export_name_list();
}
// https://webassembly.github.io/esm-integration/js-api/index.html#resolve-export
JS::ResolvedBinding WebAssemblyModule::resolve_export(JS::VM&, Utf16FlyString const& export_name, Vector<JS::ResolvedBinding>)
{
// 1. Let record be this WebAssembly Module Record.
auto* record = this;
// 2. If the export name list of record contains exportName, return { [[Module]]: record, [[BindingName]]: exportName }.
if (export_name_list().contains_slow(export_name)) {
return JS::ResolvedBinding { JS::ResolvedBinding::Type::BindingName, record, export_name };
}
// 3. Otherwise, return null.
return JS::ResolvedBinding::null();
}
// https://webassembly.github.io/esm-integration/js-api/index.html#module-declaration-environment-setup
JS::ThrowCompletionOr<void> WebAssemblyModule::initialize_environment(JS::VM& vm)
{
// 1. Let record be this WebAssembly Module Record.
auto* record = this;
// 2. Let env be NewModuleEnvironment(null).
auto env = vm.heap().allocate<JS::ModuleEnvironment>(nullptr);
// 3. Set record.[[Environment]] to env.
record->set_environment(env);
// 4. For each name in the export name list of record,
for (auto const& name : export_name_list()) {
// 1. Perform !env.CreateImmutableBinding(name, true).
MUST(env->create_immutable_binding(vm, name, true));
}
return {};
}
// https://webassembly.github.io/esm-integration/js-api/index.html#module-execution
JS::ThrowCompletionOr<void> WebAssemblyModule::execute_module(JS::VM& vm, GC::Ptr<JS::PromiseCapability> capability)
{
auto& cache = Detail::get_cache(*vm.current_realm());
// 1. Assert: promiseCapability was not provided.
VERIFY(!capability);
// 2. Let record be this WebAssembly Module Record.
auto* record = this;
// 3. Let module be record.[[ModuleSource]].[[Module]].
auto module = record->m_module_source->compiled_module();
// 4. Let imports be « ».
Vector<Wasm::ExternValue> imports;
// 5. For each (importedModuleName, name, importtype) in module_imports(module),
for (auto const& entry : module->module->import_section().imports()) {
// NOTE: The following step only applies when integrating with the JS String Builtins proposal.
// FIXME: 1. If Find a builtin with (importedModuleName, name) and builtins module.[[BuiltinSets]] is not null, then continue.
// 2. Let importedModule be GetImportedModule(record, importedModuleName).
auto imported_module = record->get_imported_module(JS::ModuleRequest { Utf16FlyString::from_utf8(entry.module()) });
// 3. Let resolution be importedModule.ResolveExport(name).
auto resolution = imported_module->resolve_export(vm, Utf16FlyString::from_utf8(entry.name()));
// 4. Assert: resolution is a ResolvedBinding Record, as validated during environment initialization.
VERIFY(resolution.is_valid());
// 5. Let resolvedModule be resolution.\[[Module]].
auto resolved_module = resolution.module;
// 6. Let resolvedName be resolution.[[BindingName]].
auto resolved_name = resolution.export_name;
// 7. If resolvedModule is a WebAssembly Module Record,
if (is<WebAssemblyModule>(*resolved_module)) {
auto& resolved_webassembly_module = as<WebAssemblyModule>(*resolved_module);
// 1. If resolvedModule.[[Instance]] is ~empty~, throw a {LinkError} exception.
if (!resolved_webassembly_module.m_instance) {
return vm.throw_completion<LinkError>("Module has not been instantiated"sv);
}
// 2. Assert: resolvedModule.[[Instance]] is a WebAssembly Instance object.
// 3. Assert: resolvedModule.[[ModuleSource]] is a WebAssembly Module object.
// 4. Let module be resolvedModule.[[ModuleSource]].[[Module]].
auto module = resolved_webassembly_module.m_module_source->compiled_module();
// 5. Let externval be instance_export(resolvedModule.[[Instance]], resolvedName).
// https://webassembly.github.io/spec/core/appendix/embedding.html#embed-instance-export
auto externval = resolved_webassembly_module.m_instance->module_instance()->exports().first_matching([resolved_name](auto const& export_instance) { return export_instance.name() == resolved_name; });
// 6. Assert: externval is not error.
VERIFY(externval.has_value());
// 7. Assert: module_exports(module) contains an element (resolvedName, type).
auto module_export = module->module->export_section().entries().first_matching([resolved_name](auto& element) { return element.name() == resolved_name; });
VERIFY(module_export.has_value());
// 8. Let externtype be the value of type for the element (resolvedName, type) in module_exports(module).
auto externtype = module_export->description();
// 9. If importtype is not an extern subtype of externtype, throw a LinkError exception.
// https://webassembly.github.io/spec/core/valid/types.html#match-externtype
auto& store = cache.abstract_machine().store();
auto invalid = entry.description().visit(
[&](Wasm::MemoryType const& mem_type) -> Optional<ByteString> {
if (!externtype.has<Wasm::MemoryIndex>())
return "Expected memory import"sv;
auto other_mem_type = store.get(Wasm::MemoryAddress { externtype.get<Wasm::MemoryIndex>().value() })->type();
if (other_mem_type.limits().is_subset_of(mem_type.limits()))
return {};
return ByteString::formatted("Memory import and extern do not match: {}-{} vs {}-{}", mem_type.limits().min(), mem_type.limits().max(), other_mem_type.limits().min(), other_mem_type.limits().max());
},
[&](Wasm::TableType const& table_type) -> Optional<ByteString> {
if (!externtype.has<Wasm::TableIndex>())
return "Expected table import"sv;
auto other_table_type = store.get(Wasm::TableAddress { externtype.get<Wasm::TableIndex>().value() })->type();
if (table_type.element_type() == other_table_type.element_type()
&& other_table_type.limits().is_subset_of(table_type.limits()))
return {};
return ByteString::formatted("Table import and extern do not match: {}-{} vs {}-{}", table_type.limits().min(), table_type.limits().max(), other_table_type.limits().min(), other_table_type.limits().max());
},
[&](Wasm::GlobalType const& global_type) -> Optional<ByteString> {
if (!externtype.has<Wasm::GlobalIndex>())
return "Expected global import"sv;
auto other_global_type = store.get(Wasm::GlobalAddress { externtype.get<Wasm::GlobalIndex>().value() })->type();
if (global_type.type() == other_global_type.type()
&& global_type.is_mutable() == other_global_type.is_mutable())
return {};
return "Global import and extern do not match"sv;
},
[&](Wasm::FunctionType const& type) -> Optional<ByteString> {
if (!externtype.has<Wasm::FunctionIndex>())
return "Expected function import"sv;
auto other_type = store.get(Wasm::FunctionAddress { externtype.get<Wasm::FunctionIndex>().value() })->visit([&](Wasm::WasmFunction const& wasm_func) { return wasm_func.type(); }, [&](Wasm::HostFunction const& host_func) { return host_func.type(); });
if (type.results() != other_type.results())
return ByteString::formatted("Function import and extern do not match, results: {} vs {}", type.results(), other_type.results());
if (type.parameters() != other_type.parameters())
return ByteString::formatted("Function import and extern do not match, parameters: {} vs {}", type.parameters(), other_type.parameters());
return {};
},
[&](Wasm::TagType const& type) -> Optional<ByteString> {
if (!externtype.has<Wasm::TagIndex>())
return "Expected tag import"sv;
auto* other_tag_instance = store.get(Wasm::TagAddress { externtype.get<Wasm::TagIndex>().value() });
if (other_tag_instance->flags() != type.flags())
return "Tag import and extern do not match"sv;
auto const& this_type = module->module->type_section().types()[type.type().value()];
if (other_tag_instance->type().parameters() != this_type.function().parameters())
return "Tag import and extern do not match"sv;
return {};
},
[&](Wasm::TypeIndex type_index) -> Optional<ByteString> {
if (!externtype.has<Wasm::FunctionIndex>())
return "Expected function import"sv;
auto other_type = store.get(Wasm::FunctionAddress { externtype.get<Wasm::FunctionIndex>().value() })->visit([&](Wasm::WasmFunction const& wasm_func) { return wasm_func.type(); }, [&](Wasm::HostFunction const& host_func) { return host_func.type(); });
auto const& type = module->module->type_section().types()[type_index.value()].function();
if (type.results() != other_type.results())
return ByteString::formatted("Function import and extern do not match, results: {} vs {}", type.results(), other_type.results());
if (type.parameters() != other_type.parameters())
return ByteString::formatted("Function import and extern do not match, parameters: {} vs {}", type.parameters(), other_type.parameters());
return {};
});
if (invalid.has_value())
return vm.throw_completion<LinkError>(ByteString::formatted("{}::{}: {}", entry.module(), entry.name(), invalid.release_value()));
// 10. Append externval to imports.
imports.append(externval.value().value());
}
// 8. Otherwise,
else {
// 1. Let env be resolvedModule.[[Environment]].
auto env = resolved_module->environment();
// 2. Let v be ?env.GetBindingValue(resolvedName, true).
auto v = TRY(env->get_binding_value(vm, resolved_name, true));
// 3. If importtype is of the form func functype,
// AD-HOC: Resolve type index
if (entry.description().has<Wasm::FunctionType>() || entry.description().has<Wasm::TypeIndex>()) {
auto functype = entry.description().visit(
[](Wasm::FunctionType function_type) { return function_type; },
[&module](Wasm::TypeIndex type_index) { return module->module->type_section().types()[type_index.value()].function(); },
[](auto) -> Wasm::FunctionType { VERIFY_NOT_REACHED(); });
// 1. If IsCallable(v) is false, throw a LinkError exception.
if (!v.is_function())
return vm.throw_completion<LinkError>(JS::ErrorType::NotAFunction, v);
auto& function = v.as_function();
// 2. If v has a [[FunctionAddress]] internal slot, and therefore is an Exported Function,
Optional<Wasm::FunctionAddress> funcaddr;
if (is<Detail::ExportedWasmFunction>(function)) {
// 1. Let funcaddr be the value of vs [[FunctionAddress]] internal slot.
auto& exported_function = static_cast<Detail::ExportedWasmFunction&>(function);
funcaddr = exported_function.exported_address();
}
// 3. Otherwise,
else {
// 1. Create a host function from v and functype, and let funcaddr be the result.
cache.add_imported_object(function);
auto host_function = Detail::create_host_function(vm, function, functype, ByteString::formatted("func{}", imports.size()));
funcaddr = cache.abstract_machine().store().allocate(move(host_function));
// FIXME: 2. Let index be the number of external functions in imports, defining the index of the host function funcaddr.
}
// 4. Let externfunc be the external value func funcaddr.
Wasm::ExternValue externfunc { Wasm::FunctionAddress { *funcaddr } };
// 5. Append externfunc to imports.
imports.append(externfunc);
}
// 4. If importtype is of the form global mut valtype,
if (entry.description().has<Wasm::GlobalType>()) {
auto valtype = entry.description().get<Wasm::GlobalType>();
// 1. Let store be the surrounding agents associated store.
auto& store = cache.abstract_machine().store();
// 2. If v implements Global,
Optional<Wasm::GlobalAddress> globaladdr;
if (v.is_object() && is<Global>(v.as_object())) {
// 1. Let globaladdr be v.[[Global]].
globaladdr = as<Global>(v.as_object()).address();
// 2. Let targetmut valuetype be global_type(store, globaladdr).
auto* valuetype = store.get(*globaladdr);
// 3. If mut is const and targetmut is var, throw a LinkError exception.
if (!valtype.is_mutable() && valuetype->is_mutable()) {
return vm.throw_completion<LinkError>("Mutable globals are not supported for immutable imports"sv);
}
}
// 3. Otherwise,
else {
// AD-HOC: If valtype is i64 and v is a Number, throw a LinkError exception.
if (valtype.type().kind() == Wasm::ValueType::I64 && v.is_number()) {
return vm.throw_completion<LinkError>("Import resolution attempted to cast a Number to a BigInteger"sv);
}
// AD-HOC: If valtype is not i64 and v is a BigInt, throw a LinkError exception.
if (valtype.type().kind() != Wasm::ValueType::I64 && v.is_bigint()) {
return vm.throw_completion<LinkError>("Import resolution attempted to cast a BigInteger to a Number"sv);
}
// 1. If valtype is v128, throw a LinkError exception.
if (valtype.type().kind() == Wasm::ValueType::V128) {
return vm.throw_completion<LinkError>("V128 is not supported as a global value type"sv);
}
// 2. If mut is var, throw a LinkError exception.
if (valtype.is_mutable()) {
return vm.throw_completion<LinkError>("Variable global value types are not supported"sv);
}
// 3. Let value be ?ToWebAssemblyValue(v, valtype).
auto value = TRY(Detail::to_webassembly_value(vm, v, valtype.type()));
// 4. Let(store, globaladdr) be global_alloc(store, mut valtype, value).
// 5. Set the surrounding agents associated store to store.
globaladdr = cache.abstract_machine().store().allocate(valtype, value);
}
// 4. Let externglobal be global globaladdr.
Wasm::ExternValue externglobal { Wasm::GlobalAddress { *globaladdr } };
// 5. Append externglobal to imports.
imports.append(externglobal);
}
// 5. If importtype is of the form mem memtype,
if (entry.description().has<Wasm::MemoryType>()) {
// 1. If v does not implement Memory, throw a LinkError exception.
if (!v.is_object() || !is<WebAssembly::Memory>(v.as_object())) {
return vm.throw_completion<LinkError>("Expected an instance of WebAssembly.Memory for a memory import"sv);
}
// 2. Let externmem be the external value mem v.[[Memory]].
auto externmem = static_cast<WebAssembly::Memory const&>(v.as_object()).address();
// 3. Append externmem to imports.
imports.append(externmem);
}
// 6. If importtype is of the form table tabletype,
if (entry.description().has<Wasm::TableType>()) {
// 1. If v does not implement Table, throw a LinkError exception.
if (!v.is_object() || !is<WebAssembly::Table>(v.as_object())) {
return vm.throw_completion<LinkError>("Expected an instance of WebAssembly.Table for a table import"sv);
}
// 2. Let tableaddr be v.[[Table]].
auto tableaddr = static_cast<WebAssembly::Table const&>(v.as_object()).address();
// 3. Let externtable be the external value table tableaddr.
Wasm::ExternValue externtable { tableaddr };
// 4. Append externtable to imports.
imports.append(externtable);
}
}
}
// 6. Instantiate the core of a WebAssembly module module with imports, and let instance be the result.
// https://webassembly.github.io/spec/js-api/index.html#instantiate-the-core-of-a-webassembly-module
auto instantiation_result = cache.abstract_machine().instantiate(module->module, imports);
if (instantiation_result.is_error()) {
auto instantiation_error = instantiation_result.release_error();
switch (instantiation_error.source) {
case Wasm::InstantiationErrorSource::Linking:
return vm.throw_completion<LinkError>(instantiation_error.error);
case Wasm::InstantiationErrorSource::StartFunction:
return vm.throw_completion<RuntimeError>(instantiation_error.error);
}
VERIFY_NOT_REACHED();
}
// 7. Set record.[[Instance]] to instance.
record->m_instance = vm.heap().allocate<Instance>(*vm.current_realm(), instantiation_result.release_value());
// 8. For each (name, externtype) of module_exports(module),
for (auto const& entry : module->module->export_section().entries()) {
// 1. If externtype is of the form global mut globaltype,
if (entry.description().has<Wasm::GlobalIndex>()) {
// 1. Assert: externval is of the form global globaladdr.
// 2. Let global globaladdr be externval.
// 3. Let global_value be global_read(store, globaladdr).
auto globaladdr = Wasm::GlobalAddress { entry.description().get<Wasm::GlobalIndex>().value() };
auto* global_value = cache.abstract_machine().store().get(globaladdr);
VERIFY(global_value);
// 4. If globaltype is not v128,
auto type = global_value->type();
if (type.type().kind() != Wasm::ValueType::Kind::V128) {
// NOTE: The condition above leaves unsupported JS values as uninitialized in TDZ and therefore as a
// reference error on access. When integrating with shared globals, they may be excluded here
// similarly to v128 above.
// 1. Perform !record.[[Environment]].InitializeBinding(name, ToJSValue(global_value)).
auto value = global_value->value();
MUST(record->environment()->initialize_binding(vm, Utf16FlyString::from_utf8(entry.name()), Detail::to_js_value(vm, value, type.type()), JS::Environment::InitializeBindingHint::Normal));
// FIXME: 2. If mut is var, then associate all future mutations of globaladdr with the ECMA-262 binding record
// for name in record.[[Environment]], such that record.[[Environment]].GetBindingValue(resolution.[[BindingName]], true)
// always returns ToJSValue(global_read(store, globaladdr)) for the current surrounding agents associated store store.
}
}
// 2. Otherwise,
else {
// 1. Perform !record.[[Environment]].InitializeBinding(name, !Get(instance.[[Exports]], name)).
auto name = Utf16FlyString::from_utf8(entry.name());
MUST(record->environment()->initialize_binding(vm, name, MUST(record->m_instance->get(JS::PropertyKey { name })), JS::Environment::InitializeBindingHint::Normal));
}
}
// NOTE: The linking semantics here for Wasm to Wasm modules are identical to the WebAssembly JS API semantics as if
// passing the the exports object as the imports object in instantiation. When linking Wasm module imports to
// JS module exports, the JS API semantics are exactly followed as well. It is only in the case of importing
// Wasm from JS that WebAssembly.Global unwrapping is observable on the WebAssembly Module Record Environment
// Record.
return {};
}
}