diff options
-rw-r--r-- | src/ir/names.h | 4 | ||||
-rw-r--r-- | src/passes/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/passes/JSPI.cpp | 171 | ||||
-rw-r--r-- | src/passes/pass.cpp | 3 | ||||
-rw-r--r-- | src/passes/passes.h | 1 | ||||
-rw-r--r-- | src/wasm.h | 1 | ||||
-rw-r--r-- | src/wasm/wasm.cpp | 4 | ||||
-rw-r--r-- | test/lit/help/wasm-opt.test | 3 | ||||
-rw-r--r-- | test/lit/help/wasm2js.test | 3 | ||||
-rw-r--r-- | test/lit/passes/jspi.wast | 124 |
10 files changed, 315 insertions, 0 deletions
diff --git a/src/ir/names.h b/src/ir/names.h index af6ca1562..4bdfacb1e 100644 --- a/src/ir/names.h +++ b/src/ir/names.h @@ -83,6 +83,10 @@ inline Name getValidElementSegmentName(Module& module, Name root) { return getValidName( root, [&](Name test) { return !module.getElementSegmentOrNull(test); }); } +inline Name getValidLocalName(Function& func, Name root) { + return getValidName(root, + [&](Name test) { return !func.hasLocalIndex(test); }); +} class MinifiedNameGenerator { size_t state = 0; diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index eed7845bd..9379fbe27 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -47,6 +47,7 @@ set(passes_SOURCES InstrumentLocals.cpp InstrumentMemory.cpp Intrinsics.cpp + JSPI.cpp LegalizeJSInterface.cpp LimitSegments.cpp LocalCSE.cpp diff --git a/src/passes/JSPI.cpp b/src/passes/JSPI.cpp new file mode 100644 index 000000000..8ed2ba832 --- /dev/null +++ b/src/passes/JSPI.cpp @@ -0,0 +1,171 @@ +/* + * Copyright 2022 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "asmjs/shared-constants.h" +#include "ir/element-utils.h" +#include "ir/import-utils.h" +#include "ir/literal-utils.h" +#include "ir/names.h" +#include "ir/utils.h" +#include "pass.h" +#include "shared-constants.h" +#include "wasm-builder.h" +#include "wasm.h" +#include <utility> + +// +// Convert a module to be compatible with JavaScript promise integration (JSPI). +// All exports will be wrapped with a function that will handle storing +// the suspsender that is passed in as the first param from a "promising" +// `WebAssembly.Function`. All imports will also be wrapped, but they will take +// the stored suspender and pass it as the first param to the imported function +// that should be created from a "suspending" `WebAssembly.Function`. +// +namespace wasm { + +struct JSPI : public Pass { + + Type externref = Type(HeapType::ext, Nullable); + + void run(PassRunner* runner, Module* module) override { + Builder builder(*module); + // Create a global to store the suspender that is passed into exported + // functions and will then need to be passed out to the imported functions. + Name suspender = Names::getValidGlobalName(*module, "suspender"); + module->addGlobal(builder.makeGlobal( + suspender, externref, builder.makeRefNull(externref), Builder::Mutable)); + + // Keep track of already wrapped functions since they can be exported + // multiple times, but only one wrapper is needed. + std::unordered_map<Name, Name> wrappedExports; + + // Wrap each exported function in a function that stores the suspender + // and calls the original export. + for (auto& ex : module->exports) { + if (ex->kind == ExternalKind::Function) { + auto* func = module->getFunction(ex->value); + Name wrapperName; + auto iter = wrappedExports.find(func->name); + if (iter == wrappedExports.end()) { + wrapperName = makeWrapperForExport(func, module, suspender); + wrappedExports[func->name] = wrapperName; + } else { + wrapperName = iter->second; + } + ex->value = wrapperName; + } + } + + // Avoid iterator invalidation later. + std::vector<Function*> originalFunctions; + for (auto& func : module->functions) { + originalFunctions.push_back(func.get()); + } + // Wrap each imported function in a function that gets the global suspender + // and passes it on to the imported function. + for (auto* im : originalFunctions) { + if (im->imported()) { + makeWrapperForImport(im, module, suspender); + } + } + } + +private: + Name makeWrapperForExport(Function* func, Module* module, Name suspender) { + Name wrapperName = Names::getValidFunctionName( + *module, std::string("export$") + func->name.str); + + Builder builder(*module); + + auto* call = module->allocator.alloc<Call>(); + call->target = func->name; + call->type = func->getResults(); + + // Add an externref param as the first argument and copy all the original + // params to new export. + std::vector<Type> wrapperParams; + std::vector<NameType> namedWrapperParams; + wrapperParams.push_back(externref); + namedWrapperParams.emplace_back(Names::getValidLocalName(*func, "susp"), + externref); + Index i = 0; + for (const auto& param : func->getParams()) { + call->operands.push_back( + builder.makeLocalGet(wrapperParams.size(), param)); + wrapperParams.push_back(param); + namedWrapperParams.emplace_back(func->getLocalNameOrGeneric(i), param); + i++; + } + auto* block = builder.makeBlock(); + block->list.push_back( + builder.makeGlobalSet(suspender, builder.makeLocalGet(0, externref))); + block->list.push_back(call); + Type resultsType = func->getResults(); + if (resultsType == Type::none) { + // A void return is not currently allowed by v8. Add an i32 return value + // that is ignored. + // https://bugs.chromium.org/p/v8/issues/detail?id=13231 + resultsType = Type::i32; + block->list.push_back(builder.makeConst(0)); + } + block->finalize(); + auto wrapperFunc = + Builder::makeFunction(wrapperName, + std::move(namedWrapperParams), + Signature(Type(wrapperParams), resultsType), + {}, + block); + return module->addFunction(std::move(wrapperFunc))->name; + } + + void makeWrapperForImport(Function* im, Module* module, Name suspender) { + Builder builder(*module); + auto wrapperIm = make_unique<Function>(); + wrapperIm->name = Names::getValidFunctionName( + *module, std::string("import$") + im->name.str); + wrapperIm->module = im->module; + wrapperIm->base = im->base; + auto stub = make_unique<Function>(); + stub->name = Name(im->name.str); + stub->type = im->type; + + auto* call = module->allocator.alloc<Call>(); + call->target = wrapperIm->name; + + // Add an externref as the first argument to the imported function. + std::vector<Type> params; + params.push_back(externref); + call->operands.push_back(builder.makeGlobalGet(suspender, externref)); + Index i = 0; + for (const auto& param : im->getParams()) { + call->operands.push_back(builder.makeLocalGet(i, param)); + params.push_back(param); + ++i; + } + + call->type = im->getResults(); + stub->body = call; + wrapperIm->type = Signature(Type(params), call->type); + + module->removeFunction(im->name); + module->addFunction(std::move(stub)); + module->addFunction(std::move(wrapperIm)); + } +}; + +Pass* createJSPIPass() { return new JSPI(); } + +} // namespace wasm diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index f55f5018e..eaf20eb51 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -197,6 +197,9 @@ void PassRegistry::registerPasses() { registerPass("intrinsic-lowering", "lower away binaryen intrinsics", createIntrinsicLoweringPass); + registerPass("jspi", + "wrap imports and exports for JavaScript promise integration", + createJSPIPass); registerPass("legalize-js-interface", "legalizes i64 types on the import/export boundary", createLegalizeJSInterfacePass); diff --git a/src/passes/passes.h b/src/passes/passes.h index 4a1fbdce0..d665939ac 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -61,6 +61,7 @@ Pass* createI64ToI32LoweringPass(); Pass* createInlineMainPass(); Pass* createInliningPass(); Pass* createInliningOptimizingPass(); +Pass* createJSPIPass(); Pass* createLegalizeJSInterfacePass(); Pass* createLegalizeJSInterfaceMinimallyPass(); Pass* createLimitSegmentsPass(); diff --git a/src/wasm.h b/src/wasm.h index 7b895a0cf..eb5c15514 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -1971,6 +1971,7 @@ public: Name getLocalName(Index index); Index getLocalIndex(Name name); + bool hasLocalIndex(Name name) const; Index getVarIndexBase(); Type getLocalType(Index index); diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index 56db8d7a9..a445d74bf 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -1293,6 +1293,10 @@ Name Function::getLocalNameOrGeneric(Index index) { return Name::fromInt(index); } +bool Function::hasLocalIndex(Name name) const { + return localIndices.find(name) != localIndices.end(); +} + Index Function::getLocalIndex(Name name) { auto iter = localIndices.find(name); if (iter == localIndices.end()) { diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index b2ac0c7f5..9af51001d 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -206,6 +206,9 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --intrinsic-lowering lower away binaryen intrinsics ;; CHECK-NEXT: +;; CHECK-NEXT: --jspi wrap imports and exports for +;; CHECK-NEXT: JavaScript promise integration +;; CHECK-NEXT: ;; CHECK-NEXT: --legalize-js-interface legalizes i64 types on the ;; CHECK-NEXT: import/export boundary ;; CHECK-NEXT: diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index f70f7d6ef..ef32a87a1 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -165,6 +165,9 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --intrinsic-lowering lower away binaryen intrinsics ;; CHECK-NEXT: +;; CHECK-NEXT: --jspi wrap imports and exports for +;; CHECK-NEXT: JavaScript promise integration +;; CHECK-NEXT: ;; CHECK-NEXT: --legalize-js-interface legalizes i64 types on the ;; CHECK-NEXT: import/export boundary ;; CHECK-NEXT: diff --git a/test/lit/passes/jspi.wast b/test/lit/passes/jspi.wast new file mode 100644 index 000000000..052e72ad7 --- /dev/null +++ b/test/lit/passes/jspi.wast @@ -0,0 +1,124 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: wasm-opt %s --jspi -all -S -o - | filecheck %s + +(module + + ;; CHECK: (type $externref_f64_=>_i32 (func (param externref f64) (result i32))) + + ;; CHECK: (type $f64_=>_i32 (func (param f64) (result i32))) + + ;; CHECK: (type $externref_i32_=>_i32 (func (param externref i32) (result i32))) + + ;; CHECK: (type $f64_=>_none (func (param f64))) + + ;; CHECK: (type $i32_=>_i32 (func (param i32) (result i32))) + + ;; CHECK: (import "js" "compute_delta" (func $import$compute_delta (param externref f64) (result i32))) + (import "js" "compute_delta" (func $compute_delta (param f64) (result i32))) + ;; CHECK: (import "js" "import_and_export" (func $import$import_and_export (param externref i32) (result i32))) + (import "js" "import_and_export" (func $import_and_export (param i32) (result i32))) + ;; CHECK: (global $suspender (mut externref) (ref.null extern)) + + ;; CHECK: (export "update_state_void" (func $export$update_state_void)) + (export "update_state_void" (func $update_state_void)) + ;; CHECK: (export "update_state" (func $export$update_state)) + (export "update_state" (func $update_state)) + ;; Test duplicating an export. + ;; CHECK: (export "update_state_again" (func $export$update_state)) + (export "update_state_again" (func $update_state)) + ;; Test that a name collision on the parameters is handled. + ;; CHECK: (export "update_state_param_collision" (func $export$update_state_param_collision)) + (export "update_state_param_collision" (func $update_state_param_collision)) + ;; Test function that is imported and exported. + ;; CHECK: (export "import_and_export" (func $export$import_and_export)) + (export "import_and_export" (func $import_and_export)) + + + ;; CHECK: (func $update_state (param $param f64) (result i32) + ;; CHECK-NEXT: (call $compute_delta + ;; CHECK-NEXT: (f64.sub + ;; CHECK-NEXT: (f64.const 1.1) + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $update_state (param $param f64) (result i32) + (call $compute_delta (f64.sub (f64.const 1.1) (local.get $param))) + ) + + ;; CHECK: (func $update_state_void (param $0 f64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $compute_delta + ;; CHECK-NEXT: (f64.const 1.1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $update_state_void (param f64) + ;; This function doesn't return anything, but the JSPI pass should add a + ;; fake return value to make v8 happy. + (drop (call $compute_delta (f64.const 1.1))) + ) + + ;; CHECK: (func $update_state_param_collision (param $susp f64) (result i32) + ;; CHECK-NEXT: (call $update_state_param_collision + ;; CHECK-NEXT: (f64.sub + ;; CHECK-NEXT: (f64.const 1.1) + ;; CHECK-NEXT: (local.get $susp) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $update_state_param_collision (param $susp f64) (result i32) + (call $update_state_param_collision (f64.sub (f64.const 1.1) (local.get $susp))) + ) +) +;; CHECK: (func $export$update_state_void (param $susp externref) (param $0 f64) (result i32) +;; CHECK-NEXT: (global.set $suspender +;; CHECK-NEXT: (local.get $susp) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $update_state_void +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) + +;; CHECK: (func $export$update_state (param $susp externref) (param $param f64) (result i32) +;; CHECK-NEXT: (global.set $suspender +;; CHECK-NEXT: (local.get $susp) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $update_state +;; CHECK-NEXT: (local.get $param) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $export$update_state_param_collision (param $susp_0 externref) (param $susp f64) (result i32) +;; CHECK-NEXT: (global.set $suspender +;; CHECK-NEXT: (local.get $susp_0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $update_state_param_collision +;; CHECK-NEXT: (local.get $susp) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $export$import_and_export (param $susp externref) (param $0 i32) (result i32) +;; CHECK-NEXT: (global.set $suspender +;; CHECK-NEXT: (local.get $susp) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $import_and_export +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $compute_delta (param $0 f64) (result i32) +;; CHECK-NEXT: (call $import$compute_delta +;; CHECK-NEXT: (global.get $suspender) +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $import_and_export (param $0 i32) (result i32) +;; CHECK-NEXT: (call $import$import_and_export +;; CHECK-NEXT: (global.get $suspender) +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) |