/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const Generator = require("../Generator"); const Template = require("../Template"); const WebAssemblyUtils = require("./WebAssemblyUtils"); const { RawSource } = require("webpack-sources"); const { editWithAST, addWithAST } = require("@webassemblyjs/wasm-edit"); const { decode } = require("@webassemblyjs/wasm-parser"); const t = require("@webassemblyjs/ast"); const { moduleContextFromModuleAST } = require("@webassemblyjs/helper-module-context"); const WebAssemblyExportImportedDependency = require("../dependencies/WebAssemblyExportImportedDependency"); /** @typedef {import("../Module")} Module */ /** @typedef {import("./WebAssemblyUtils").UsedWasmDependency} UsedWasmDependency */ /** @typedef {import("../NormalModule")} NormalModule */ /** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */ /** @typedef {import("webpack-sources").Source} Source */ /** @typedef {import("../Dependency").DependencyTemplate} DependencyTemplate */ /** * @typedef {(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 ); }; // TODO replace with @callback /** * 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 {Array<t.ModuleImport>} - nodes */ const getImportedGlobals = ast => { 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; 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 {Set<string>} state.externalExports Module * @returns {ArrayBufferTransform} transform */ const rewriteExportNames = ({ ast, module, externalExports }) => bin => { return editWithAST(ast, bin, { ModuleExport(path) { const isExternal = externalExports.has(path.node.name); if (isExternal) { path.remove(); return; } const usedName = module.isUsed(path.node.name); 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(importedGlobal.descr.valtype, id); }); const funcBody = importedGlobals.reduce((acc, importedGlobal, index) => { const args = [t.indexLiteral(index)]; const body = [ t.instruction("get_local", args), t.instruction("set_global", args) ]; return [...acc, ...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")); 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 {Module} module current module * @param {boolean} mangle mangle imports * @returns {Map<string, UsedWasmDependency>} mappings to mangled names */ const getUsedDependencyMap = (module, mangle) => { /** @type {Map<string, UsedWasmDependency>} */ const map = new Map(); for (const usedDep of WebAssemblyUtils.getUsedDependencies(module, mangle)) { const dep = usedDep.dependency; const request = dep.request; const exportName = dep.name; map.set(request + ":" + exportName, usedDep); } return map; }; class WebAssemblyGenerator extends Generator { constructor(options) { super(); this.options = options; } /** * @param {NormalModule} module module for which the code should be generated * @param {Map<Function, DependencyTemplate>} dependencyTemplates mapping from dependencies to templates * @param {RuntimeTemplate} runtimeTemplate the runtime template * @param {string} type which kind of code should be generated * @returns {Source} generated code */ generate(module, dependencyTemplates, runtimeTemplate, type) { let bin = module.originalSource().source(); const initFuncId = t.identifier( Array.isArray(module.usedExports) ? Template.numberToIdentifer(module.usedExports.length) : "__webpack_init__" ); // 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( 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, module, externalExports }), removeStartFunc({ ast }), rewriteImportedGlobals({ ast, additionalInitCode }), rewriteImports({ ast, usedDependencyMap }), addInitFunction({ ast, initFuncId, importedGlobals, additionalInitCode, startAtFuncOffset, nextFuncIndex, nextTypeIndex }) ); const newBin = transform(bin); return new RawSource(newBin); } } module.exports = WebAssemblyGenerator;