| /* | |
| * Copyright 2016 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. | |
| */ | |
| // | |
| // i64 values are not valid in JS, and must be handled in some other | |
| // way. This pass transforms all i64s in params and results in imports | |
| // and exports into pairs of i32, i32 (low, high). If JS on the outside | |
| // calls with that ABI, then everything should then just work, using | |
| // stub methods added in this pass, that thunk i64s into i32, i32 and | |
| // vice versa as necessary. | |
| // | |
| // Another variation also "prunes" imports and exports that we cannot yet | |
| // legalize, like exports and imports with SIMD or multivalue. Until we write | |
| // the logic to legalize them, removing those imports/exports still allows us to | |
| // fuzz all the legal imports/exports. (Note that multivalue is supported in | |
| // exports in newer VMs - node 16+ - so that part is only needed for older VMs.) | |
| // | |
| #include "asmjs/shared-constants.h" | |
| #include "ir/element-utils.h" | |
| #include "ir/import-utils.h" | |
| #include "ir/literal-utils.h" | |
| #include "ir/utils.h" | |
| #include "pass.h" | |
| #include "shared-constants.h" | |
| #include "wasm-builder.h" | |
| #include "wasm.h" | |
| #include <utility> | |
| namespace wasm { | |
| namespace { | |
| // These are aliases for getTempRet0/setTempRet0 which emscripten defines in | |
| // compiler-rt and exports under these names. | |
| static const Name GET_TEMP_RET_EXPORT("__get_temp_ret"); | |
| static const Name SET_TEMP_RET_EXPORT("__set_temp_ret"); | |
| // For non-emscripten module we expect the host to define these functions so | |
| // and we import them under these names. | |
| static const Name GET_TEMP_RET_IMPORT("getTempRet0"); | |
| static const Name SET_TEMP_RET_IMPORT("setTempRet0"); | |
| struct LegalizeJSInterface : public Pass { | |
| // Adds calls to new imports. | |
| bool addsEffects() override { return true; } | |
| LegalizeJSInterface() {} | |
| void run(Module* module) override { | |
| setTempRet0 = nullptr; | |
| getTempRet0 = nullptr; | |
| auto exportOriginals = | |
| hasArgument("legalize-js-interface-export-originals"); | |
| exportedHelpers = hasArgument("legalize-js-interface-exported-helpers"); | |
| // for each illegal export, we must export a legalized stub instead | |
| std::vector<std::unique_ptr<Export>> newExports; | |
| for (auto& ex : module->exports) { | |
| if (ex->kind == ExternalKind::Function) { | |
| // if it's an import, ignore it | |
| auto* name = ex->getInternalName(); | |
| auto* func = module->getFunction(*name); | |
| if (isIllegal(func)) { | |
| // Provide a legal function for the export. | |
| auto legalName = makeLegalStub(func, module); | |
| *name = legalName; | |
| if (exportOriginals) { | |
| // Also export the original function, before legalization. This is | |
| // not normally useful for JS, except in cases like dynamic linking | |
| // where the JS loader code must copy exported wasm functions into | |
| // the table, and they must not be legalized as other wasm code will | |
| // do an indirect call to them. However, don't do this for imported | |
| // functions, as those would be legalized in their actual module | |
| // anyhow. It also makes no sense to do this for dynCalls, as they | |
| // are only called from JS. | |
| if (!func->imported() && !isDynCall(ex->name)) { | |
| Builder builder(*module); | |
| Name newName = std::string("orig$") + ex->name.toString(); | |
| newExports.push_back(builder.makeExport( | |
| newName, func->name, ExternalKind::Function)); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| for (auto& ex : newExports) { | |
| module->addExport(std::move(ex)); | |
| } | |
| // Avoid iterator invalidation later. | |
| std::vector<Function*> originalFunctions; | |
| for (auto& func : module->functions) { | |
| originalFunctions.push_back(func.get()); | |
| } | |
| // for each illegal import, we must call a legalized stub instead | |
| for (auto* im : originalFunctions) { | |
| if (im->imported() && isIllegal(im)) { | |
| auto* func = makeLegalStubForCalledImport(im, module); | |
| illegalImportsToLegal[im->name] = func; | |
| } | |
| } | |
| if (!illegalImportsToLegal.empty()) { | |
| // fix up imports: call of an illegal import must be turned to a call of a | |
| // legal import. the same must be done with ref.funcs. | |
| struct Fixer : public WalkerPass<PostWalker<Fixer>> { | |
| bool isFunctionParallel() override { return true; } | |
| std::unique_ptr<Pass> create() override { | |
| return std::make_unique<Fixer>(illegalImportsToLegal); | |
| } | |
| std::unordered_map<Name, Function*>& illegalImportsToLegal; | |
| Fixer(std::unordered_map<Name, Function*>& illegalImportsToLegal) | |
| : illegalImportsToLegal(illegalImportsToLegal) {} | |
| void visitCall(Call* curr) { | |
| auto iter = illegalImportsToLegal.find(curr->target); | |
| if (iter == illegalImportsToLegal.end()) { | |
| return; | |
| } | |
| replaceCurrent(Builder(*getModule()) | |
| .makeCall(iter->second->name, | |
| curr->operands, | |
| curr->type, | |
| curr->isReturn)); | |
| } | |
| void visitRefFunc(RefFunc* curr) { | |
| auto iter = illegalImportsToLegal.find(curr->func); | |
| if (iter == illegalImportsToLegal.end()) { | |
| return; | |
| } | |
| curr->func = iter->second->name; | |
| curr->finalize(*getModule()); | |
| } | |
| }; | |
| Fixer fixer(illegalImportsToLegal); | |
| fixer.run(getPassRunner(), module); | |
| fixer.runOnModuleCode(getPassRunner(), module); | |
| // Finally we can remove all the now-unused illegal imports | |
| for (const auto& pair : illegalImportsToLegal) { | |
| module->removeFunction(pair.first); | |
| } | |
| } | |
| } | |
| private: | |
| // map of illegal to legal names for imports | |
| std::unordered_map<Name, Function*> illegalImportsToLegal; | |
| bool exportedHelpers = false; | |
| Function* getTempRet0 = nullptr; | |
| Function* setTempRet0 = nullptr; | |
| template<typename T> bool isIllegal(T* t) { | |
| for (const auto& param : t->getParams()) { | |
| if (param == Type::i64) { | |
| return true; | |
| } | |
| } | |
| return t->getResults() == Type::i64; | |
| } | |
| bool isDynCall(Name name) { return name.startsWith("dynCall_"); } | |
| Function* tempSetter(Module* module) { | |
| if (!setTempRet0) { | |
| if (exportedHelpers) { | |
| auto* ex = module->getExport(SET_TEMP_RET_EXPORT); | |
| setTempRet0 = module->getFunction((ex->kind == ExternalKind::Function) | |
| ? *ex->getInternalName() | |
| : Name()); | |
| } else { | |
| setTempRet0 = getFunctionOrImport( | |
| module, SET_TEMP_RET_IMPORT, Type::i32, Type::none); | |
| } | |
| } | |
| return setTempRet0; | |
| } | |
| Function* tempGetter(Module* module) { | |
| if (!getTempRet0) { | |
| if (exportedHelpers) { | |
| auto* ex = module->getExport(GET_TEMP_RET_EXPORT); | |
| getTempRet0 = module->getFunction((ex->kind == ExternalKind::Function) | |
| ? *ex->getInternalName() | |
| : Name()); | |
| } else { | |
| getTempRet0 = getFunctionOrImport( | |
| module, GET_TEMP_RET_IMPORT, Type::none, Type::i32); | |
| } | |
| } | |
| return getTempRet0; | |
| } | |
| // JS calls the export, so it must call a legal stub that calls the actual | |
| // wasm function | |
| Name makeLegalStub(Function* func, Module* module) { | |
| Name legalName(std::string("legalstub$") + func->name.toString()); | |
| // a method may be exported multiple times | |
| if (module->getFunctionOrNull(legalName)) { | |
| return legalName; | |
| } | |
| Builder builder(*module); | |
| auto* legal = new Function(); | |
| legal->name = legalName; | |
| legal->hasExplicitName = true; | |
| auto* call = module->allocator.alloc<Call>(); | |
| call->target = func->name; | |
| call->type = func->getResults(); | |
| std::vector<Type> legalParams; | |
| for (const auto& param : func->getParams()) { | |
| if (param == Type::i64) { | |
| call->operands.push_back(I64Utilities::recreateI64( | |
| builder, legalParams.size(), legalParams.size() + 1)); | |
| legalParams.push_back(Type::i32); | |
| legalParams.push_back(Type::i32); | |
| } else { | |
| call->operands.push_back( | |
| builder.makeLocalGet(legalParams.size(), param)); | |
| legalParams.push_back(param); | |
| } | |
| } | |
| Type resultsType = | |
| func->getResults() == Type::i64 ? Type::i32 : func->getResults(); | |
| legal->type = | |
| Type(Signature(Type(legalParams), resultsType), NonNullable, Exact); | |
| if (func->getResults() == Type::i64) { | |
| auto index = Builder::addVar(legal, Name(), Type::i64); | |
| auto* block = builder.makeBlock(); | |
| block->list.push_back(builder.makeLocalSet(index, call)); | |
| block->list.push_back( | |
| builder.makeCall(tempSetter(module)->name, | |
| {I64Utilities::getI64High(builder, index)}, | |
| Type::none)); | |
| block->list.push_back(I64Utilities::getI64Low(builder, index)); | |
| block->finalize(); | |
| legal->body = block; | |
| } else { | |
| legal->body = call; | |
| } | |
| return module->addFunction(legal)->name; | |
| } | |
| // wasm calls the import, so it must call a stub that calls the actual legal | |
| // JS import | |
| Function* makeLegalStubForCalledImport(Function* im, Module* module) { | |
| Builder builder(*module); | |
| auto legalIm = std::make_unique<Function>(); | |
| legalIm->name = Name(std::string("legalimport$") + im->name.toString()); | |
| legalIm->module = im->module; | |
| legalIm->base = im->base; | |
| legalIm->hasExplicitName = true; | |
| auto stub = std::make_unique<Function>(); | |
| stub->name = Name(std::string("legalfunc$") + im->name.toString()); | |
| stub->type = im->type.with(Exact); | |
| stub->hasExplicitName = true; | |
| auto* call = module->allocator.alloc<Call>(); | |
| call->target = legalIm->name; | |
| std::vector<Type> params; | |
| Index i = 0; | |
| for (const auto& param : im->getParams()) { | |
| if (param == Type::i64) { | |
| call->operands.push_back(I64Utilities::getI64Low(builder, i)); | |
| call->operands.push_back(I64Utilities::getI64High(builder, i)); | |
| params.push_back(Type::i32); | |
| params.push_back(Type::i32); | |
| } else { | |
| call->operands.push_back(builder.makeLocalGet(i, param)); | |
| params.push_back(param); | |
| } | |
| ++i; | |
| } | |
| if (im->getResults() == Type::i64) { | |
| call->type = Type::i32; | |
| Expression* get = | |
| builder.makeCall(tempGetter(module)->name, {}, call->type); | |
| stub->body = I64Utilities::recreateI64(builder, call, get); | |
| } else { | |
| call->type = im->getResults(); | |
| stub->body = call; | |
| } | |
| legalIm->type = | |
| Type(Signature(Type(params), call->type), NonNullable, Inexact); | |
| auto* stubPtr = stub.get(); | |
| if (!module->getFunctionOrNull(stub->name)) { | |
| module->addFunction(std::move(stub)); | |
| } | |
| if (!module->getFunctionOrNull(legalIm->name)) { | |
| module->addFunction(std::move(legalIm)); | |
| } | |
| return stubPtr; | |
| } | |
| static Function* | |
| getFunctionOrImport(Module* module, Name name, Type params, Type results) { | |
| // First look for the function by name | |
| if (Function* f = module->getFunctionOrNull(name)) { | |
| return f; | |
| } | |
| // Then see if its already imported | |
| ImportInfo info(*module); | |
| if (Function* f = info.getImportedFunction(ENV, name)) { | |
| return f; | |
| } | |
| // Failing that create a new function import. | |
| auto import = Builder::makeFunction( | |
| name, Type(Signature(params, results), NonNullable, Inexact), {}); | |
| import->module = ENV; | |
| import->base = name; | |
| auto* ret = import.get(); | |
| module->addFunction(std::move(import)); | |
| return ret; | |
| } | |
| }; | |
| struct LegalizeAndPruneJSInterface : public LegalizeJSInterface { | |
| // Legalize and add pruning on top. | |
| LegalizeAndPruneJSInterface() : LegalizeJSInterface() {} | |
| void run(Module* module) override { | |
| LegalizeJSInterface::run(module); | |
| prune(module); | |
| } | |
| void prune(Module* module) { | |
| // For each function name, the exported id it is exported with. For | |
| // example, | |
| // | |
| // (func $foo (export "bar") | |
| // | |
| // Would have exportedFunctions["foo"] = "bar"; | |
| std::unordered_map<Name, Name> exportedFunctions; | |
| for (auto& exp : module->exports) { | |
| if (exp->kind == ExternalKind::Function) { | |
| exportedFunctions[*exp->getInternalName()] = exp->name; | |
| } | |
| } | |
| for (auto& func : module->functions) { | |
| // If the function is neither exported nor imported, no problem. | |
| auto imported = func->imported(); | |
| auto exported = exportedFunctions.contains(func->name); | |
| if (!imported && !exported) { | |
| continue; | |
| } | |
| // The params are allowed to be multivalue, but not the results. Otherwise | |
| // look for SIMD etc. | |
| auto sig = func->getSig(); | |
| auto illegal = isIllegal(sig.results); | |
| illegal = | |
| illegal || std::any_of(sig.params.begin(), | |
| sig.params.end(), | |
| [&](const Type& t) { return isIllegal(t); }); | |
| if (!illegal) { | |
| continue; | |
| } | |
| // Prune an import by implementing it in a trivial manner. | |
| if (imported) { | |
| func->module = func->base = Name(); | |
| func->type = func->type.with(Exact); | |
| Builder builder(*module); | |
| if (sig.results == Type::none) { | |
| func->body = builder.makeNop(); | |
| } else if (sig.results.isDefaultable()) { | |
| func->body = | |
| builder.makeConstantExpression(Literal::makeZeros(sig.results)); | |
| } else { | |
| // We have nothing better than to trap here. | |
| func->body = builder.makeUnreachable(); | |
| } | |
| } | |
| // Prune an export by just removing it. | |
| if (exported) { | |
| module->removeExport(exportedFunctions[func->name]); | |
| } | |
| } | |
| // RefFunc types etc. need updating. | |
| ReFinalize().run(getPassRunner(), module); | |
| ReFinalize().runOnModuleCode(getPassRunner(), module); | |
| // Globals. | |
| std::vector<Name> illegalExports; | |
| for (auto& exp : module->exports) { | |
| if (exp->kind == ExternalKind::Global) { | |
| auto name = *exp->getInternalName(); | |
| if (isIllegal(module->getGlobal(name)->type)) { | |
| illegalExports.push_back(exp->name); | |
| } | |
| } | |
| } | |
| for (auto name : illegalExports) { | |
| module->removeExport(name); | |
| } | |
| } | |
| bool isIllegal(Type type) { | |
| auto features = type.getFeatures(); | |
| return features.hasSIMD() || features.hasMultivalue() || | |
| features.hasExceptionHandling() || features.hasStackSwitching(); | |
| } | |
| }; | |
| } // anonymous namespace | |
| Pass* createLegalizeJSInterfacePass() { return new LegalizeJSInterface(); } | |
| Pass* createLegalizeAndPruneJSInterfacePass() { | |
| return new LegalizeAndPruneJSInterface(); | |
| } | |
| } // namespace wasm |