Rspack 源码解析(三十四):rspack_hook 与宏系统如何用过程宏生成异步 Hook 与插件

28 阅读7分钟

本篇是 Rspack 源码解析系列第三十四篇。前面三十多篇里,#[plugin]、#[plugin_hook]、#[define_hook]、#[cacheable] 反复出现,但一直没展开。本文进入 rspack_hook 和 rspack_macros,看这些过程宏到底生成了什么代码,以及 Hook 的五种执行语义(Series/Sync/Bail/Waterfall/Parallel)如何实现。

前言:宏是「样板代码生成器」

写插件时我们只写:

#[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:

#[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 结构体:

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

于是:

#[plugin]
pub struct ExternalsPlugin { externals: Vec<ExternalItem>, ... }

展开成:

pub struct ExternalsPlugin {
  inner: ::std::sync::Arc<ExternalsPluginInner>,
}

pub struct ExternalsPluginInner {
  externals: Vec<ExternalItem>,
  // ... 原字段
}

为什么要 Arc 包裹

插件实例会在多个 Hook tap 之间共享。plugin_hook 生成的 tap 结构体都持有同一个 Arc<Inner>:

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:

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 是可选参数:

#[plugin_hook(SomeHook for SomePlugin, stage = 5, tracing = false)]
async fn hook_impl(...) { ... }

生成 tap 结构体

expand_fn 为每个 hook 函数生成一个同名结构体(fn_ident = 函数名):

#[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 里,作为真正的方法:

impl ExternalsPlugin {
  #[allow(clippy::ptr_arg)]
  #real_sig #block   // 原始的 async fn factorize(&self, data: ...) { ... }
}

然后生成 trait 实现,内部调用这个方法:

#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 去掉:

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:

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 解析:

define_hook!(NormalModuleFactoryFactorize: SeriesBail(NormalModuleFactoryCreateData) -> Option<BoxModule>);

解析出:

  • trait_name:NormalModuleFactoryFactorize
  • args:NormalModuleFactoryCreateData
  • exec_kind:SeriesBail { ret: Option<BoxModule> }

五种 ExecKind:

enum ExecKind {
  Series,               // 顺序执行,忽略返回值
  Sync,                 // 同步顺序执行
  SeriesBail { ret },   // 顺序执行,遇到 Some 提前返回
  SeriesWaterfall { ret },  // 顺序执行,返回值传给下一个
  Parallel,             // 并行执行
}

is_async 判断:除了 Sync,其他都是 async:

fn is_async(&self) -> bool {
  !matches!(self, Self::Sync)
}

生成的 Hook 结构体

define_hook 为每个 trait 生成一个 XxxHook 结构体:

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:

pub fn tap(&mut self, tap: impl NormalModuleFactoryFactorize + Send + Sync + 'static) {
  self.taps.push(Box::new(tap));
}

call:五种执行语义的实现

call 是 Hook 的核心。先统一处理 interceptor:

fn additional_taps(&self) -> TokenStream {
  // 收集 interceptor 提供的额外 tap
  // 合并到 self.taps,按 stage 排序
  all_taps.sort_by_key(|hook| hook.stage());
}

interceptor 可以动态添加 tap,合并后按 stage 排序——stage 小的先执行,保证执行顺序确定性。

然后五种语义:

Series(顺序执行,忽略返回值):

for tap in all_taps {
  tap.run(#args).await?;
}
Ok(())

Sync(同步顺序):

for tap in all_taps {
  tap.run(#args)?;
}
Ok(())

SeriesBail(遇到 Some 提前返回):

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(返回值传给下一个):

let mut data = #args;
for tap in all_taps {
  data = tap.run(data).await?
}
Ok(data)

Parallel(并行执行,等待全部完成):

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:不同语义的返回类型

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:

let run_sig = quote! { fn run(&self, #args) -> #ret; };
let run_sig = if is_async {
  quote! { async #run_sig }
} else {
  run_sig
};

call 同理:

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:

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] 常一起出现:

#[plugin]
#[derive(Debug)]
#[cacheable]
pub struct SomePlugin { ... }

#[plugin] 处理 Arc 包装和 Deref,#[cacheable] 处理序列化,两者正交,共同支撑「可共享、可持久化」的插件对象。

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 系统上。

这一篇应该带走什么

  1. #[plugin] 把结构体包成 Arc<Inner> + Deref,支持 tap 间共享;
  2. #[plugin_hook] 把函数变成 tap 结构体 + trait 实现;
  3. #[define_hook] 生成 Hook 结构体,支持 Series/Sync/Bail/Waterfall/Parallel 五种语义;
  4. SeriesBail 是 externals 拦截的核心(Some 提前返回);
  5. tracing 默认开启,数据供 Perfetto 按插件名分析耗时。

写在最后

到这里,Hook 系统、插件体系、模块类型、chunk 渲染、运行时加载这条主线已经完整。整个系列的最后一层是「Rust 如何暴露给 JS」——rspack_binding_api 和 rspack_napi。JS 侧如何调用 Rust、Rust 的 Compiler 如何驱动 JS 的 loader/plugin 回调。这一层是「Rust 核心」与「webpack 生态」的桥梁,留作系列的下一个阶段。

系列回顾:这十四篇的主线

从第 21 篇到第 34 篇,我们走完了「缓存 → 特殊模块 → 插件层 → 运行时 → 宏系统」的完整链路:

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 + 按需运行时 + 持久化缓存的系统性设计。