/* * 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 namespace wasm { namespace { // These are aliases for getTempRet0/setTempRet0 which emscripten defines in // compiler-rt and exports under these names. static Name GET_TEMP_RET_EXPORT("__get_temp_ret"); static 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 Name GET_TEMP_RET_IMPORT("getTempRet0"); static 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> newExports; for (auto& ex : module->exports) { if (ex->kind == ExternalKind::Function) { // if it's an import, ignore it auto* func = module->getFunction(ex->value); if (isIllegal(func)) { // Provide a legal function for the export. auto legalName = makeLegalStub(func, module); ex->value = 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 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 funcName = makeLegalStubForCalledImport(im, module); illegalImportsToLegal[im->name] = funcName; // we need to use the legalized version in the tables, as the import // from JS is legal for JS. Our stub makes it look like a native wasm // function. ElementUtils::iterAllElementFunctionNames(module, [&](Name& name) { if (name == im->name) { name = funcName; } }); } } if (!illegalImportsToLegal.empty()) { // fix up imports: call_import of an illegal must be turned to a call of a // legal. the same must be done with ref.funcs. struct Fixer : public WalkerPass> { bool isFunctionParallel() override { return true; } std::unique_ptr create() override { return std::make_unique(illegalImportsToLegal); } std::map* illegalImportsToLegal; Fixer(std::map* illegalImportsToLegal) : illegalImportsToLegal(illegalImportsToLegal) {} void visitCall(Call* curr) { auto iter = illegalImportsToLegal->find(curr->target); if (iter == illegalImportsToLegal->end()) { return; } replaceCurrent( Builder(*getModule()) .makeCall( iter->second, 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; } }; 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); } } module->removeExport(GET_TEMP_RET_EXPORT); module->removeExport(SET_TEMP_RET_EXPORT); } private: // map of illegal to legal names for imports std::map illegalImportsToLegal; bool exportedHelpers = false; Function* getTempRet0 = nullptr; Function* setTempRet0 = nullptr; template 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->value); } 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->value); } 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->target = func->name; call->type = func->getResults(); std::vector 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 = Signature(Type(legalParams), resultsType); 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 Name makeLegalStubForCalledImport(Function* im, Module* module) { Builder builder(*module); auto legalIm = std::make_unique(); 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(); stub->name = Name(std::string("legalfunc$") + im->name.toString()); stub->type = im->type; stub->hasExplicitName = true; auto* call = module->allocator.alloc(); call->target = legalIm->name; std::vector 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 = Signature(Type(params), call->type); const auto& stubName = stub->name; if (!module->getFunctionOrNull(stubName)) { module->addFunction(std::move(stub)); } if (!module->getFunctionOrNull(legalIm->name)) { module->addFunction(std::move(legalIm)); } return stubName; } 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, Signature(params, results), {}); 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 exportedFunctions; for (auto& exp : module->exports) { if (exp->kind == ExternalKind::Function) { exportedFunctions[exp->value] = exp->name; } } for (auto& func : module->functions) { // If the function is neither exported nor imported, no problem. auto imported = func->imported(); auto exported = exportedFunctions.count(func->name); if (!imported && !exported) { continue; } // The params are allowed to be multivalue, but not the results. Otherwise // look for SIMD. auto sig = func->type.getSignature(); 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(); Builder builder(*module); if (sig.results == Type::none) { func->body = builder.makeNop(); } else { func->body = builder.makeConstantExpression(Literal::makeZeros(sig.results)); } } // Prune an export by just removing it. if (exported) { module->removeExport(exportedFunctions[func->name]); } } // TODO: globals etc. } bool isIllegal(Type type) { auto features = type.getFeatures(); return features.hasSIMD() || features.hasMultivalue(); } }; } // anonymous namespace Pass* createLegalizeJSInterfacePass() { return new LegalizeJSInterface(); } Pass* createLegalizeAndPruneJSInterfacePass() { return new LegalizeAndPruneJSInterface(); } } // namespace wasm