515 lines
12 KiB
JavaScript
Executable file
515 lines
12 KiB
JavaScript
Executable file
/*
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
Author Tobias Koppers @sokra
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const { RawSource } = require("webpack-sources");
|
|
const Generator = require("../Generator");
|
|
const WebAssemblyUtils = require("./WebAssemblyUtils");
|
|
|
|
const t = require("@webassemblyjs/ast");
|
|
const { moduleContextFromModuleAST } = require("@webassemblyjs/ast");
|
|
const { editWithAST, addWithAST } = require("@webassemblyjs/wasm-edit");
|
|
const { decode } = require("@webassemblyjs/wasm-parser");
|
|
|
|
const WebAssemblyExportImportedDependency = require("../dependencies/WebAssemblyExportImportedDependency");
|
|
|
|
/** @typedef {import("webpack-sources").Source} Source */
|
|
/** @typedef {import("../DependencyTemplates")} DependencyTemplates */
|
|
/** @typedef {import("../Generator").GenerateContext} GenerateContext */
|
|
/** @typedef {import("../Module")} Module */
|
|
/** @typedef {import("../ModuleGraph")} ModuleGraph */
|
|
/** @typedef {import("../NormalModule")} NormalModule */
|
|
/** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */
|
|
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
|
|
/** @typedef {import("./WebAssemblyUtils").UsedWasmDependency} UsedWasmDependency */
|
|
|
|
/**
|
|
* @typedef {(buf: ArrayBuffer) => ArrayBuffer} ArrayBufferTransform
|
|
*/
|
|
|
|
/**
|
|
* @template T
|
|
* @param {Function[]} fns transforms
|
|
* @returns {Function} composed transform
|
|
*/
|
|
const compose = (...fns) => {
|
|
return fns.reduce(
|
|
(prevFn, nextFn) => {
|
|
return value => nextFn(prevFn(value));
|
|
},
|
|
value => value
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Removes the start instruction
|
|
*
|
|
* @param {Object} state unused state
|
|
* @returns {ArrayBufferTransform} transform
|
|
*/
|
|
const removeStartFunc = state => bin => {
|
|
return editWithAST(state.ast, bin, {
|
|
Start(path) {
|
|
path.remove();
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get imported globals
|
|
*
|
|
* @param {Object} ast Module's AST
|
|
* @returns {t.ModuleImport[]} - nodes
|
|
*/
|
|
const getImportedGlobals = ast => {
|
|
/** @type {t.ModuleImport[]} */
|
|
const importedGlobals = [];
|
|
|
|
t.traverse(ast, {
|
|
ModuleImport({ node }) {
|
|
if (t.isGlobalType(node.descr)) {
|
|
importedGlobals.push(node);
|
|
}
|
|
}
|
|
});
|
|
|
|
return importedGlobals;
|
|
};
|
|
|
|
/**
|
|
* Get the count for imported func
|
|
*
|
|
* @param {Object} ast Module's AST
|
|
* @returns {Number} - count
|
|
*/
|
|
const getCountImportedFunc = ast => {
|
|
let count = 0;
|
|
|
|
t.traverse(ast, {
|
|
ModuleImport({ node }) {
|
|
if (t.isFuncImportDescr(node.descr)) {
|
|
count++;
|
|
}
|
|
}
|
|
});
|
|
|
|
return count;
|
|
};
|
|
|
|
/**
|
|
* Get next type index
|
|
*
|
|
* @param {Object} ast Module's AST
|
|
* @returns {t.Index} - index
|
|
*/
|
|
const getNextTypeIndex = ast => {
|
|
const typeSectionMetadata = t.getSectionMetadata(ast, "type");
|
|
|
|
if (typeSectionMetadata === undefined) {
|
|
return t.indexLiteral(0);
|
|
}
|
|
|
|
return t.indexLiteral(typeSectionMetadata.vectorOfSize.value);
|
|
};
|
|
|
|
/**
|
|
* Get next func index
|
|
*
|
|
* The Func section metadata provide informations for implemented funcs
|
|
* in order to have the correct index we shift the index by number of external
|
|
* functions.
|
|
*
|
|
* @param {Object} ast Module's AST
|
|
* @param {Number} countImportedFunc number of imported funcs
|
|
* @returns {t.Index} - index
|
|
*/
|
|
const getNextFuncIndex = (ast, countImportedFunc) => {
|
|
const funcSectionMetadata = t.getSectionMetadata(ast, "func");
|
|
|
|
if (funcSectionMetadata === undefined) {
|
|
return t.indexLiteral(0 + countImportedFunc);
|
|
}
|
|
|
|
const vectorOfSize = funcSectionMetadata.vectorOfSize.value;
|
|
|
|
return t.indexLiteral(vectorOfSize + countImportedFunc);
|
|
};
|
|
|
|
/**
|
|
* Creates an init instruction for a global type
|
|
* @param {t.GlobalType} globalType the global type
|
|
* @returns {t.Instruction} init expression
|
|
*/
|
|
const createDefaultInitForGlobal = globalType => {
|
|
if (globalType.valtype[0] === "i") {
|
|
// create NumberLiteral global initializer
|
|
return t.objectInstruction("const", globalType.valtype, [
|
|
t.numberLiteralFromRaw(66)
|
|
]);
|
|
} else if (globalType.valtype[0] === "f") {
|
|
// create FloatLiteral global initializer
|
|
return t.objectInstruction("const", globalType.valtype, [
|
|
t.floatLiteral(66, false, false, "66")
|
|
]);
|
|
} else {
|
|
throw new Error("unknown type: " + globalType.valtype);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Rewrite the import globals:
|
|
* - removes the ModuleImport instruction
|
|
* - injects at the same offset a mutable global of the same type
|
|
*
|
|
* Since the imported globals are before the other global declarations, our
|
|
* indices will be preserved.
|
|
*
|
|
* Note that globals will become mutable.
|
|
*
|
|
* @param {Object} state unused state
|
|
* @returns {ArrayBufferTransform} transform
|
|
*/
|
|
const rewriteImportedGlobals = state => bin => {
|
|
const additionalInitCode = state.additionalInitCode;
|
|
/** @type {Array<t.Global>} */
|
|
const newGlobals = [];
|
|
|
|
bin = editWithAST(state.ast, bin, {
|
|
ModuleImport(path) {
|
|
if (t.isGlobalType(path.node.descr)) {
|
|
const globalType = path.node.descr;
|
|
|
|
globalType.mutability = "var";
|
|
|
|
const init = [
|
|
createDefaultInitForGlobal(globalType),
|
|
t.instruction("end")
|
|
];
|
|
|
|
newGlobals.push(t.global(globalType, init));
|
|
|
|
path.remove();
|
|
}
|
|
},
|
|
|
|
// in order to preserve non-imported global's order we need to re-inject
|
|
// those as well
|
|
Global(path) {
|
|
const { node } = path;
|
|
const [init] = node.init;
|
|
|
|
if (init.id === "get_global") {
|
|
node.globalType.mutability = "var";
|
|
|
|
const initialGlobalIdx = init.args[0];
|
|
|
|
node.init = [
|
|
createDefaultInitForGlobal(node.globalType),
|
|
t.instruction("end")
|
|
];
|
|
|
|
additionalInitCode.push(
|
|
/**
|
|
* get_global in global initializer only works for imported globals.
|
|
* They have the same indices as the init params, so use the
|
|
* same index.
|
|
*/
|
|
t.instruction("get_local", [initialGlobalIdx]),
|
|
t.instruction("set_global", [t.indexLiteral(newGlobals.length)])
|
|
);
|
|
}
|
|
|
|
newGlobals.push(node);
|
|
|
|
path.remove();
|
|
}
|
|
});
|
|
|
|
// Add global declaration instructions
|
|
return addWithAST(state.ast, bin, newGlobals);
|
|
};
|
|
|
|
/**
|
|
* Rewrite the export names
|
|
* @param {Object} state state
|
|
* @param {Object} state.ast Module's ast
|
|
* @param {Module} state.module Module
|
|
* @param {ModuleGraph} state.moduleGraph module graph
|
|
* @param {Set<string>} state.externalExports Module
|
|
* @param {RuntimeSpec} state.runtime runtime
|
|
* @returns {ArrayBufferTransform} transform
|
|
*/
|
|
const rewriteExportNames =
|
|
({ ast, moduleGraph, module, externalExports, runtime }) =>
|
|
bin => {
|
|
return editWithAST(ast, bin, {
|
|
ModuleExport(path) {
|
|
const isExternal = externalExports.has(path.node.name);
|
|
if (isExternal) {
|
|
path.remove();
|
|
return;
|
|
}
|
|
const usedName = moduleGraph
|
|
.getExportsInfo(module)
|
|
.getUsedName(path.node.name, runtime);
|
|
if (!usedName) {
|
|
path.remove();
|
|
return;
|
|
}
|
|
path.node.name = usedName;
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Mangle import names and modules
|
|
* @param {Object} state state
|
|
* @param {Object} state.ast Module's ast
|
|
* @param {Map<string, UsedWasmDependency>} state.usedDependencyMap mappings to mangle names
|
|
* @returns {ArrayBufferTransform} transform
|
|
*/
|
|
const rewriteImports =
|
|
({ ast, usedDependencyMap }) =>
|
|
bin => {
|
|
return editWithAST(ast, bin, {
|
|
ModuleImport(path) {
|
|
const result = usedDependencyMap.get(
|
|
path.node.module + ":" + path.node.name
|
|
);
|
|
|
|
if (result !== undefined) {
|
|
path.node.module = result.module;
|
|
path.node.name = result.name;
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Add an init function.
|
|
*
|
|
* The init function fills the globals given input arguments.
|
|
*
|
|
* @param {Object} state transformation state
|
|
* @param {Object} state.ast Module's ast
|
|
* @param {t.Identifier} state.initFuncId identifier of the init function
|
|
* @param {t.Index} state.startAtFuncOffset index of the start function
|
|
* @param {t.ModuleImport[]} state.importedGlobals list of imported globals
|
|
* @param {t.Instruction[]} state.additionalInitCode list of addition instructions for the init function
|
|
* @param {t.Index} state.nextFuncIndex index of the next function
|
|
* @param {t.Index} state.nextTypeIndex index of the next type
|
|
* @returns {ArrayBufferTransform} transform
|
|
*/
|
|
const addInitFunction =
|
|
({
|
|
ast,
|
|
initFuncId,
|
|
startAtFuncOffset,
|
|
importedGlobals,
|
|
additionalInitCode,
|
|
nextFuncIndex,
|
|
nextTypeIndex
|
|
}) =>
|
|
bin => {
|
|
const funcParams = importedGlobals.map(importedGlobal => {
|
|
// used for debugging
|
|
const id = t.identifier(
|
|
`${importedGlobal.module}.${importedGlobal.name}`
|
|
);
|
|
|
|
return t.funcParam(
|
|
/** @type {string} */ (importedGlobal.descr.valtype),
|
|
id
|
|
);
|
|
});
|
|
|
|
const funcBody = [];
|
|
importedGlobals.forEach((importedGlobal, index) => {
|
|
const args = [t.indexLiteral(index)];
|
|
const body = [
|
|
t.instruction("get_local", args),
|
|
t.instruction("set_global", args)
|
|
];
|
|
|
|
funcBody.push(...body);
|
|
});
|
|
|
|
if (typeof startAtFuncOffset === "number") {
|
|
funcBody.push(
|
|
t.callInstruction(t.numberLiteralFromRaw(startAtFuncOffset))
|
|
);
|
|
}
|
|
|
|
for (const instr of additionalInitCode) {
|
|
funcBody.push(instr);
|
|
}
|
|
|
|
funcBody.push(t.instruction("end"));
|
|
|
|
/** @type {string[]} */
|
|
const funcResults = [];
|
|
|
|
// Code section
|
|
const funcSignature = t.signature(funcParams, funcResults);
|
|
const func = t.func(initFuncId, funcSignature, funcBody);
|
|
|
|
// Type section
|
|
const functype = t.typeInstruction(undefined, funcSignature);
|
|
|
|
// Func section
|
|
const funcindex = t.indexInFuncSection(nextTypeIndex);
|
|
|
|
// Export section
|
|
const moduleExport = t.moduleExport(
|
|
initFuncId.value,
|
|
t.moduleExportDescr("Func", nextFuncIndex)
|
|
);
|
|
|
|
return addWithAST(ast, bin, [func, moduleExport, funcindex, functype]);
|
|
};
|
|
|
|
/**
|
|
* Extract mangle mappings from module
|
|
* @param {ModuleGraph} moduleGraph module graph
|
|
* @param {Module} module current module
|
|
* @param {boolean | undefined} mangle mangle imports
|
|
* @returns {Map<string, UsedWasmDependency>} mappings to mangled names
|
|
*/
|
|
const getUsedDependencyMap = (moduleGraph, module, mangle) => {
|
|
/** @type {Map<string, UsedWasmDependency>} */
|
|
const map = new Map();
|
|
for (const usedDep of WebAssemblyUtils.getUsedDependencies(
|
|
moduleGraph,
|
|
module,
|
|
mangle
|
|
)) {
|
|
const dep = usedDep.dependency;
|
|
const request = dep.request;
|
|
const exportName = dep.name;
|
|
map.set(request + ":" + exportName, usedDep);
|
|
}
|
|
return map;
|
|
};
|
|
|
|
const TYPES = new Set(["webassembly"]);
|
|
|
|
/**
|
|
* @typedef {Object} WebAssemblyGeneratorOptions
|
|
* @property {boolean} [mangleImports] mangle imports
|
|
*/
|
|
|
|
class WebAssemblyGenerator extends Generator {
|
|
/**
|
|
* @param {WebAssemblyGeneratorOptions} options options
|
|
*/
|
|
constructor(options) {
|
|
super();
|
|
this.options = options;
|
|
}
|
|
|
|
/**
|
|
* @param {NormalModule} module fresh module
|
|
* @returns {Set<string>} available types (do not mutate)
|
|
*/
|
|
getTypes(module) {
|
|
return TYPES;
|
|
}
|
|
|
|
/**
|
|
* @param {NormalModule} module the module
|
|
* @param {string=} type source type
|
|
* @returns {number} estimate size of the module
|
|
*/
|
|
getSize(module, type) {
|
|
const originalSource = module.originalSource();
|
|
if (!originalSource) {
|
|
return 0;
|
|
}
|
|
return originalSource.size();
|
|
}
|
|
|
|
/**
|
|
* @param {NormalModule} module module for which the code should be generated
|
|
* @param {GenerateContext} generateContext context for generate
|
|
* @returns {Source} generated code
|
|
*/
|
|
generate(module, { moduleGraph, runtime }) {
|
|
const bin = /** @type {Source} */ (module.originalSource()).source();
|
|
|
|
const initFuncId = t.identifier("");
|
|
|
|
// parse it
|
|
const ast = decode(bin, {
|
|
ignoreDataSection: true,
|
|
ignoreCodeSection: true,
|
|
ignoreCustomNameSection: true
|
|
});
|
|
|
|
const moduleContext = moduleContextFromModuleAST(ast.body[0]);
|
|
|
|
const importedGlobals = getImportedGlobals(ast);
|
|
const countImportedFunc = getCountImportedFunc(ast);
|
|
const startAtFuncOffset = moduleContext.getStart();
|
|
const nextFuncIndex = getNextFuncIndex(ast, countImportedFunc);
|
|
const nextTypeIndex = getNextTypeIndex(ast);
|
|
|
|
const usedDependencyMap = getUsedDependencyMap(
|
|
moduleGraph,
|
|
module,
|
|
this.options.mangleImports
|
|
);
|
|
const externalExports = new Set(
|
|
module.dependencies
|
|
.filter(d => d instanceof WebAssemblyExportImportedDependency)
|
|
.map(d => {
|
|
const wasmDep = /** @type {WebAssemblyExportImportedDependency} */ (
|
|
d
|
|
);
|
|
return wasmDep.exportName;
|
|
})
|
|
);
|
|
|
|
/** @type {t.Instruction[]} */
|
|
const additionalInitCode = [];
|
|
|
|
const transform = compose(
|
|
rewriteExportNames({
|
|
ast,
|
|
moduleGraph,
|
|
module,
|
|
externalExports,
|
|
runtime
|
|
}),
|
|
|
|
removeStartFunc({ ast }),
|
|
|
|
rewriteImportedGlobals({ ast, additionalInitCode }),
|
|
|
|
rewriteImports({
|
|
ast,
|
|
usedDependencyMap
|
|
}),
|
|
|
|
addInitFunction({
|
|
ast,
|
|
initFuncId,
|
|
importedGlobals,
|
|
additionalInitCode,
|
|
startAtFuncOffset,
|
|
nextFuncIndex,
|
|
nextTypeIndex
|
|
})
|
|
);
|
|
|
|
const newBin = transform(bin);
|
|
|
|
const newBuf = Buffer.from(newBin);
|
|
|
|
return new RawSource(newBuf);
|
|
}
|
|
}
|
|
|
|
module.exports = WebAssemblyGenerator;
|