尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

在 webpack 中打包 Emscripten 编译的 WebAssembly:通过 source-phase import(`import source`)把实例化职责交还给胶水层

在 webpack 中打包 Emscripten 编译的 WebAssembly:通过 source-phase import(`import source`)把实例化职责交还给胶水层 在 webpack 中打包 Emscripten 编译的 WebAssembly通过 source-phase importimport source把实例化职责交还给胶水层【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack本文以 webpack 官方仓库中的 wasm-emscripten 示例 为核心讲解当 WebAssembly 由 Emscripten或其他 C/C/Rust 工具链编译并带有 JS “胶水模块”时如何正确接入 webpack 的打包流水线。读者将理解为什么这类.wasm不能走默认的webassembly/async实例化路径掌握experiments.sourceImportsource-phase import即import source的配置、instantiateWasm钩子契约以及 webpack 从解析、代码生成到运行时“只编译不实例化”的完整原理与产物形态。问题背景为什么 Emscripten 产物会报export default ... was not foundEmscripten以及大量 C/C/Rust 工具链的产物并不只是一份裸.wasm二进制而是一份“.wasm二进制 一份 JavaScript 胶水模块”。胶水模块承担了实例化的全部职责它负责构建 import object即 wasm 模块声明导入的宿主函数与内存例如把 JS 回调塞进env命名空间它负责提供并设置线性内存它负责执行 C/C 的全局构造器constructors它负责在实例化完成后读回导出的函数封装成面向用户的 API。也就是说只有胶水模块知道如何构造 import object。如果让 webpack 走常规的type: webassembly/async实例化路径webpack 会自己 fetch、compile 并instantiate这个 wasm然后试图把 wasm 的导出暴露给消费者——这恰恰不是胶水所期望的形态。示例文档指出此路径下会以export default ... was not found之类的报错失败webpack 实例化后暴露的是 wasm 的“原始导出”而胶水期望拿到一个待它自己实例化的模块。示例中的 program.wat 很能说明问题——模块顶层声明了外部导入(module ;; Imported from the host (glue): webpack cannot provide this, which is ;; why the .wasm must be instantiated by the runtime, not by webpack. (import env log (func $log (param i32))) ...这里的(import env log ...)依赖一个只有胶水才知道如何注入的宿主函数webpack 无从得知其实现因此webpack 必须放弃对实例化环节的接管。解决方案source-phase importimport source——webpack 负责编译胶水负责实例化示例给出的修正是 WebAssembly 的source-phase import语法形如import source programWasm from ./program.wasm;在这一模式下webpack 依然把.wasm当作一等公民的异步 WebAssembly 模块来对待——它照常被fetch、compile、参与 content-hash、具备 code splitting 能力——但流水线止步于“编译”阶段把WebAssembly.Module交到消费者手里。之后由 Emscripten 胶水通过其官方提供的instantiateWasm逃逸口完成实例化。这种方式带来的额外收益是完全不需要asset/resource把它们当静态文件拷走不需要 Emscripten 侧的locateFile配置不需要resolve.fallback: { fs: false }这类对 Node 内置模块的兜底也不需要用copy-webpack-plugin手动同步产物。示例文档特别说明emscripten-module.js只是一个小巧的替身stand-in用于镜像真实 Emscripten 在-sMODULARIZE -sEXPORT_ES6模式下产物的契约一个默认导出的工厂函数且尊重Module.instantiateWasm。真实胶水可以直接原样替换进来无需任何改动。仓库中的完整示例文件清单与构建方式本示例位于 examples/wasm-emscripten包含以下文件文件作用example.js应用入口import source引入 wasm调用胶水工厂并注入instantiateWasmemscripten-module.jsEmscripten 胶水的微型替身实现工厂 instantiateWasm契约program.watwasm 文本格式源码导入env.log导出斐波那契runprogram.wasm对应的已编译二进制约 96 字节webpack.config.js构建配置index.html浏览器入口页面加载dist/output.jstest.filter.js示例测试的过滤器仅在宿主支持 WebAssembly 时运行template.mdREADME 的生成模板_{example.js}_等占位符被真实文件内容与构建统计替换这是一类“运行并校验”风格的示例其 README.md 由模板与真实构建输出合并而成文档中Info一节的产物统计即来自真实的 dev/production 构建因此dist/目录并不入库。需要在本机复现时可参考 examples/README.md 中 “Building an Example” 一节的流程在仓库根目录执行yarn、yarn setup然后在示例目录内执行node build.js或在根目录执行npm run build:examples由 examples/buildAll.js 逐个目录驱动构建。浏览器场景则用任意静态服务器把index.html与dist/一起托管后访问即可。配置解读webpack.config.js示例的 webpack.config.js 非常精简但每一行都关键use strict; /** type {import(webpack).Configuration} */ const config { // mode: development || production, module: { rules: [ { test: /\.wasm$/, type: webassembly/async } ] }, experiments: { // import source for WebAssembly: compile (not instantiate) the module. asyncWebAssembly: true, sourceImport: true }, optimization: { chunkIds: deterministic // keep filenames stable between modes } }; module.exports config;逐项拆解module.rulestest: /\.wasm$/type: webassembly/async—— 让 webpack 把.wasm当作异步 WebAssembly 模块处理放入独立的 chunk/asset、异步加载。在 source-phase 模式下它依然沿用该类型只是执行到“编译”而非“实例化”。experiments.asyncWebAssembly: true—— 启用异步 WebAssembly 支持旧版syncWebAssembly已不在本仓库的推荐路径上这是处理现代 wasm 集成的前置开关。experiments.sourceImport: true—— 关键开关开启 source phase imports允许解析import source m from ...这种语法。与之对应的环境能力在配置校验 schema schemas/WebpackOptions.json 中被标记为experimental其能力描述为 “The environment supports source phase imports (import source m from ..., import.source(...))”——即这是实验性特性语义可能随版本演进对应 schema 条目标注added为 5.110.0 之后的实验位使用时请留意所选 webpack 版本的支持情况。optimization.chunkIds: deterministic—— 让模块 id / chunk 命名在 development 与 production 之间保持一致保证两种模式产物便于对比示例 README 的 Info 段落正因如此才具有可比性。应用侧代码import sourceinstantiateWasm钩子example.js 是理解整套协作的关键import source programWasm from ./program.wasm; import createModule from ./emscripten-module; // webpack fetches and compiles program.wasm through its async WebAssembly // pipeline (content-hashed, code-split-capable) and hands us the compiled // WebAssembly.Module. The glue then instantiates it, supplying the imports // webpack cannot know about. createModule({ onLog: (value) console.log(wasm logged:, value), instantiateWasm(imports, receiveInstance) { WebAssembly.instantiate(programWasm, imports).then((instance) receiveInstance(instance, programWasm) ); return {}; // signal that instantiation happens asynchronously } }).then((Module) { console.log(run(10) , Module.run(10)); });这里有三层契约import source programWasmprogramWasm不是实例、也不是字节数组而是一个已经编译好的WebAssembly.Module。webpack 保证它已被正确 fetch 与编译并参与 content-hash 与 code splitting。胶水工厂的参数对象调用createModule(...)时传入onLog回调与instantiateWasm钩子。instantiateWasm收到胶水构造好的imports与receiveInstance回调应用侧在此调用标准WebAssembly.instantiate(programWasm, imports)完成真正的实例化。返回{}表示异步进行中instantiateWasm需要返回值以告知胶水“实例化是异步的完成后会通过receiveInstance回传”因此示例显式return {};。一旦实例化完成receiveInstance(instance, module)被调用随后.then((Module) ...)即可使用Module.run(10)。胶水模块emscripten-module.js与真实产物契约emscripten-module.js 完整再现了 Emscripten-sMODULARIZE -sEXPORT_ES6输出必须满足的打包契约// Minimal stand-in for the JS glue Emscripten emits with // -sMODULARIZE -sEXPORT_ES6. Real glue is large and minified, but the // contract a bundler must satisfy is small: a default-exported factory that // owns wasm instantiation and honors the instantiateWasm escape hatch. export default function createModule(moduleArg {}) { const Module moduleArg; // The import object the wasm needs. Only the glue knows how to build it, // which is why webpack cannot instantiate the module itself. const imports { env: { log(value) { if (Module.onLog) Module.onLog(value); } } }; return new Promise((resolve, reject) { const receiveInstance (instance) { Module.run (n) instance.exports.run(n); resolve(Module); }; // Emscriptens official hook: hand instantiation to the embedder. if (Module.instantiateWasm) { Module.instantiateWasm(imports, receiveInstance); return; } reject(new Error(This minimal glue requires an instantiateWasm hook)); }); }要点默认导出一个工厂函数createModule(moduleArg)返回PromiseModule。真实 Emscripten 胶水体量庞大且被压缩但面向打包器必须满足的“表面契约”只有这么小——这也是本示例能用替身演示、且真实胶水可无改动替换的原因。import object 由胶水构造示例中 wasm 声明(import env log ...)胶水便构造{ env: { log } }并通过instantiateWasm交给宿主。这正是“webpack 无法实例化该模块”的根因——import object 的知识只存在于胶水内部。instantiateWasm是 Emscripten 官方逃逸口若调用方传入该钩子胶水把imports与receiveInstance直接转发由宿主决定何时WebAssembly.instantiate否则替身直接reject真实胶水在无钩子时会自行走默认的加载/实例化路径。源码级原理webpack 如何实现“只编译不实例化”示例 README 给出的打包产物并非凭空生成而是 webpack 源码中 source phase 路径的真实输出。仓库 lib/wasm-async 下的实现可以相互印证。解析阶段跳过完整解码只暴露 default 导出AsyncWebAssemblyParser.js 的parse会先检查模块的phase。当 phase 为source时对应源码第 60~75 行附近解析器不再对 wasm 二进制做完整解码不再遍历模块的 import/export 表而是只校验 wasm magic header\0asm非法模块直接抛错通过StaticExportsDependency([default])声明该模块只有default导出提前返回。这一步很关键因为不需要知道 wasm 的导入表/导出表webpack 也就不需要、也无法为它拼装 import object——这与“实例化交给胶水”的分工完全自洽。代码生成阶段生成 compile 调用而非 instantiate 调用AsyncWebAssemblyJavascriptGenerator.js 的generate检测到module.phase source时会转入专门的_generateSourcePhase对应源码 237~295 行附近其产物可概括为// Source phase: export default WebAssembly.Module (via compileWasm) var __webpack_wasm_module__ await __webpack_require__.vs(moduleId, hash); __webpack_require__.d(exports, { default: () (__webpack_wasm_module__) });它通过 async module 包装RuntimeGlobals.asyncModule等待__webpack_require__.vs(...)即编译运行时返回的WebAssembly.Module再用definePropertyGetters把default指向它。对比非 source-phase 的普通路径生成instantiateWasm运行时并暴露实例导出这正是“编译”与“实例化”在代码生成层的分水岭。运行时阶段fetch compileStreaming产出WebAssembly.Module示例 README 中dist/output.js的 “webpack/runtime/wasm compile” 运行时即__webpack_require__.vs展示了加载行为对应运行时模块 AsyncWasmCompileRuntimeModule.js 与 AsyncWasmLoadingRuntimeModule.js__webpack_require__.vs (wasmModuleId, wasmModuleHash) { var req fetch(dist/ wasmModuleHash .module.wasm); var fallback () req .then((x) x.arrayBuffer()) .then((bytes) WebAssembly.compile(bytes)); return req.then((res) { if (typeof WebAssembly.compileStreaming function) { return WebAssembly.compileStreaming(res) .catch((e) { if (res.headers.get(Content-Type) ! application/wasm) { // MIME 不正确时回退到 WebAssembly.compile较慢 return fallback(); } throw e; }); } return fallback(); }); };注意它调用的是WebAssembly.compile/WebAssembly.compileStreaming返回WebAssembly.Module全程没有instantiate。只要服务器以application/wasmMIME 提供服务就可用流式编译否则控制台会打印 “WebAssembly.compileStreamingfailed because your server does not serve wasm withapplication/wasmMIME type. Falling back toWebAssembly.compilewhich is slower.” 并自动回退。模块化与内容寻址从示例构建统计可以看到 wasm 被作为一个独立辅助资产产出asset output.js 10.7 KiB [emitted] (name: main) asset f052564a523e50ee50a2.module.wasm 96 bytes [emitted] [immutable] (auxiliary name: main).wasm以其内容哈希命名*.module.wasm并被标记为immutable天然适合长缓存同时作为依赖模块被异步加载意味着它同样可以进入 code splitting 的协作关系“auxiliary name: main”表明它与主 chunk 关联。生产构建中产物被压缩为 2.29 KiB运行时模块从 5 个收敛到 4 个见下文 Info 对比但辅助 wasm 资产形态与职责不变。打包产物解读dist/output.js里发生了什么README 中dist/output.js是真实的生成结果切片可以清晰读出三个模块的协作模块 0./example.js入口整个入口被包进__webpack_require__.a(module, async (...) {...}, 1)异步模块包装器内部先__webpack_handle_async_dependencies__等待 wasm 依赖再调用胶水工厂并传入instantiateWasm。模块 1./program.wasm导出非常简洁——通过__webpack_require__.vs(module.id, f052564a523e50ee50a2)拿到编译好的模块并注册为defaultvar __webpack_wasm_module__ await __webpack_require__.vs(module.id, f052564a523e50ee50a2); __webpack_require__.d(exports, { default: () (__webpack_wasm_module__) });模块 2./emscripten-module.js胶水工厂原样进入 bundle注释中明示“webpack cannot instantiate the module itself”。Info开发模式与生产模式对比文档末尾给出两种模式的真实构建统计Unoptimizeddevelopmentoutput.js10.7 KiB辅助 wasmf052564a523e50ee50a2.module.wasm96 字节immutableruntime 模块 5 个3.31 KiB。Production modeoutput.js压缩到 2.29 KiB辅助 wasm 为f5155e54cc54c8650d10.module.wasm仍 96 字节、immutableruntime 模块收敛为 4 个3.1 KiB。两者的 wasm 资产哈希不同是因为 development/production 下 webpack 内部渲染与哈希输入存在差异——这正是optimization.chunkIds: deterministic的意义让除哈希外的模块组织保持稳定便于在两种模式下对照验证示例用模板文件 template.md 以_{stdout}_、_{production:stdout}_占位符分别注入两种构建的输出。常见误区与排查建议结合示例 README 的“反面清单”与源码行为实际工程中容易踩的坑包括沿用普通webassembly/async实例化语义当.wasm附带自实例化胶水时应使用import source 手动instantiateWasm否则会得到export default ... was not found因为 webpack 暴露的是 wasm 原始导出而不是胶水期望的模块。把.wasm当静态资源处理示例明确“Noasset/resource, nolocateFile, noresolve.fallback: { fs: false }, nocopy-webpack-plugin”——source-phase 模式下 webpack 本身就负责 fetch/编译/哈希重复用资源拷贝方案反而会破坏模块化与缓存设计。忘记开启实验开关experiments.asyncWebAssembly与experiments.sourceImport缺一不可且import source属实验性语法schemaschemas/WebpackOptions.json中对应能力位被标记为experimental请确认所使用 webpack 版本已支持。服务器 MIME 类型错误WebAssembly.compileStreaming需要application/wasm否则运行时告警并降级为较慢的WebAssembly.compile本地开发可用任意正确配置了 MIME 的静态服务器仓库测试通过 test.filter.js 预先探测宿主是否支持 WebAssembly 来决定是否执行本示例。忘记向instantiateWasm返回信号异步实例化场景中钩子应返回{}非undefined语义以告知胶水“实例化由外部异步完成”随后务必调用receiveInstance(instance, module)交回控制权。延伸阅读本示例查看 examples/wasm-emscripten 目录下全部文件README 由其 template.md 生成。同主题的 source-phase 对照示例examples/wasm-simple-source-phase无胶水的简单 source-phase 导入。其他 wasm 集成形态可参考 examples/README.md 的 “WebAssembly” 一节所收录的 wasm-simple、wasm-complex 等示例。webpack 侧核心实现位于 lib/wasm-async解析器 AsyncWebAssemblyParser.js、代码生成器 AsyncWebAssemblyJavascriptGenerator.js、模块定义 AsyncWasmModule.js、编译/加载运行时 AsyncWasmCompileRuntimeModule.js 与 AsyncWasmLoadingRuntimeModule.js以及插件入口 AsyncWebAssemblyModulesPlugin.js。【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表