#include "ir/module-utils.h"
#include "ir/names.h"
#include "support/colors.h"
#include "support/file.h"
#include "wasm-builder.h"
#include "wasm-io.h"
#include "wasm-validator.h"
#include "wasm.h"
#include "tool-options.h"
using namespace wasm;
namespace {
Module merged;
enum ExportMergeMode {
ErrorOnExportConflicts,
RenameExportConflicts,
SkipExportConflicts,
} exportMergeMode = ErrorOnExportConflicts;
struct ExportInfo {
Name moduleName;
Name baseName;
};
std::unordered_map<Export*, ExportInfo> exportModuleMap;
using NameUpdates = std::unordered_map<Name, Name>;
using KindNameUpdates = std::unordered_map<ModuleItemKind, NameUpdates>;
void updateNames(Module& wasm, KindNameUpdates& kindNameUpdates) {
if (kindNameUpdates.empty()) {
return;
}
struct NameMapper
: public WalkerPass<
PostWalker<NameMapper, UnifiedExpressionVisitor<NameMapper>>> {
bool isFunctionParallel() override { return true; }
std::unique_ptr<Pass> create() override {
return std::make_unique<NameMapper>(kindNameUpdates);
}
KindNameUpdates& kindNameUpdates;
NameMapper(KindNameUpdates& kindNameUpdates)
: kindNameUpdates(kindNameUpdates) {}
void visitExpression(Expression* curr) {
#define DELEGATE_ID curr->_id
#define DELEGATE_START(id) [[maybe_unused]] auto* cast = curr->cast<id>();
#define DELEGATE_GET_FIELD(id, field) cast->field
#define DELEGATE_FIELD_TYPE(id, field)
#define DELEGATE_FIELD_HEAPTYPE(id, field)
#define DELEGATE_FIELD_CHILD(id, field)
#define DELEGATE_FIELD_OPTIONAL_CHILD(id, field)
#define DELEGATE_FIELD_INT(id, field)
#define DELEGATE_FIELD_LITERAL(id, field)
#define DELEGATE_FIELD_NAME(id, field)
#define DELEGATE_FIELD_SCOPE_NAME_DEF(id, field)
#define DELEGATE_FIELD_SCOPE_NAME_USE(id, field)
#define DELEGATE_FIELD_ADDRESS(id, field)
#define DELEGATE_FIELD_NAME_KIND(id, field, kind) \
if (cast->field.is()) { \
mapName(kind, cast->field); \
}
#include "wasm-delegations-fields.def"
}
void mapModuleFields(Module& wasm) {
for (auto& curr : wasm.exports) {
mapName(ModuleItemKind(curr->kind), curr->value);
}
for (auto& curr : wasm.elementSegments) {
mapName(ModuleItemKind::Table, curr->table);
}
for (auto& curr : wasm.dataSegments) {
mapName(ModuleItemKind::Memory, curr->memory);
}
mapName(ModuleItemKind::Function, wasm.start);
}
private:
Name resolveName(NameUpdates& updates, Name newName, Name oldName) {
std::set<Name> visited;
auto name = newName;
while (1) {
auto iter = updates.find(name);
if (iter == updates.end()) {
return name;
}
if (visited.count(name)) {
Fatal() << "wasm-merge: infinite loop of imports on " << oldName;
}
visited.insert(name);
name = iter->second;
}
}
void mapName(ModuleItemKind kind, Name& name) {
auto iter = kindNameUpdates.find(kind);
if (iter == kindNameUpdates.end()) {
return;
}
auto& nameUpdates = iter->second;
auto iter2 = nameUpdates.find(name);
if (iter2 != nameUpdates.end()) {
name = resolveName(nameUpdates, iter2->second, name);
}
}
} nameMapper(kindNameUpdates);
PassRunner runner(&wasm);
nameMapper.run(&runner, &wasm);
nameMapper.runOnModuleCode(&runner, &wasm);
nameMapper.mapModuleFields(wasm);
}
void renameInputItems(Module& input) {
KindNameUpdates kindNameUpdates;
auto maybeAdd = [&](ModuleItemKind kind, Name& name, const Name newName) {
if (newName != name) {
kindNameUpdates[kind][name] = newName;
name = newName;
}
};
for (auto& curr : input.functions) {
auto name = Names::getValidFunctionName(merged, curr->name);
maybeAdd(ModuleItemKind::Function, curr->name, name);
}
for (auto& curr : input.globals) {
auto name = Names::getValidGlobalName(merged, curr->name);
maybeAdd(ModuleItemKind::Global, curr->name, name);
}
for (auto& curr : input.tags) {
auto name = Names::getValidTagName(merged, curr->name);
maybeAdd(ModuleItemKind::Tag, curr->name, name);
}
for (auto& curr : input.elementSegments) {
auto name = Names::getValidElementSegmentName(merged, curr->name);
maybeAdd(ModuleItemKind::ElementSegment, curr->name, name);
}
for (auto& curr : input.memories) {
auto name = Names::getValidMemoryName(merged, curr->name);
maybeAdd(ModuleItemKind::Memory, curr->name, name);
}
for (auto& curr : input.dataSegments) {
auto name = Names::getValidDataSegmentName(merged, curr->name);
maybeAdd(ModuleItemKind::DataSegment, curr->name, name);
}
for (auto& curr : input.tables) {
auto name = Names::getValidTableName(merged, curr->name);
maybeAdd(ModuleItemKind::Table, curr->name, name);
}
updateNames(input, kindNameUpdates);
}
void copyModuleContents(Module& input, Name inputName) {
ModuleUtils::copyModuleItems(input, merged);
for (auto& curr : input.exports) {
auto copy = std::make_unique<Export>(*curr);
exportModuleMap[copy.get()] = ExportInfo{inputName, curr->name};
copy->name = Names::getValidExportName(merged, copy->name);
if (copy->name != curr->name) {
if (exportMergeMode == ErrorOnExportConflicts) {
Fatal() << "Export name conflict: " << curr->name << " (consider"
<< " --rename-export-conflicts or"
<< " --skip-export-conflicts)\n";
} else if (exportMergeMode == SkipExportConflicts) {
continue;
}
}
merged.addExport(std::move(copy));
}
if (input.start.is()) {
if (!merged.start.is()) {
merged.start = input.start;
} else {
auto copiedOldName =
Names::getValidFunctionName(merged, "merged.start.old");
auto copiedNewName =
Names::getValidFunctionName(merged, "merged.start.new");
auto* copiedOld = ModuleUtils::copyFunction(
merged.getFunction(merged.start), merged, copiedOldName);
ModuleUtils::copyFunction(
merged.getFunction(input.start), merged, copiedNewName);
Builder builder(merged);
copiedOld->body = builder.makeSequence(
copiedOld->body, builder.makeCall(copiedNewName, {}, Type::none));
merged.start = copiedOldName;
}
}
}
void fuseImportsAndExports() {
using ModuleExportMap = std::unordered_map<Name, NameUpdates>;
using KindModuleExportMaps =
std::unordered_map<ExternalKind, ModuleExportMap>;
KindModuleExportMaps kindModuleExportMaps;
for (auto& ex : merged.exports) {
assert(exportModuleMap.count(ex.get()));
ExportInfo& exportInfo = exportModuleMap[ex.get()];
kindModuleExportMaps[ex->kind][exportInfo.moduleName][exportInfo.baseName] =
ex->value;
}
KindNameUpdates kindNameUpdates;
ModuleUtils::iterImportable(merged, [&](ExternalKind kind, Importable* curr) {
if (curr->imported()) {
auto internalName = kindModuleExportMaps[kind][curr->module][curr->base];
if (internalName.is()) {
kindNameUpdates[ModuleItemKind(kind)][curr->name] = internalName;
}
}
});
updateNames(merged, kindNameUpdates);
}
void mergeInto(Module& input, Name inputName) {
renameInputItems(input);
copyModuleContents(input, inputName);
}
}
int main(int argc, const char* argv[]) {
std::vector<std::string> inputFiles;
std::vector<std::string> inputFileNames;
bool emitBinary = true;
bool debugInfo = false;
std::map<size_t, std::string> inputSourceMapFilenames;
std::string outputSourceMapFilename;
std::string outputSourceMapUrl;
const std::string WasmMergeOption = "wasm-merge options";
ToolOptions options("wasm-merge",
R"(Merge wasm files into one.
For example,
wasm-merge foo.wasm foo bar.wasm bar -o merged.wasm
will read foo.wasm and bar.wasm, with names 'foo' and 'bar' respectively, so if the second imports from 'foo', we will see that as an import from the first module after the merge. The merged output will be written to merged.wasm.
Note that filenames and modules names are interleaved (which is hopefully less confusing).
Input source maps can be specified by adding an -ism option right after the module name:
wasm-merge foo.wasm foo -ism foo.wasm.map ...)");
options
.add("--output",
"-o",
"Output file (stdout if not specified)",
WasmMergeOption,
Options::Arguments::One,
[](Options* o, const std::string& argument) {
o->extra["output"] = argument;
Colors::setEnabled(false);
})
.add_positional("INFILE1 NAME1 INFILE2 NAME2 [..]",
Options::Arguments::N,
[&](Options* o, const std::string& argument) {
if (inputFiles.size() == inputFileNames.size()) {
inputFiles.push_back(argument);
} else {
inputFileNames.push_back(argument);
}
})
.add("--input-source-map",
"-ism",
"Consume source maps from the specified files",
WasmMergeOption,
Options::Arguments::N,
[&](Options* o, const std::string& argument) {
size_t pos = inputFiles.size();
if (pos == 0 || pos != inputFileNames.size() ||
inputSourceMapFilenames.count(pos - 1)) {
std::cerr << "Option '-ism " << argument
<< "' should be right after the module name\n";
exit(EXIT_FAILURE);
}
inputSourceMapFilenames.insert({pos - 1, argument});
})
.add("--output-source-map",
"-osm",
"Emit source map to the specified file",
WasmMergeOption,
Options::Arguments::One,
[&outputSourceMapFilename](Options* o, const std::string& argument) {
outputSourceMapFilename = argument;
})
.add("--output-source-map-url",
"-osu",
"Emit specified string as source map URL",
WasmMergeOption,
Options::Arguments::One,
[&outputSourceMapUrl](Options* o, const std::string& argument) {
outputSourceMapUrl = argument;
})
.add("--rename-export-conflicts",
"-rec",
"Rename exports to avoid conflicts (rather than error)",
WasmMergeOption,
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) {
exportMergeMode = RenameExportConflicts;
})
.add("--skip-export-conflicts",
"-sec",
"Skip exports that conflict with previous ones",
WasmMergeOption,
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) {
exportMergeMode = SkipExportConflicts;
})
.add("--emit-text",
"-S",
"Emit text instead of binary for the output file",
WasmMergeOption,
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) { emitBinary = false; })
.add("--debuginfo",
"-g",
"Emit names section and debug info",
WasmMergeOption,
Options::Arguments::Zero,
[&](Options* o, const std::string& arguments) { debugInfo = true; });
options.parse(argc, argv);
if (inputFiles.size() != inputFileNames.size()) {
Fatal() << "Please provide an import name for each input file. "
"In particular, the number of positional inputs must be even as "
"each wasm binary must be followed by its name.";
}
for (Index i = 0; i < inputFiles.size(); i++) {
auto inputFile = inputFiles[i];
auto inputFileName = inputFileNames[i];
auto iter = inputSourceMapFilenames.find(i);
auto inputSourceMapFilename =
(iter == inputSourceMapFilenames.end()) ? "" : iter->second;
if (options.debug) {
std::cerr << "reading input '" << inputFile << "' as '" << inputFileName
<< "'...\n";
}
std::unique_ptr<Module> laterInput;
Module* currModule;
if (i == 0) {
currModule = &merged;
} else {
laterInput = std::make_unique<Module>();
currModule = laterInput.get();
}
options.applyFeatures(*currModule);
ModuleReader reader;
try {
reader.read(inputFile, *currModule, inputSourceMapFilename);
} catch (ParseException& p) {
p.dump(std::cerr);
Fatal() << "error in parsing wasm input: " << inputFile;
}
if (options.passOptions.validate) {
if (!WasmValidator().validate(*currModule)) {
std::cout << *currModule << '\n';
Fatal() << "error in validating input: " << inputFile;
}
}
if (!laterInput) {
for (auto& curr : merged.exports) {
exportModuleMap[curr.get()] = ExportInfo{inputFileName, curr->name};
}
} else {
mergeInto(*currModule, inputFileName);
if (options.passOptions.validate) {
if (!WasmValidator().validate(merged)) {
std::cout << merged << '\n';
Fatal() << "error in validating merged after: " << inputFile;
}
}
}
}
fuseImportsAndExports();
{
PassRunner passRunner(&merged);
passRunner.add("reorder-globals-always");
passRunner.add("remove-unused-module-elements");
passRunner.run();
}
if (options.extra.count("output") > 0) {
ModuleWriter writer;
writer.setBinary(emitBinary);
writer.setDebugInfo(debugInfo);
if (outputSourceMapFilename.size()) {
writer.setSourceMapFilename(outputSourceMapFilename);
writer.setSourceMapUrl(outputSourceMapUrl);
}
writer.write(merged, options.extra["output"]);
}
}