本篇是 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()));
}
}
}
三个关键点:
Parser::new(0).parse_all(...):从偏移 0 开始解析全部 payload;ExportSection:收集导出名,存进exports;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 的「假模块 + 运行时加载」是同一思路的延续。
这一篇应该带走什么
- wasm parse 用
wasmparser遍历 section,导出转StaticExportsDependency,导入转WasmImportDependency; build_meta.has_top_level_await = true标记异步加载;- generate 把 wasm import 映射成 JS import + import 对象,核心是
instantiateWasm; - 有异步 import 时用
ASYNC_MODULE包裹,否则同步赋值; enable_wasm_loading_plugin按 Fetch/AsyncNode/Universal 三种 target 注入加载实现。
写在最后
wasm 是「二进制 + 运行时胶水」。接下来回到最常用的 Web 场景------HTML。HtmlRspackPlugin 不在「模块」体系里,而是在 process_assets 阶段把一个 HTML 模板变成带 <script>/<link> 的最终产物。下一篇进入 rspack_plugin_html,看 HTML 如何注入资源标签。