Rspack 源码解析(二十七):AsyncWasmParserAndGenerator 如何把二进制变成异步模块

本篇是 Rspack 源码解析系列第二十七篇。前面的模块要么是文本(JS/CSS/JSON),要么是纯值(asset)。本文进入 rspack_plugin_wasm,看一个 .wasm 二进制如何被 wasmparser 解析出导入/导出,又如何通过 INSTANTIATE_WASM 把实例化推迟到运行时。

前言:wasm 没有 AST,只有二进制段

.wasm 是二进制格式,没有「源代码」,也没有传统 AST。它的「解析」其实是读取 WebAssembly 的 section(段):

text 复制代码
ExportSection   -> 导出什么函数/内存
ImportSection   -> 依赖哪些外部模块

AsyncWasmParserAndGenerator 的 parse 就是遍历这些 section,导出变成 StaticExportsDependency,导入变成 WasmImportDependency。

源码入口:wasmparser 遍历段

crates/rspack_plugin_wasm/src/parser_and_generator.rs 的 parse:

rust 复制代码
let mut exports = Vec::with_capacity(1);
let mut dependencies: Vec<BoxDependency> = Vec::with_capacity(1);
let mut diagnostic = Vec::with_capacity(1);

for payload in Parser::new(0).parse_all(&source.buffer()) {
  match payload {
    Ok(payload) => match payload {
      Payload::ExportSection(s) => {
        for export in s {
          match export {
            Ok(export) => exports.push(export.name.to_string()),
            Err(err) => diagnostic.push(Diagnostic::error(
              "Wasm Export Parse Error".into(), err.to_string())),
          };
        }
      }
      Payload::ImportSection(s) => {
        for import in s {
          match import {
            Ok(Import { module, name, .. }) => {
              dependencies.push(Box::new(WasmImportDependency::new(
                module.into(), name.into())));
            }
            Err(err) => diagnostic.push(Diagnostic::error(
              "Wasm Import Parse Error".into(), err.to_string())),
          }
        }
      }
      _ => {}
    },
    Err(err) => {
      diagnostic.push(Diagnostic::error("Wasm Parse Error".into(), err.to_string()));
    }
  }
}

三个关键点:

  1. Parser::new(0).parse_all(...):从偏移 0 开始解析全部 payload;
  2. ExportSection:收集导出名,存进 exports;
  3. ImportSection:每个 Import { module, name } 生成一个 WasmImportDependency。

导出变成 StaticExportsDependency

parse 末尾把收集到的导出名打包:

rust 复制代码
dependencies.push(Box::new(StaticExportsDependency::new(
  StaticExportsSpec::Array(exports.iter().cloned().map(Atom::from).collect::<Vec<_>>()),
  false,
)));

StaticExportsDependency 声明「这个模块有哪些静态导出」,false 表示「这些不是可跳过副作用的导出」。这和第 25 篇 CSS 的 exports 机制类似,都是把导出信息交给图优化。

build_meta:标记异步与 namespace

wasm 模块有几个特殊标记:

rust 复制代码
parse_context.build_info.strict = true;
parse_context.build_meta.has_top_level_await = true;
parse_context.build_meta.exports_type = BuildMetaExportsType::Namespace;

has_top_level_await = true 说明 wasm 模块的加载是异步的(WebAssembly.instantiate 返回 Promise)。exports_type = Namespace 说明 wasm 的导出是命名空间导出。

源码入口:WasmImportDependency

crates/rspack_plugin_wasm/src/dependency/wasm_import_dependency.rs 定义了导入依赖。它继承 ModuleDependency,request() 返回 wasm 里 import 的 module 名(通常是 JS 模块路径)。

generate:从 wasm import 到 JS import

generate 的 SourceType::JavaScript 分支是核心。它遍历所有 WasmImportDependency,把它们转成 JS 的 import:

rust 复制代码
let mut dep_modules = IndexMap::<ModuleIdentifier, DepModule>::new();
let mut promises: Vec<String> = vec![];

module.get_dependencies().iter()
  .map(|id| module_graph.dependency_by_id(id))
  .filter(|dep| dep.dependency_type() == &DependencyType::WasmImport)
  .map(|dep| (dep, module_graph.module_graph_module_by_dependency_id(dep.id())))
  .for_each(|(dep, mgm)| {
    if let Some(mgm) = mgm {
      let dep = dep.as_any().downcast_ref::<WasmImportDependency>().expect("...");
      let request = dep.request();
      let len = dep_modules.len();
      let dep_module = dep_modules.entry(mgm.module_identifier).or_insert_with(|| {
        let import_var = format!("rspack_import_{len}");
        if ModuleGraph::is_async(&compilation.async_modules_artifact, &mgm.module_identifier) {
          promises.push(import_var.clone());
        }
        DepModule { request, import_var, deps: vec![] }
      });
      dep_module.deps.push((*dep.id(), dep.name().clone()));
    }
  });

注意 import_var 用 rspack_import_{len} 这种数字序号,而不是变量名------这是为了生成紧凑、确定性的代码。

import 语句的生成

每个 dep_module 生成一条 import 语句:

rust 复制代码
let (imports_code, imports_compat_code) = dep_modules.iter().map(|(_, dep_module)| {
  runtime_template.import_statement(
    module, compilation, &dep_module.deps[0].0, &dep_module.import_var,
    dep_module.request, ImportPhase::Evaluation, false,
  )
}).unzip();

import_statement 会根据目标模块类型生成 import 或 require,同时返回「兼容代码」(imports_compat_code),用于把 ESM 命名空间转成可用的对象。

import 对象:把 wasm import 映射到 JS 导出

wasm 实例化需要一个 import 对象,把 wasm 里的 (module, name) 映射到 JS 值:

rust 复制代码
let import_obj_request_items = dep_modules.into_values().map(|dep_module| {
  let deps = dep_module.deps.into_iter().map(|(dep_id, export_name)| {
    let export = runtime_template.export_from_import(
      compilation, &mut ModuleInitFragments::default(),
      module.identifier(), *runtime, true,
      dep_module.request, &dep_module.import_var,
      std::slice::from_ref(&export_name), &dep_id,
      false, false, Some(true), ImportPhase::Evaluation,
    );
    let name = json_stringify(&export_name);
    format!("{name}: {export}")
  }).collect::<Vec<_>>().join(",\n");

  format!("{}: {{\n{deps}\n}}", json_stringify(dep_module.request))
}).collect::<Vec<_>>();

最终生成类似:

js 复制代码
{
  "./math.js": {
    "add": rspack_import_0.add
  }
}

INSTANTIATE_WASM:实例化调用

生成的核心是一句 __webpack_require__.instantiateWasm:

rust 复制代码
let instantiate_call = format!(
  r#"{}({exports_argument}, {}, "{}" {})"#,
  runtime_template.render_runtime_globals(&RuntimeGlobals::INSTANTIATE_WASM),
  runtime_template.render_runtime_globals(&RuntimeGlobals::MODULE_ID),
  &hash,
  imports_obj.unwrap_or_default()
);

hash 来自 module.build_info().hash,用于运行时判断「这个 wasm 是否已经实例化过」。MODULE_ID 是模块 id,exports_argument 是导出对象。

异步与同步两条路径

如果 wasm 的 import 里有异步模块,生成异步加载代码:

rust 复制代码
let source = if !promises.is_empty() {
  let promises = promises.join(", ");
  let decl = format!(
    "var __rspack_instantiate__ = function ([{promises}]) {{\n{imports_compat_code}return {instantiate_call};\n}}\n"
  );
  let async_dependencies = format!(
"{}({module_argument}, async function (__rspack_load_async_deps, __rspack_async_done) {{
  try {{
{imports_code}
    var __rspack_async_deps = __rspack_load_async_deps([{promises}]);
    var [{promises}] = __rspack_async_deps.then ? (await __rspack_async_deps)() : __rspack_async_deps;
    {imports_compat_code}await {instantiate_call};
  __rspack_async_done();
  }} catch(e) {{ __rspack_async_done(e); }}
}}, 1);",
    runtime_template.render_runtime_globals(&RuntimeGlobals::ASYNC_MODULE),
  );
  RawStringSource::from(format!("{decl}{async_dependencies}"))
} else {
  RawStringSource::from(format!(
    "{imports_code}{imports_compat_code}{module_argument}.exports = {instantiate_call};"
  ))
};

有异步依赖时,用 ASYNC_MODULE 包裹,先 await 所有异步 import,再实例化;否则直接同步赋值 module.exports = instantiateWasm(...)。

源码入口:loading_plugin 的三种加载方式

crates/rspack_plugin_wasm/src/loading_plugin.rs 的 enable_wasm_loading_plugin 根据 wasmLoading 选择:

rust 复制代码
pub fn enable_wasm_loading_plugin(wasm_loading_type: WasmLoadingType) -> BoxPlugin {
  match wasm_loading_type {
    WasmLoadingType::Fetch => FetchCompileAsyncWasmPlugin::default().boxed(),
    WasmLoadingType::AsyncNode => ReadFileCompileAsyncWasmPlugin::new().boxed(),
    WasmLoadingType::Universal => UniversalCompileAsyncWasmPlugin::default().boxed(),
  }
}

三种对应三种运行时:

  • Fetch :浏览器 fetch(url) 拉取;
  • AsyncNode :Node.js fs.readFile 读取;
  • Universal:运行时探测环境,自动选择 fetch 或 fs。

runtime_requirement_in_tree:按需注入

三个插件都在 runtime_requirement_in_tree Hook 里响应 INSTANTIATE_WASM:

rust 复制代码
#[plugin_hook(CompilationRuntimeRequirementInTree for FetchCompileAsyncWasmPlugin)]
async fn fetch_compile_async_wasm_plugin_runtime_requirements_in_tree(...) {
  if !runtime_requirements.contains(RuntimeGlobals::INSTANTIATE_WASM) {
    return Ok(None);
  }
  runtime_requirements_mut.insert(RuntimeGlobals::PUBLIC_PATH);
  runtime_modules_to_add.push((
    *chunk_ukey,
    AsyncWasmLoadingRuntimeModule::new(
      &compilation.runtime_template,
      format!("fetch({} + $PATH)", runtime_template.render_runtime_globals(&RuntimeGlobals::PUBLIC_PATH)),
      true,
    ).boxed(),
  ));
  Ok(None)
}

只有当 chunk 里确实有 wasm 模块(INSTANTIATE_WASM 被标记)时,才注入 instantiateWasm 的运行时实现。这是第 9 篇「runtime requirements」机制的直接应用。

Universal 的运行时探测

UniversalCompileAsyncWasmPlugin 生成同时兼容浏览器和 Node 的代码:

rust 复制代码
let generate_before_load_binary_code =
  r#"var useFetch = typeof document !== 'undefined' || typeof self !== 'undefined';
var wasmUrl = $PATH;"#.to_string();

let generate_load_binary_code = format!(
  r#"(useFetch
  ? fetch(new URL(wasmUrl, {import_meta_name}.url))
  : Promise.all([import('fs'), import('url')]).then(([{{ readFile }}, {{ URL }}]) => new Promise((resolve, reject) => {{
      readFile(new URL(wasmUrl, {import_meta_name}.url), (err, buffer) => {{
        if (err) return reject(err);
        resolve({{ arrayBuffer() {{ return buffer; }} }});
      }});
    }})))"#
);

浏览器走 fetch,Node 走 fs.readFile,并把 Buffer 伪装成 arrayBuffer() 返回值,复用同一套后续逻辑。

Rust 角度:二进制模块靠「运行时胶水」

wasm 插件展示了「二进制模块」的处理范式:

text 复制代码
parse 只提取 section 信息(导入/导出)
generate 生成 import 语句 + instantiateWasm 调用
runtime 根据 target 注入 fetch/fs/universal 加载实现

模块本身不包含真正的实例化逻辑,实例化被推迟到运行时,由 instantiateWasm 这个 runtime global 承担。这和第 22 篇 externals 的「假模块 + 运行时加载」是同一思路的延续。

这一篇应该带走什么

  1. wasm parse 用 wasmparser 遍历 section,导出转 StaticExportsDependency,导入转 WasmImportDependency;
  2. build_meta.has_top_level_await = true 标记异步加载;
  3. generate 把 wasm import 映射成 JS import + import 对象,核心是 instantiateWasm;
  4. 有异步 import 时用 ASYNC_MODULE 包裹,否则同步赋值;
  5. enable_wasm_loading_plugin 按 Fetch/AsyncNode/Universal 三种 target 注入加载实现。

写在最后

wasm 是「二进制 + 运行时胶水」。接下来回到最常用的 Web 场景------HTML。HtmlRspackPlugin 不在「模块」体系里,而是在 process_assets 阶段把一个 HTML 模板变成带 <script>/<link> 的最终产物。下一篇进入 rspack_plugin_html,看 HTML 如何注入资源标签。

相关推荐
遇码5 小时前
Rust 桌面应用自托管自动更新:latest.json 清单 + minisign 签名校验的完整做法
人工智能·ai·rust·大模型·agent
geovindu6 小时前
rust: Template Method Pattern(续)
开发语言·后端·设计模式·rust·行为模式·板方法模式
tryCbest6 小时前
Vue3——第 6 篇:事件处理与表单绑定——v-on 与 v-model
前端·javascript·vue.js
恋猫de小郭6 小时前
Flutter 的文字为什么比 SwiftUI 更细?问题主要在 Skia 的 256 项查找表里
android·前端·flutter
lexiangqicheng6 小时前
前端高阶面试核心通关指南:深入解析Vue3与React18底层原理及性能优化
前端·面试·性能优化
怕浪猫6 小时前
前端对接聊天 API:我用 React 做了一个比 ChatGPT 还好看的界面
前端·后端·面试
程序员小孔.6 小时前
基于 Spring Cloud + Vue 的 JEEWMS 开源仓储管理平台设计与实现
前端·javascript·vue.js·spring boot·spring cloud·开源
IT_陈寒6 小时前
深入理解Python:核心原理与最佳实践
前端·人工智能·后端
我喜欢就喜欢6 小时前
Chrome 插件开发实战指南
前端·chrome
夕阳_醉了6 小时前
花了一天的时间,地板式扫盲了vue3所有API盲点
前端·javascript·vue.js