news 2026/9/8 17:33:45

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

作者头像

张小明

前端开发工程师

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

在 webpack 中打包 Emscripten 编译的 WebAssembly:通过 source-phase import(import 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.sourceImport(source-phase import,即import source)的配置、instantiateWasm钩子契约,以及 webpack 从解析、代码生成到运行时“只编译不实例化”的完整原理与产物形态。


问题背景:为什么 Emscripten 产物会报export 'default' ... was not found

Emscripten(以及大量 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 import(import 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,调用胶水工厂并注入instantiateWasm
emscripten-module.jsEmscripten 胶水的微型替身,实现工厂 +instantiateWasm契约
program.watwasm 文本格式源码(导入env.log,导出斐波那契run
program.wasm对应的已编译二进制(约 96 字节)
webpack.config.js构建配置
index.html浏览器入口页面,加载dist/output.js
test.filter.js示例测试的过滤器:仅在宿主支持 WebAssembly 时运行
template.mdREADME 的生成模板(_{example.js}_等占位符被真实文件内容与构建统计替换)

这是一类“运行并校验”风格的示例:其 README.md 由模板与真实构建输出合并而成(文档中Info一节的产物统计即来自真实的 dev/production 构建),因此dist/目录并不入库。需要在本机复现时,可参考 examples/README.md 中 “Building an Example” 一节的流程:在仓库根目录执行yarnyarn setup,然后在示例目录内执行node build.js(或在根目录执行npm run build:examples,由 examples/buildAll.js 逐个目录驱动构建)。浏览器场景则用任意静态服务器把index.htmldist/一起托管后访问即可。

配置解读: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 source+instantiateWasm钩子

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)); });

这里有三层契约:

  1. import source programWasmprogramWasm不是实例、也不是字节数组,而是一个已经编译好的WebAssembly.Module。webpack 保证它已被正确 fetch 与编译(并参与 content-hash 与 code splitting)。
  2. 胶水工厂的参数对象:调用createModule(...)时传入onLog回调与instantiateWasm钩子。instantiateWasm收到胶水构造好的importsreceiveInstance回调,应用侧在此调用标准WebAssembly.instantiate(programWasm, imports)完成真正的实例化。
  3. 返回{}表示异步进行中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); }; // Emscripten's 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),返回Promise<Module>。真实 Emscripten 胶水体量庞大且被压缩,但面向打包器必须满足的“表面契约”只有这么小——这也是本示例能用替身演示、且真实胶水可无改动替换的原因。
  • import object 由胶水构造:示例中 wasm 声明(import "env" "log" ...),胶水便构造{ env: { log } }并通过instantiateWasm交给宿主。这正是“webpack 无法实例化该模块”的根因——import object 的知识只存在于胶水内部。
  • instantiateWasm是 Emscripten 官方逃逸口:若调用方传入该钩子,胶水把importsreceiveInstance直接转发,由宿主决定何时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,再用definePropertyGettersdefault指向它。对比非 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")拿到编译好的模块并注册为default

    var __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:开发模式与生产模式对比

文档末尾给出两种模式的真实构建统计:

  • Unoptimized(development)output.js10.7 KiB,辅助 wasmf052564a523e50ee50a2.module.wasm96 字节(immutable);runtime 模块 5 个(3.31 KiB)。
  • Production modeoutput.js压缩到 2.29 KiB,辅助 wasm 为f5155e54cc54c8650d10.module.wasm(仍 96 字节、immutable);runtime 模块收敛为 4 个(3.1 KiB)。

两者的 wasm 资产哈希不同,是因为 development/production 下 webpack 内部渲染与哈希输入存在差异——这正是optimization.chunkIds: "deterministic"的意义:让除哈希外的模块组织保持稳定,便于在两种模式下对照验证(示例用模板文件 template.md 以_{stdout}__{production:stdout}_占位符分别注入两种构建的输出)。

常见误区与排查建议

结合示例 README 的“反面清单”与源码行为,实际工程中容易踩的坑包括:

  1. 沿用普通webassembly/async实例化语义:当.wasm附带自实例化胶水时,应使用import source+ 手动instantiateWasm;否则会得到export 'default' ... was not found,因为 webpack 暴露的是 wasm 原始导出,而不是胶水期望的模块。
  2. .wasm当静态资源处理:示例明确“Noasset/resource, nolocateFile, noresolve.fallback: { fs: false }, nocopy-webpack-plugin”——source-phase 模式下 webpack 本身就负责 fetch/编译/哈希,重复用资源拷贝方案反而会破坏模块化与缓存设计。
  3. 忘记开启实验开关experiments.asyncWebAssemblyexperiments.sourceImport缺一不可,且import source属实验性语法,schema(schemas/WebpackOptions.json)中对应能力位被标记为experimental,请确认所使用 webpack 版本已支持。
  4. 服务器 MIME 类型错误WebAssembly.compileStreaming需要application/wasm,否则运行时告警并降级为较慢的WebAssembly.compile;本地开发可用任意正确配置了 MIME 的静态服务器(仓库测试通过 test.filter.js 预先探测宿主是否支持 WebAssembly 来决定是否执行本示例)。
  5. 忘记向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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 17:32:33

Python零基础入门:环境配置、虚拟环境与第一个小项目实战

1. 为什么我会把“装环境”和“写代码”这两件事放在同一篇里记如果你真的打算从零开始学Python&#xff0c;多半会和我当初一样&#xff0c;到处搜“python安装教程”“python入门”“python基础语法”&#xff0c;然后被一堆结果砸晕——问题是&#xff0c;收藏了十几个教程&…

作者头像 李华
网站建设 2026/9/8 17:31:47

微信场景下的多模态Embedding训练:数据、损失与部署全攻略

1. 写在前面&#xff1a;为什么要在“微信”语境下训练多模态 Embedding看到这个标题&#xff0c;你可能第一反应是&#xff1a;微信还能自己训模型&#xff1f;其实这里的“微信”有两层意思&#xff1a;一是微信生态里的业务场景&#xff08;小程序、公众号、视频号、扫一扫、…

作者头像 李华
网站建设 2026/9/8 17:31:42

接口测试全攻略:从工具实战到自动化框架与平台演进

1. 接口测试到底测什么&#xff1a;先厘清基础概念 聊接口测试之前&#xff0c;得先统一一下认知。很多人一提到接口测试&#xff0c;第一反应就是"用Postman发个请求&#xff0c;看返回是不是200"。这其实只摸到了皮毛。接口测试的核心&#xff0c;是直接对服务端提…

作者头像 李华
网站建设 2026/9/8 17:29:26

Atmosphere 19.0.1 固件适配指南:从机型判断到排障的完整流程

Atmosphere 19.0.1 固件适配指南&#xff1a;从机型判断到排障的完整流程 【免费下载链接】Atmosphere Atmosphre is a work-in-progress customized firmware for the Nintendo Switch. 项目地址: https://gitcode.com/GitHub_Trending/at/Atmosphere Atmosphere 是运行…

作者头像 李华
网站建设 2026/9/8 17:28:08

书霸AI|www.shubaai.com|微信搜书霸AI写作

https://www.shubaai.com写文献综述最容易踩的坑&#xff0c;并不是“资料不够多”&#xff0c;而是没有建立清晰的研究坐标。第一次接触某个选题时&#xff0c;很多人习惯边搜边写&#xff1a;看到一篇摘一句&#xff0c;换一篇再补一段。最后引用不少&#xff0c;文章却像文献…

作者头像 李华