本篇是 Rspack 源码解析系列第三十四篇。前面三十多篇里,
#[plugin]、#[plugin_hook]、#[define_hook]、#[cacheable]反复出现,但一直没展开。本文进入rspack_hook和rspack_macros,看这些过程宏到底生成了什么代码,以及 Hook 的五种执行语义(Series/Sync/Bail/Waterfall/Parallel)如何实现。
前言:宏是「样板代码生成器」
写插件时我们只写:
rust
#[plugin]
#[derive(Debug)]
pub struct ExternalsPlugin { ... }
#[plugin_hook(NormalModuleFactoryFactorize for ExternalsPlugin)]
async fn factorize(&self, data: &mut ModuleFactoryCreateData) -> Result<Option<BoxModule>> {
// ...
}
但编译器看到的远不止这些。#[plugin] 和 #[plugin_hook] 会展开成大量的胶水代码:结构体包装、Deref、Arc 共享、trait 实现、tracing 插桩。本文逐层拆解。
源码入口:三个 proc macro
crates/rspack_macros/src/lib.rs 导出了几个 proc macro:
rust
#[proc_macro_attribute]
pub fn plugin(_args, tokens) -> proc_macro::TokenStream {
let item = syn::parse_macro_input!(tokens as syn::ItemStruct);
plugin::expand_struct(item)
}
#[proc_macro_attribute]
pub fn plugin_hook(args, tokens) -> proc_macro::TokenStream {
let args = syn::parse_macro_input!(args as plugin::HookArgs);
let input = syn::parse_macro_input!(tokens as syn::ItemFn);
plugin::expand_fn(args, input)
}
#[proc_macro]
pub fn define_hook(input) -> proc_macro::TokenStream {
let input = syn::parse_macro_input!(input as hook::DefineHookInput);
input.expand()
}
三个宏分别负责:定义 Hook、定义插件结构体、把函数变成 Hook 的 tap 实现。
#plugin:Arc 包裹 + Deref
crates/rspack_macros/src/plugin.rs 的 expand_struct 先把结构体的字段抽出来,生成一个 Inner 结构体:
rust
let inner_ident = plugin_inner_ident(ident); // XxxPlugin -> XxxPluginInner
// 原始字段全部移入 Inner
let inner_struct = quote! {
pub struct #inner_ident #impl_generics #where_clause #inner_fields
};
// 原来的结构体只保留一个 Arc<Inner>
input.fields = syn::Fields::Named(syn::FieldsNamed::parse.parse2(
quote! { { inner: ::std::sync::Arc<#inner_ident #ty_generics> } }
).expect("Failed to parse"));
于是:
rust
#[plugin]
pub struct ExternalsPlugin { externals: Vec<ExternalItem>, ... }
展开成:
rust
pub struct ExternalsPlugin {
inner: ::std::sync::Arc<ExternalsPluginInner>,
}
pub struct ExternalsPluginInner {
externals: Vec<ExternalItem>,
// ... 原字段
}
为什么要 Arc 包裹
插件实例会在多个 Hook tap 之间共享。plugin_hook 生成的 tap 结构体都持有同一个 Arc<Inner>:
rust
impl ExternalsPlugin {
fn new_inner(...) -> Self {
Self { inner: ::std::sync::Arc::new(ExternalsPluginInner { ... }) }
}
fn inner(&self) -> &::std::sync::Arc<ExternalsPluginInner> { &self.inner }
}
impl ::std::ops::Deref for ExternalsPlugin {
type Target = ExternalsPluginInner;
fn deref(&self) -> &Self::Target { &self.inner }
}
Deref 让我们能 self.externals 直接访问字段(自动解引用到 Inner),而 Arc 让 tap 之间共享同一份状态、无需复制。
#plugin_hook:函数变成 tap 结构体
expand_fn 接收 NormalModuleFactoryFactorize for ExternalsPlugin 和一个 async fn,展开成一个结构体 + trait 实现。
先解析 HookArgs:
rust
pub struct HookArgs {
trait_: syn::Path, // NormalModuleFactoryFactorize
name: syn::Ident, // ExternalsPlugin
stage: Option<syn::Expr>, // 可选 stage
tracing: Option<syn::LitBool>, // 可选 tracing
generics: syn::Generics,
}
stage 和 tracing 是可选参数:
rust
#[plugin_hook(SomeHook for SomePlugin, stage = 5, tracing = false)]
async fn hook_impl(...) { ... }
生成 tap 结构体
expand_fn 为每个 hook 函数生成一个同名结构体(fn_ident = 函数名):
rust
#[allow(non_camel_case_types)]
#vis struct #fn_ident #impl_generics #where_clause {
#vis inner: ::std::sync::Arc<#inner_ident #ty_generics>,
}
impl #fn_ident {
fn new(plugin: &#name) -> Self {
#fn_ident { inner: ::std::sync::Arc::clone(plugin.inner()) }
}
}
new(plugin) 从插件克隆 Arc,所以 tap 和插件共享状态。
把函数体搬进 impl 块
原始函数体被放进 impl ExternalsPlugin 里,作为真正的方法:
rust
impl ExternalsPlugin {
#[allow(clippy::ptr_arg)]
#real_sig #block // 原始的 async fn factorize(&self, data: ...) { ... }
}
然后生成 trait 实现,内部调用这个方法:
rust
#attr // #[async_trait] 或空
impl NormalModuleFactoryFactorize for factorize {
#tracing_annotation
async fn run(&self, data: &mut ModuleFactoryCreateData) -> ... {
ExternalsPlugin::factorize(&ExternalsPlugin::from_inner(&self.inner), data).await
}
}
注意 &self 变成了 from_inner(&self.inner) 重新包一层,因为方法签名要求 &ExternalsPlugin。
处理 &mut self 的参数
expand_fn 有个细节:把函数参数里的 mut 去掉:
rust
for arg in sig.inputs.iter_mut().skip(1) {
if let syn::FnArg::Typed(syn::PatType { pat, .. }) = arg
&& let Pat::Ident(PatIdent { mutability, .. }) = &mut **pat
{
*mutability = None; // 去掉 &mut data 里的 mut
}
}
这是因为 tap trait 的 run 签名需要统一,&mut 的 mutability 由 trait 定义控制,函数签名里不能重复标注。
tracing 插桩
默认开启 tracing:
rust
let tracing_annotation = tracing.is_none_or(|bool_lit| bool_lit.value).then(|| {
quote! {
#[::rspack_hook::__macro_helper::tracing::instrument(name = #tracing_name, skip_all, fields(
perfetto.track_name = #plugin_name,
perfetto.process_name = "Plugin Analysis"
))]
}
});
perfetto.track_name 和 perfetto.process_name 说明 tracing 数据是给 Perfetto(性能分析工具)用的,能按插件名追踪每个 hook 的执行耗时。
源码入口:define_hook 的五种执行语义
crates/rspack_macros/src/hook.rs 的 DefineHookInput::parse 解析:
rust
define_hook!(NormalModuleFactoryFactorize: SeriesBail(NormalModuleFactoryCreateData) -> Option<BoxModule>);
解析出:
trait_name:NormalModuleFactoryFactorizeargs:NormalModuleFactoryCreateDataexec_kind:SeriesBail { ret: Option<BoxModule> }
五种 ExecKind:
rust
enum ExecKind {
Series, // 顺序执行,忽略返回值
Sync, // 同步顺序执行
SeriesBail { ret }, // 顺序执行,遇到 Some 提前返回
SeriesWaterfall { ret }, // 顺序执行,返回值传给下一个
Parallel, // 并行执行
}
is_async 判断:除了 Sync,其他都是 async:
rust
fn is_async(&self) -> bool {
!matches!(self, Self::Sync)
}
生成的 Hook 结构体
define_hook 为每个 trait 生成一个 XxxHook 结构体:
rust
pub struct NormalModuleFactoryFactorizeHook {
taps: Vec<Box<dyn NormalModuleFactoryFactorize + Send + Sync>>,
interceptors: Vec<Box<dyn Interceptor<Self> + Send + Sync>>,
}
impl Hook for NormalModuleFactoryFactorizeHook {
type Tap = Box<dyn NormalModuleFactoryFactorize + Send + Sync>;
fn used_stages(&self) -> FxHashSet<i32> { ... }
fn intercept(&mut self, interceptor) { self.interceptors.push(Box::new(interceptor)); }
}
tap 方法追加一个 tap:
rust
pub fn tap(&mut self, tap: impl NormalModuleFactoryFactorize + Send + Sync + 'static) {
self.taps.push(Box::new(tap));
}
call:五种执行语义的实现
call 是 Hook 的核心。先统一处理 interceptor:
rust
fn additional_taps(&self) -> TokenStream {
// 收集 interceptor 提供的额外 tap
// 合并到 self.taps,按 stage 排序
all_taps.sort_by_key(|hook| hook.stage());
}
interceptor 可以动态添加 tap,合并后按 stage 排序------stage 小的先执行,保证执行顺序确定性。
然后五种语义:
Series(顺序执行,忽略返回值):
rust
for tap in all_taps {
tap.run(#args).await?;
}
Ok(())
Sync(同步顺序):
rust
for tap in all_taps {
tap.run(#args)?;
}
Ok(())
SeriesBail(遇到 Some 提前返回):
rust
for tap in all_taps {
if let Some(res) = tap.run(#args).await? {
return Ok(Some(res));
}
}
Ok(None)
这正是第 22 篇 externals 的 factorize 语义:第一个返回 Some(module) 的 tap 胜出,后续不再执行。
SeriesWaterfall(返回值传给下一个):
rust
let mut data = #args;
for tap in all_taps {
data = tap.run(data).await?
}
Ok(data)
Parallel(并行执行,等待全部完成):
rust
let futs: std::vec::Vec<_> = all_taps.iter().map(|t| t.run(#args)).collect();
futures_concurrency::vec::TryJoin(futs).await?;
Ok(())
用 futures_concurrency::TryJoin 并发执行所有 tap,任何一个出错就整体失败。
return_type:不同语义的返回类型
rust
pub fn return_type(&self) -> TokenStream {
match self {
Self::SeriesBail { ret } => {
if let Some(ret) = ret {
quote! { Result<Option<#ret>> }
} else {
quote! { Result<Option<()>> }
}
}
Self::SeriesWaterfall { ret } => quote! { Result<#ret> },
_ => quote! { Result<()> },
}
}
SeriesBail 返回 Result<Option<T>>,SeriesWaterfall 返回 Result<T>(瀑布流转),其他返回 Result<()>。
run_sig 与 call_fn 的 async 区分
define_hook 生成的 trait 的 run 方法签名根据 is_async 决定是否加 async:
rust
let run_sig = quote! { fn run(&self, #args) -> #ret; };
let run_sig = if is_async {
quote! { async #run_sig }
} else {
run_sig
};
call 同理:
rust
let call_fn = if is_async {
quote! { async fn call(&self, #args) -> #ret { #call_body } }
} else {
quote! { fn call(&self, #args) -> #ret { #call_body } }
};
tracing:hook 级别的插桩
define_hook 也支持 tracing:
rust
let call_body = if tracing.is_none_or(|bool_lit| bool_lit.value) {
let tracing_span_name = LitStr::new(&format!("hook:{trait_name}"), trait_name.span());
if is_async {
quote! {
tracing::Instrument::instrument(async { #call_body },
tracing::info_span!(#tracing_span_name)).await
}
} else {
quote! {
let tracing_span = tracing::info_span!(#tracing_span_name);
let _tracing_span_guard = tracing_span.enter();
#call_body
}
}
} else {
call_body
};
hook 名作为 span 名,方便在 tracing 输出里看到「哪个 hook 花了多久」。
与 #cacheable 的呼应
#[cacheable](rspack_cacheable_macros)也是过程宏,为结构体派生 rkyv 序列化。它和 #[plugin] 常一起出现:
rust
#[plugin]
#[derive(Debug)]
#[cacheable]
pub struct SomePlugin { ... }
#[plugin] 处理 Arc 包装和 Deref,#[cacheable] 处理序列化,两者正交,共同支撑「可共享、可持久化」的插件对象。
Rust 角度:宏让插件开发「只写业务」
对比手写版:
rust
// 手写需要:Inner 结构体 + Arc + Deref + tap 结构体 + trait impl + tracing
// 用了宏只需:
#[plugin]
#[derive(Debug)]
pub struct ExternalsPlugin { ... }
#[plugin_hook(NormalModuleFactoryFactorize for ExternalsPlugin)]
async fn factorize(&self, data: &mut ModuleFactoryCreateData) -> Result<Option<BoxModule>> {
// 业务逻辑
}
宏把「框架胶水」和「业务逻辑」分离:开发者只关心业务逻辑(factorize 里匹配 external),框架负责把它接到 Hook 系统上。
这一篇应该带走什么
#[plugin]把结构体包成Arc<Inner>+Deref,支持 tap 间共享;#[plugin_hook]把函数变成 tap 结构体 + trait 实现;#[define_hook]生成 Hook 结构体,支持 Series/Sync/Bail/Waterfall/Parallel 五种语义;SeriesBail是 externals 拦截的核心(Some 提前返回);- tracing 默认开启,数据供 Perfetto 按插件名分析耗时。
写在最后
到这里,Hook 系统、插件体系、模块类型、chunk 渲染、运行时加载这条主线已经完整。整个系列的最后一层是「Rust 如何暴露给 JS」------rspack_binding_api 和 rspack_napi。JS 侧如何调用 Rust、Rust 的 Compiler 如何驱动 JS 的 loader/plugin 回调。这一层是「Rust 核心」与「webpack 生态」的桥梁,留作系列的下一个阶段。
系列回顾:这十四篇的主线
从第 21 篇到第 34 篇,我们走完了「缓存 → 特殊模块 → 插件层 → 运行时 → 宏系统」的完整链路:
text
21 cacheable + storage 持久化缓存
22 externals 模块创建拦截
23 json 纯值模块 tree shaking
24 asset 双 source type 模块
25 css parser/generator CSS 词法 + 导出
26 css chunking/order CSS 顺序 + 拆分
27 wasm 二进制异步模块
28 html 产物注入
29 copy 静态复制
30 devtool/source map source map 生成
31 hmr hash 比对增量更新
32 runtime chunk loading chunk 加载运行时
33 mf 远程模块
34 hook 宏系统 框架胶水
每一条线都印证同一个核心思想:Rspack 的「快」不是单点优化,而是 Rust 核心 + 可插拔 Hook + 按需运行时 + 持久化缓存的系统性设计。