blob: d5fed1faf633d86eb7fe779b7dcd06dbf5e17467 [file] [log] [blame] [edit]
/*
* 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/module-splitting.h"
#include "ir/names.h"
#include "ir/utils.h"
#include "pass.h"
#include "shared-constants.h"
#include "support/file.h"
#include "support/string.h"
#include "wasm-builder.h"
#include "wasm.h"
#include <utility>
//
// Convert a module to be compatible with JavaScript promise integration (JSPI).
// Promising 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`. Suspending 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`.
//
// By default all imports and exports will be wrapped, but this can be
// controlled with the following options:
//
// [email protected],module2.base2,module3.base3
//
// Wrap each import in the comma-separated list. Wildcards and a separate
// files are supported. See `asyncify-imports` for more details.
//
// --pass-arg=jspi-exports@function_one,function_two,function_three
//
// Wrap each export in the comma-separated list. Similar to jspi-imports,
// wildcards and separate files are supported.
//
// --pass-arg=jspi-split-module
//
// Enables integration with wasm-split. A JSPI'ed function named
// `__load_secondary_module` will be injected that is used by wasm-split to
// load a secondary module.
//
namespace wasm {
static std::string getFullFunctionName(Name module, Name base) {
return std::string(module.str) + '.' + base.toString();
}
static bool canChangeState(std::string name, String::Split stateChangers) {
// When no state changers are given default to everything changes state.
if (stateChangers.empty()) {
return true;
}
for (auto& stateChanger : stateChangers) {
if (String::wildcardMatch(stateChanger, name)) {
return true;
}
}
return false;
}
struct JSPI : public Pass {
Type externref = Type(HeapType::ext, Nullable);
void run(Module* module) override {
Builder builder(*module);
// Find which imports can suspend.
auto stateChangingImports = String::trim(
read_possible_response_file(getArgumentOrDefault("jspi-imports", "")));
String::Split listedImports(stateChangingImports, ",");
// Find which exports should create a promise.
auto stateChangingExports = String::trim(
read_possible_response_file(getArgumentOrDefault("jspi-exports", "")));
String::Split listedExports(stateChangingExports, ",");
bool wasmSplit = hasArgument("jspi-split-module");
if (wasmSplit) {
// Make an import for the load secondary module function so a JSPI wrapper
// version will be created.
auto import =
Builder::makeFunction(ModuleSplitting::LOAD_SECONDARY_MODULE,
Signature(Type::none, Type::none),
{});
import->module = ENV;
import->base = ModuleSplitting::LOAD_SECONDARY_MODULE;
module->addFunction(std::move(import));
listedImports.push_back(
ENV.toString() + "." +
ModuleSplitting::LOAD_SECONDARY_MODULE.toString());
}
// 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(HeapType::noext),
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 &&
canChangeState(ex->name.toString(), listedExports)) {
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;
}
}
// Replace any references to the original exports that are in the elements.
for (auto& segment : module->elementSegments) {
if (!segment->type.isFunction()) {
continue;
}
for (Index i = 0; i < segment->data.size(); i++) {
if (auto* get = segment->data[i]->dynCast<RefFunc>()) {
auto iter = wrappedExports.find(get->func);
if (iter == wrappedExports.end()) {
continue;
}
auto* replacementRef = builder.makeRefFunc(
iter->second, module->getFunction(iter->second)->type);
segment->data[i] = replacementRef;
}
}
}
// 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() &&
canChangeState(getFullFunctionName(im->module, im->base),
listedImports)) {
makeWrapperForImport(im, module, suspender, wasmSplit);
}
}
}
private:
Name makeWrapperForExport(Function* func, Module* module, Name suspender) {
Name wrapperName = Names::getValidFunctionName(
*module, std::string("export$") + func->name.toString());
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,
bool wasmSplit) {
Builder builder(*module);
auto wrapperIm = std::make_unique<Function>();
wrapperIm->name = Names::getValidFunctionName(
*module, std::string("import$") + im->name.toString());
wrapperIm->module = im->module;
wrapperIm->base = im->base;
auto stub = std::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;
}
auto* block = builder.makeBlock();
// Store the suspender so it can be restored after the call in case it is
// modified by another entry into a Wasm export.
auto supsenderCopyIndex = Builder::addVar(stub.get(), externref);
// If there's a return value we need to store it so it can be returned
// after restoring the suspender.
std::optional<Index> returnIndex;
if (stub->getResults().isConcrete()) {
returnIndex = Builder::addVar(stub.get(), stub->getResults());
}
block->list.push_back(builder.makeLocalSet(
supsenderCopyIndex, builder.makeGlobalGet(suspender, externref)));
if (returnIndex) {
block->list.push_back(builder.makeLocalSet(*returnIndex, call));
} else {
block->list.push_back(call);
}
// Restore the suspender.
block->list.push_back(builder.makeGlobalSet(
suspender, builder.makeLocalGet(supsenderCopyIndex, externref)));
if (returnIndex) {
block->list.push_back(
builder.makeLocalGet(*returnIndex, stub->getResults()));
}
block->finalize();
call->type = im->getResults();
stub->body = block;
wrapperIm->type = Signature(Type(params), call->type);
if (wasmSplit && im->name == ModuleSplitting::LOAD_SECONDARY_MODULE) {
// In non-debug builds the name of the JSPI wrapper function for loading
// the secondary module will be removed. Create an export of it so that
// wasm-split can find it.
module->addExport(
builder.makeExport(ModuleSplitting::LOAD_SECONDARY_MODULE,
ModuleSplitting::LOAD_SECONDARY_MODULE,
ExternalKind::Function));
}
module->removeFunction(im->name);
module->addFunction(std::move(stub));
module->addFunction(std::move(wrapperIm));
}
};
Pass* createJSPIPass() { return new JSPI(); }
} // namespace wasm