- 前端构建
- 构建工具
- 开发工具
【免费下载链接】farm
Extremely fast Vite-compatible web build tool written in Rust
farmfe_macro_plugin是 Farm 编译器中用于生成 Rust 原生插件入口代码的过程宏(attribute macro),它让插件作者只需标注#[farm_plugin]即可自动生成动态库导出符号与版本校验函数;配合farmfe_macro_cache_item的#[cache_item]派生宏,还能让自定义 struct / enum 获得基于 rkyv 的零拷贝序列化能力,直接接入 Farm 的持久化缓存体系。本文以 crates/macro_plugin/README.md 为核心,结合 Farm 核心源码与真实插件(如 rust-plugins/virtual/src/lib.rs、rust-plugins/yaml/src/lib.rs)说明这两个宏的用法、原理与注意事项,读完即可独立编写一个可被 Farm 加载的 Rust 插件。
一、为什么需要farm_plugin过程宏
Farm 是使用 Rust 编写的高性能打包器,其原生插件以动态库(.so/.dylib/.dll)形式存在,由 Node 侧的插件适配器通过libloading在运行时加载。要让 Rust 结构体真正成为一个插件,必须满足以下动态库导出约定(见 crates/node/src/plugin_adapters/rust_plugin_adapter/plugin_loader.rs):
- 导出
_plugin_create(config: &Config, options: String) -> Arc<dyn Plugin>符号作为构造入口; - 导出
_core_version() -> String符号用于版本兼容性检查; - 将
config(编译配置)与options(JSON 字符串形式的插件选项)传入插件的构造函数; - 最终通过
Arc<dyn Plugin>统一返回给编译器,保证 trait 对象的内存布局稳定。
这些样板代码(尤其是#[no_mangle]导出与Arc<dyn Plugin>封装)如果让每个插件作者手写,极易出错,于是farmfe_macro_plugin将这些约定封装为一个 attribute macro。
二、farm_plugin宏的声明与源码结构
宏的完整实现只有 25 行,位于 crates/macro_plugin/src/lib.rs:
use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, ItemStruct}; #[proc_macro_attribute] pub fn farm_plugin(_attr: TokenStream, item: TokenStream) -> TokenStream { let item_struct: ItemStruct = parse_macro_input!(item); let struct_name = &item_struct.ident; let ts = quote! { #[unsafe(no_mangle)] pub fn _plugin_create(config: &farmfe_core::config::Config, options: String) -> std::sync::Arc<dyn farmfe_core::plugin::Plugin> { std::sync::Arc::new(#struct_name::new(config, options)) } #[unsafe(no_mangle)] pub fn _core_version() -> std::string::String { farmfe_core::VERSION.to_string() } #item_struct }; ts.into() }可见它实际上是一个 attribute macro 的典型写法:
- 用
syn的parse_macro_input!将标注的 struct 解析为ItemStruct,取到struct_name; - 用
quote!生成三段代码:_plugin_create、_core_version与原始 struct 定义本身; - 宏对属性参数
_attr直接忽略(传什么都会被丢弃),只保留 struct 体; - 该 crate 是
proc-macro类型的库(见 crates/macro_plugin/Cargo.toml 中的[lib] proc-macro = true),依赖proc-macro2、syn(full特性)和quote。
也就是说,#[farm_plugin]严格要求标注对象是一个 struct 且该 struct 必须存在一个fn new(config: &Config, options: String) -> Self构造方法,因为宏生成的_plugin_create直接调用了#struct_name::new(config, options)。
三、快速上手:用#[farm_plugin]编写最小插件
脚手架模板 packages/create-farm-plugin/templates/rust/src/lib.rs 给出了官方推荐的最小结构:
#![deny(clippy::all)] use farmfe_core::{config::Config, plugin::Plugin}; use farmfe_macro_plugin::farm_plugin; #[farm_plugin] pub struct MyFarmPlugin {} impl MyFarmPlugin { fn new(config: &Config, options: String) -> Self { Self {} } } impl Plugin for MyFarmPlugin { fn name(&self) -> &str { "MyFarmPlugin" } }要点:
impl Plugin for ...是必须的。Plugintrait 定义了编译全流程的钩子(config、resolve、load、transform、parse、analyze_deps、generate_resources、finish等,详见 crates/core/src/plugin/mod.rs),插件只需实现name()与需要的钩子,其余均有默认空实现;fn new(config: &Config, options: String)会被宏生成的_plugin_create调用,options是编译阶段从配置文件传入的 JSON 字符串,插件可在其中解析自己的配置(通常用serde_json::from_str);- 产物类型:插件 crate 需要在
Cargo.toml中声明crate-type = ["cdylib", "rlib"],例如 rust-plugins/virtual/Cargo.toml 的配置。
3.1 真实案例:解析 options 的插件
rust-plugins/yaml/src/lib.rs 展示了如何在new中解析 options 并把配置存入 struct 字段:
#[farm_plugin] pub struct FarmPluginYaml { document_mode: DocumentMode, include: String, exclude: String, } impl FarmPluginYaml { fn new(_config: &Config, options: String) -> Self { let yaml_options: FarmPluginYamlOptions = serde_json::from_str(&options).expect("Failed to parse YAML plugin options"); let include: String = yaml_options.include.unwrap_or_default(); let exclude: String = yaml_options.exclude.unwrap_or_default(); Self { document_mode: yaml_options.document_mode.unwrap_or(DocumentMode::Single), include, exclude, } } }rust-plugins/virtual/src/lib.rs 的FarmPluginVirtualModule同样在new中通过serde_json::from_str(&options)解析虚拟模块映射,并在后续resolve/load钩子中使用,实现 virtual module 功能。
四、宏生成代码与运行时的动态库加载协议
宏生成的导出符号与 Farm 的加载器一一对应,这是理解该宏价值的关键。
4.1 加载器如何读取符号
crates/node/src/plugin_adapters/rust_plugin_adapter/plugin_loader.rs 中:
type PluginCreate = unsafe fn(config: &Config, options: String) -> Arc<dyn Plugin>; let lib = Library::new(filename.as_ref()).unwrap(); let core_version_fn: Symbol<unsafe fn() -> String> = lib.get(b"_core_version")?; let core_version = core_version_fn(); if core_version != VERSION { panic!("Incompatible Rust Plugin: ..."); } let constructor: Symbol<PluginCreate> = lib.get(b"_plugin_create")?; let plugin = constructor(config, options);流程为:libloading打开.so/.dylib/.dll→ 获取_core_version并调用 → 与farmfe_core::VERSION比对(当前仓库中 crates/core/src/lib.rs 声明VERSION = "0.5.1")→ 不匹配直接panic!提示升级/降级插件 → 获取_plugin_create符号并以(config, options)调用 → 得到Arc<dyn Plugin>进入编译管线。
4.2 版本兼容性设计
_core_version的存在是因为 Rust 插件以动态库形式与核心共享内存布局(Arc<dyn Plugin>、钩子参数结构体等),一旦核心数据结构发生影响内存布局的改动(如增删字段),就需要同步提升版本号。因此VERSION的注释明确写道:当核心数据结构改动影响内存布局时必须升级版本(crates/core/src/lib.rs)。插件作者在升级farmfe_core依赖后需要重新构建插件,确保_core_version返回值与加载方一致。
4.3 钩子参数与结果类型也可缓存
Plugintrait 中所有钩子的参数与结果类型(如PluginResolveHookParam、PluginGenerateResourcesHookResult)都标注了#[cache_item],例如 crates/core/src/plugin/hooks/resolve.rs、crates/core/src/plugin/hooks/generate_resources.rs,这使得插件与核心之间的调用数据可以进入持久化缓存,与下一节的cache_item宏形成完整闭环。
五、cache_item:让 struct / enum 可缓存
farmfe_macro_cache_item是另一个过程宏 crate,与macro_plugin配套使用(两者的 README 内容一致,见 crates/macro_cache_item/README.md),作用是“让 struct 或 enum 可缓存”,并被 Farm 大量使用于模块、资源、钩子参数等数据结构上。
5.1 宏实现与核心逻辑
完整实现见 crates/macro_cache_item/src/lib.rs:
#[proc_macro_attribute] pub fn cache_item(attr: TokenStream, input: TokenStream) -> TokenStream { let item: Item = parse_macro_input!(input); let mut crate_name: Ident = Ident::new("crate", Span::call_site()); if !attr.is_empty() { crate_name = parse_macro_input!(attr); } let item_ident = match &item { Item::Enum(e) => e.ident.clone(), Item::Struct(s) => s.ident.clone(), _ => { let ts: proc_macro2::TokenStream = parse_quote! { compile_error!("#[cache_item] can only be used on struct or enum") }; return ts.into(); } }; let archived_item_ident = Ident::new(&format!("Archived{item_ident}"), item_ident.span()); let derives = quote! { use rkyv::*; #[derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)] #[rkyv()] #item impl #crate_name::Cacheable for #item_ident { fn serialize_bytes(&self) -> std::result::Result<Vec<u8>, String> { let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(self).unwrap(); Ok(bytes.into_vec()) } fn deserialize_bytes(bytes: Vec<u8>) -> std::result::Result<Box<dyn #crate_name::Cacheable>, String> { let archived = unsafe { rkyv::access_unchecked::<#archived_item_ident>(&bytes[..]) }; let deserialized: #item_ident = rkyv::deserialize::<#item_ident, rkyv::rancor::Error>(archived).unwrap(); Ok(Box::new(deserialized)) } } }; derives.into() }关键行为:
- 支持 struct 与 enum,其他 item 类型会触发
compile_error!("#[cache_item] can only be used on struct or enum"); - 自动为类型派生
rkyv::Serialize、rkyv::Deserialize、rkyv::Archive三个 derive,并生成Archived{TypeName}类型名; - 属性参数(如
#[cache_item(farmfe_core)])用于指定实现Cacheabletrait 时的 crate 路径:默认是crate,传入参数后改用指定路径。例如在 core 内部 crates/core/src/lib.rs 重新导出pub use farmfe_macro_cache_item::cache_item;,所以插件 / 子 crate 标注时传入farmfe_core指向 core 里的Cacheable; - 自动实现
Cacheabletrait:serialize_bytes用rkyv::to_bytes零拷贝序列化,deserialize_bytes用rkyv::access_unchecked直接访问字节切片并以rkyv::deserialize反序列化,最终装箱为Box<dyn Cacheable>。
5.2 Cacheable trait 与持久化缓存接入
Cacheabletrait 定义在 crates/core/src/cache/cacheable.rs:
pub trait Cacheable: std::any::Any + Send + Sync + Downcast { fn serialize_bytes(&self) -> Result<Vec<u8>, String>; fn deserialize_bytes(bytes: Vec<u8>) -> Result<Box<dyn Cacheable>, String> where Self: Sized; }- 内置对
i8/i16/i32/i64/u8/u16/u32/u64/f32/f64/bool/char/String与Vec<u8>的默认实现; - 编译缓存管理器
PluginCacheManager在 crates/core/src/cache/plugin_cache.rs 提供泛型方法write_cache_item<V: Cacheable>/read_cache_item<V: Cacheable>,其中读缓存时通过V::deserialize_bytes(...)还原并用downcast::<V>()转回具体类型。因此插件自定义的数据结构只要标注#[cache_item],就能直接作为插件级缓存的读写单元。
5.3 使用方式示例
在 Farm 内部大量数据结构上都使用了该宏,例如 crates/core/src/module/mod.rs、crates/core/src/resource/mod.rs、crates/core/src/cache/module_cache.rs 中的Module、Resource、ModuleCacheManager等类型。插件侧的使用如 crates/plugin_css/src/lib.rs:
#[cache_item(farmfe_core)] pub struct CssPluginConfig { // ... }六、从零构建一个可加载的 Rust 插件(实操流程)
综合上述内容,完整流程如下:
- 创建 crate 并在
Cargo.toml中声明:
[lib] crate-type = ["cdylib", "rlib"] [dependencies] farmfe_core = { version = "*", path = "../../crates/core" } farmfe_macro_plugin = { version = "*", path = "../../crates/macro_plugin" } farmfe_macro_cache_item = { version = "*", path = "../../crates/macro_cache_item" }- 编写插件主体,标注
#[farm_plugin],实现new与Plugintrait 的钩子; - 若自定义数据结构需要参与缓存,标注
#[cache_item(farmfe_core)]; - 构建出 cdylib(如
cargo build --release得到libxxx.so); - 在 Farm 配置文件中通过
rustPlugins/ 插件路径引用该动态库(插件路径为编译产物文件路径,如./node_modules/.../target/release/libxxx.so); - 运行时 Farm 的 Node 侧加载器会校验
_core_version后调用_plugin_create完成注入(见 crates/node/src/plugin_adapters/rust_plugin_adapter/mod.rs)。
仓库中 rust-plugins 目录下 20 余个官方 Rust 插件(yaml、virtual、svg、vue、sass、compress、icons、worker 等)全部采用这一模式,可作为学习范本。
七、小结与注意事项
#[farm_plugin]只接受 struct,且必须提供fn new(config: &Config, options: String) -> Self;- 宏生成的导出符号
_plugin_create/_core_version与 Node 侧加载器协议严格对应,_core_version的返回值必须与加载方farmfe_core::VERSION一致,否则加载阶段直接 panic; #[cache_item]仅适用于 struct / enum,其他类型会触发编译错误;属性参数用于指定Cacheabletrait 的 crate 路径(core 内部可省略,外部使用farmfe_core);cache_item基于 rkyv 实现零拷贝、无运行时依赖的反序列化,deserialize_bytes使用unsafe直接访问字节,因此缓存数据必须来自可信任来源(access_unchecked);- 两个宏 crate 遵循语义化版本发布,改动记录见 crates/macro_plugin/CHANGELOG.md 与 crates/macro_cache_item/CHANGELOG.md。
通过这两个宏,插件作者可以完全聚焦钩子逻辑本身,动态库导出、版本兼容与缓存序列化等底层细节全部交由宏处理,这正是 Farm Rust 插件生态能够快速扩展的基础设施。
- 前端构建
- 构建工具
- 开发工具
【免费下载链接】farm
Extremely fast Vite-compatible web build tool written in Rust
相关推荐
基于 `@farmfe/plugin-tools` 的 Farm Rust 插件构建与发布实战指南
基于 @farmfe/plugin tools 的 Farm Rust 插件构建与发布实战指南 本指南围绕 Farm 仓库中的 packages/plugin
前端构建构建工具开发工具Farm 插件系统完全指南:Rust 和 JavaScript 双模式开发终极教程
Farm 插件系统完全指南:Rust 和 JavaScript 双模式开发终极教程 Farm 是一个基于 Rust 开发的极速 Vite 兼容构建工具,其核心优
前端构建构建工具开发工具Farm插件开发终极指南:如何从零开始构建高性能Web构建工具插件
Farm插件开发终极指南:如何从零开始构建高性能Web构建工具插件 Farm是一款基于Rust开发的极速Vite兼容Web构建工具,以其出色的性能和灵活的插件系
前端构建构建工具开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考