summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/ir/names.h4
-rw-r--r--src/passes/CMakeLists.txt1
-rw-r--r--src/passes/JSPI.cpp171
-rw-r--r--src/passes/pass.cpp3
-rw-r--r--src/passes/passes.h1
-rw-r--r--src/wasm.h1
-rw-r--r--src/wasm/wasm.cpp4
-rw-r--r--test/lit/help/wasm-opt.test3
-rw-r--r--test/lit/help/wasm2js.test3
-rw-r--r--test/lit/passes/jspi.wast124
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: )