| /* |
| * Copyright 2017 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. |
| */ |
| |
| // |
| // Loads wasm plus a list of functions that are global ctors, i.e., |
| // are to be executed. It then executes as many of them as it can, |
| // applying their changes to memory as needed, then writes it. In |
| // other words, this executes code at compile time to speed up |
| // startup later. |
| // |
| |
| #include <memory> |
| |
| #include "pass.h" |
| #include "support/command-line.h" |
| #include "support/file.h" |
| #include "support/colors.h" |
| #include "wasm-io.h" |
| #include "wasm-interpreter.h" |
| #include "wasm-builder.h" |
| #include "ir/memory-utils.h" |
| #include "ir/global-utils.h" |
| #include "ir/import-utils.h" |
| #include "ir/literal-utils.h" |
| |
| using namespace wasm; |
| |
| struct FailToEvalException { |
| std::string why; |
| FailToEvalException(std::string why) : why(why) {} |
| }; |
| |
| // We do not have access to imported globals |
| class EvallingGlobalManager { |
| // values of globals |
| std::map<Name, Literal> globals; |
| |
| // globals that are dangerous to modify in the module |
| std::set<Name> dangerousGlobals; |
| |
| // whether we are done adding new globals |
| bool sealed = false; |
| |
| public: |
| void addDangerous(Name name) { |
| dangerousGlobals.insert(name); |
| } |
| |
| void seal() { |
| sealed = true; |
| } |
| |
| // for equality purposes, we just care about the globals |
| // and whether they have changed |
| bool operator==(const EvallingGlobalManager& other) { |
| return globals == other.globals; |
| } |
| bool operator!=(const EvallingGlobalManager& other) { |
| return !(*this == other); |
| } |
| |
| Literal& operator[](Name name) { |
| if (dangerousGlobals.count(name) > 0) { |
| std::string extra; |
| if (name == "___dso_handle") { |
| extra = "\nrecommendation: build with -s NO_EXIT_RUNTIME=1 so that calls to atexit that use ___dso_handle are not emitted"; |
| } |
| throw FailToEvalException(std::string("tried to access a dangerous (import-initialized) global: ") + name.str + extra); |
| } |
| return globals[name]; |
| } |
| |
| struct Iterator { |
| Name first; |
| Literal second; |
| bool found; |
| |
| Iterator() : found(false) {} |
| Iterator(Name name, Literal value) : first(name), second(value), found(true) {} |
| |
| bool operator==(const Iterator& other) { |
| return first == other.first && second == other.second && found == other.found; |
| } |
| bool operator!=(const Iterator& other) { |
| return !(*this == other); |
| } |
| }; |
| |
| Iterator find(Name name) { |
| if (globals.find(name) == globals.end()) { |
| return end(); |
| } |
| return Iterator(name, globals[name]); |
| } |
| |
| Iterator end() { |
| return Iterator(); |
| } |
| }; |
| |
| enum { |
| // put the stack in some ridiculously high location |
| STACK_START = 0x40000000, |
| // use a ridiculously large stack size |
| STACK_SIZE = 32 * 1024 * 1024 |
| }; |
| |
| class EvallingModuleInstance : public ModuleInstanceBase<EvallingGlobalManager, EvallingModuleInstance> { |
| public: |
| EvallingModuleInstance(Module& wasm, ExternalInterface* externalInterface) : ModuleInstanceBase(wasm, externalInterface) { |
| // if any global in the module has a non-const constructor, it is using a global import, |
| // which we don't have, and is illegal to use |
| for (auto& global : wasm.globals) { |
| if (!global->init->is<Const>()) { |
| // some constants are ok to use |
| if (auto* get = global->init->dynCast<GetGlobal>()) { |
| auto name = get->name; |
| auto* import = wasm.getImport(name); |
| if (import->module == Name("env") && ( |
| import->base == Name("STACKTOP") || // stack constants are special, we handle them |
| import->base == Name("STACK_MAX") |
| )) { |
| continue; // this is fine |
| } |
| } |
| // this global is dangerously initialized by an import, so if it is used, we must fail |
| globals.addDangerous(global->name); |
| } |
| } |
| } |
| |
| std::vector<char> stack; |
| |
| // create C stack space for us to use. We do *NOT* care about their contents, |
| // assuming the stack top was unwound. the memory may have been modified, |
| // but it should not be read afterwards, doing so would be undefined behavior |
| void setupEnvironment() { |
| // prepare scratch memory |
| stack.resize(STACK_SIZE); |
| // tell the module to accept writes up to the stack end |
| auto total = STACK_START + STACK_SIZE; |
| memorySize = total / Memory::kPageSize; |
| } |
| |
| // flatten memory into a single segment |
| void flattenMemory() { |
| MemoryUtils::flatten(wasm.memory); |
| } |
| }; |
| |
| struct CtorEvalExternalInterface : EvallingModuleInstance::ExternalInterface { |
| Module* wasm; |
| EvallingModuleInstance* instance; |
| |
| void init(Module& wasm_, EvallingModuleInstance& instance_) override { |
| wasm = &wasm_; |
| instance = &instance_; |
| } |
| |
| void importGlobals(EvallingGlobalManager& globals, Module& wasm_) override { |
| // fill usable values for stack imports, and globals initialized to them |
| if (auto* stackTop = ImportUtils::getImport(wasm_, "env", "STACKTOP")) { |
| globals[stackTop->name] = Literal(int32_t(STACK_START)); |
| if (auto* stackTop = GlobalUtils::getGlobalInitializedToImport(wasm_, "env", "STACKTOP")) { |
| globals[stackTop->name] = Literal(int32_t(STACK_START)); |
| } |
| } |
| if (auto* stackMax = ImportUtils::getImport(wasm_, "env", "STACK_MAX")) { |
| globals[stackMax->name] = Literal(int32_t(STACK_START)); |
| if (auto* stackMax = GlobalUtils::getGlobalInitializedToImport(wasm_, "env", "STACK_MAX")) { |
| globals[stackMax->name] = Literal(int32_t(STACK_START)); |
| } |
| } |
| // fill in fake values for everything else, which is dangerous to use |
| for (auto& global : wasm_.globals) { |
| if (globals.find(global->name) == globals.end()) { |
| globals[global->name] = LiteralUtils::makeLiteralZero(global->type); |
| } |
| } |
| for (auto& import : wasm_.imports) { |
| if (import->kind == ExternalKind::Global) { |
| if (globals.find(import->name) == globals.end()) { |
| globals[import->name] = LiteralUtils::makeLiteralZero(import->globalType); |
| } |
| } |
| } |
| } |
| |
| Literal callImport(Import *import, LiteralList& arguments) override { |
| std::string extra; |
| if (import->module == "env" && import->base == "___cxa_atexit") { |
| extra = "\nrecommendation: build with -s NO_EXIT_RUNTIME=1 so that calls to atexit are not emitted"; |
| } |
| throw FailToEvalException(std::string("call import: ") + import->module.str + "." + import->base.str + extra); |
| } |
| |
| Literal callTable(Index index, LiteralList& arguments, Type result, EvallingModuleInstance& instance) override { |
| // we assume the table is not modified (hmm) |
| // look through the segments, try to find the function |
| for (auto& segment : wasm->table.segments) { |
| Index start; |
| // look for the index in this segment. if it has a constant offset, we look in |
| // the proper range. if it instead gets a global, we rely on the fact that when |
| // not dynamically linking then the table is loaded at offset 0. |
| if (auto* c = segment.offset->dynCast<Const>()) { |
| start = c->value.getInteger(); |
| } else if (segment.offset->is<GetGlobal>()) { |
| start = 0; |
| } else { |
| WASM_UNREACHABLE(); // wasm spec only allows const and get_global there |
| } |
| auto end = start + segment.data.size(); |
| if (start <= index && index < end) { |
| auto name = segment.data[index - start]; |
| // if this is one of our functions, we can call it; if it was imported, fail |
| if (wasm->getFunctionOrNull(name)) { |
| return instance.callFunctionInternal(name, arguments); |
| } else { |
| throw FailToEvalException(std::string("callTable on imported function: ") + name.str); |
| } |
| } |
| } |
| throw FailToEvalException(std::string("callTable on index not found in static segments: ") + std::to_string(index)); |
| } |
| |
| int8_t load8s(Address addr) override { return doLoad<int8_t>(addr); } |
| uint8_t load8u(Address addr) override { return doLoad<uint8_t>(addr); } |
| int16_t load16s(Address addr) override { return doLoad<int16_t>(addr); } |
| uint16_t load16u(Address addr) override { return doLoad<uint16_t>(addr); } |
| int32_t load32s(Address addr) override { return doLoad<int32_t>(addr); } |
| uint32_t load32u(Address addr) override { return doLoad<uint32_t>(addr); } |
| int64_t load64s(Address addr) override { return doLoad<int64_t>(addr); } |
| uint64_t load64u(Address addr) override { return doLoad<uint64_t>(addr); } |
| |
| void store8(Address addr, int8_t value) override { doStore<int8_t>(addr, value); } |
| void store16(Address addr, int16_t value) override { doStore<int16_t>(addr, value); } |
| void store32(Address addr, int32_t value) override { doStore<int32_t>(addr, value); } |
| void store64(Address addr, int64_t value) override { doStore<int64_t>(addr, value); } |
| |
| void growMemory(Address /*oldSize*/, Address newSize) override { |
| throw FailToEvalException("grow memory"); |
| } |
| |
| void trap(const char* why) override { |
| throw FailToEvalException(std::string("trap: ") + why); |
| } |
| |
| private: |
| // TODO: handle unaligned too, see shell-interface |
| |
| template <typename T> |
| T* getMemory(Address address) { |
| // if memory is on the stack, use the stack |
| if (address >= STACK_START) { |
| Address relative = address - STACK_START; |
| if (relative + sizeof(T) > STACK_SIZE) { |
| throw FailToEvalException("stack usage too high"); |
| } |
| // in range, all is good, use the stack |
| return (T*)(&instance->stack[relative]); |
| } |
| |
| // otherwise, this must be in the singleton segment. resize as needed |
| if (wasm->memory.segments.size() == 0) { |
| std::vector<char> temp; |
| Builder builder(*wasm); |
| wasm->memory.segments.push_back( |
| Memory::Segment( |
| builder.makeConst(Literal(int32_t(0))), |
| temp |
| ) |
| ); |
| } |
| assert(wasm->memory.segments[0].offset->cast<Const>()->value.getInteger() == 0); |
| auto max = address + sizeof(T); |
| auto& data = wasm->memory.segments[0].data; |
| if (max > data.size()) { |
| data.resize(max); |
| } |
| return (T*)(&data[address]); |
| } |
| |
| template <typename T> |
| void doStore(Address address, T value) { |
| // do a memcpy to avoid undefined behavior if unaligned |
| memcpy(getMemory<T>(address), &value, sizeof(T)); |
| } |
| |
| template <typename T> |
| T doLoad(Address address) { |
| // do a memcpy to avoid undefined behavior if unaligned |
| T ret; |
| memcpy(&ret, getMemory<T>(address), sizeof(T)); |
| return ret; |
| } |
| }; |
| |
| void evalCtors(Module& wasm, std::vector<std::string> ctors) { |
| CtorEvalExternalInterface interface; |
| try { |
| // create an instance for evalling |
| EvallingModuleInstance instance(wasm, &interface); |
| // flatten memory, so we do not depend on the layout of data segments |
| instance.flattenMemory(); |
| // set up the stack area and other environment details |
| instance.setupEnvironment(); |
| // we should not add new globals from here on; as a result, using |
| // an imported global will fail, as it is missing and so looks new |
| instance.globals.seal(); |
| // go one by one, in order, until we fail |
| // TODO: if we knew priorities, we could reorder? |
| for (auto& ctor : ctors) { |
| std::cerr << "trying to eval " << ctor << '\n'; |
| // snapshot memory, as either the entire function is done, or none |
| auto memoryBefore = wasm.memory; |
| // snapshot globals (note that STACKTOP might be modified, but should |
| // be returned, so that works out) |
| auto globalsBefore = instance.globals; |
| try { |
| instance.callExport(ctor); |
| } catch (FailToEvalException& fail) { |
| // that's it, we failed, so stop here, cleaning up partial |
| // memory changes first |
| std::cerr << " ...stopping since could not eval: " << fail.why << "\n"; |
| wasm.memory = memoryBefore; |
| return; |
| } |
| if (instance.globals != globalsBefore) { |
| std::cerr << " ...stopping since globals modified\n"; |
| wasm.memory = memoryBefore; |
| return; |
| } |
| std::cerr << " ...success on " << ctor << ".\n"; |
| // success, the entire function was evalled! |
| // we can nop the function (which may be used elsewhere) |
| // and remove the export |
| auto* exp = wasm.getExport(ctor); |
| auto* func = wasm.getFunction(exp->value); |
| func->body = wasm.allocator.alloc<Nop>(); |
| wasm.removeExport(exp->name); |
| } |
| } catch (FailToEvalException& fail) { |
| // that's it, we failed to even create the instance |
| std::cerr << " ...stopping since could not create module instance: " << fail.why << "\n"; |
| return; |
| } |
| } |
| |
| // |
| // main |
| // |
| |
| int main(int argc, const char* argv[]) { |
| Name entry; |
| std::vector<std::string> passes; |
| bool emitBinary = true; |
| bool debugInfo = false; |
| std::string ctorsString; |
| |
| Options options("wasm-ctor-eval", "Execute C++ global constructors ahead of time"); |
| options |
| .add("--output", "-o", "Output file (stdout if not specified)", |
| Options::Arguments::One, |
| [](Options* o, const std::string& argument) { |
| o->extra["output"] = argument; |
| Colors::disable(); |
| }) |
| .add("--emit-text", "-S", "Emit text instead of binary for the output file", |
| Options::Arguments::Zero, |
| [&](Options *o, const std::string& argument) { emitBinary = false; }) |
| .add("--debuginfo", "-g", "Emit names section and debug info", |
| Options::Arguments::Zero, |
| [&](Options *o, const std::string& arguments) { debugInfo = true; }) |
| .add("--ctors", "-c", "Comma-separated list of global constructor functions to evaluate", |
| Options::Arguments::One, |
| [&](Options* o, const std::string& argument) { |
| ctorsString = argument; |
| }) |
| .add_positional("INFILE", Options::Arguments::One, |
| [](Options* o, const std::string& argument) { |
| o->extra["infile"] = argument; |
| }); |
| options.parse(argc, argv); |
| |
| auto input(read_file<std::string>(options.extra["infile"], Flags::Text, options.debug ? Flags::Debug : Flags::Release)); |
| |
| Module wasm; |
| |
| { |
| if (options.debug) std::cerr << "reading...\n"; |
| ModuleReader reader; |
| reader.setDebug(options.debug); |
| |
| try { |
| reader.read(options.extra["infile"], wasm); |
| } catch (ParseException& p) { |
| p.dump(std::cerr); |
| Fatal() << "error in parsing input"; |
| } |
| } |
| |
| // get list of ctors, and eval them |
| std::vector<std::string> ctors; |
| std::istringstream stream(ctorsString); |
| std::string temp; |
| while (std::getline(stream, temp, ',')) { |
| ctors.push_back(temp); |
| } |
| evalCtors(wasm, ctors); |
| |
| // Do some useful optimizations after the evalling |
| { |
| PassRunner passRunner(&wasm); |
| passRunner.add("memory-packing"); // we flattened it, so re-optimize |
| passRunner.add("remove-unused-names"); |
| passRunner.add("dce"); |
| passRunner.add("merge-blocks"); |
| passRunner.add("vacuum"); |
| passRunner.add("remove-unused-module-elements"); |
| passRunner.run(); |
| } |
| |
| if (options.extra.count("output") > 0) { |
| if (options.debug) std::cerr << "writing..." << std::endl; |
| ModuleWriter writer; |
| writer.setDebug(options.debug); |
| writer.setBinary(emitBinary); |
| writer.setDebugInfo(debugInfo); |
| writer.write(wasm, options.extra["output"]); |
| } |
| } |