如何在过程宏中调用另一个过程宏?含跨crate调用场景
proc_macro_derive宏 你说得没错,#[proc_macro_derive]标记的函数确实是公开的,但它们的调用逻辑和普通函数完全不同——这类宏是编译器在代码生成阶段触发的入口,而不是可以在宏展开阶段直接调用的常规函数。不过想要在自己的宏里直接复用第三方derive宏的逻辑,还是有可行方案的,下面分情况给你讲:
为什么直接调用会失败?
#[proc_macro_derive]函数的签名是固定的fn(TokenStream) -> TokenStream,但它们属于proc-macro专属的编译单元,和普通Rust代码的编译流程是分离的。编译器会单独编译proc-macro crate,然后在处理#[derive(...)]属性时调用对应的函数,但普通代码(包括你的自定义过程宏)默认无法直接链接到这些函数——除非你明确处理依赖和导入逻辑。
可行解决方案
方案1:优先使用第三方暴露的内部代码生成函数
很多成熟的proc-macro crate会把核心的代码生成逻辑抽成独立的公共函数,而不是只通过proc_macro_derive入口暴露。比如一些crate会提供internals模块,里面包含可以直接调用的生成函数,比直接调用derive入口更灵活(比如支持自定义参数、AST节点处理)。
举个例子,如果第三方cratefoo_derive的#[proc_macro_derive(Foo)]实际上是调用了foo_derive::internals::generate_foo_impl,那你可以直接在宏里调用这个内部函数:
use proc_macro::TokenStream; use quote::quote; use syn::DeriveInput; use foo_derive::internals::generate_foo_impl; #[proc_macro] pub fn my_macro(input: TokenStream) -> TokenStream { let ast = syn::parse_macro_input!(input as DeriveInput); // 直接调用内部生成函数,传入AST节点 let foo_impl = generate_foo_impl(&ast); // 合并自己的生成代码和第三方的代码 let output = quote! { #ast #foo_impl // 你的自定义宏生成的代码 impl MyCustomTrait for #ast.ident { // ... } }; output.into() }
方案2:直接调用第三方的proc_macro_derive导出函数
如果第三方没有暴露内部函数,你也可以直接调用它的proc_macro_derive入口函数——因为#[proc_macro_derive]会强制标记函数为公开的。步骤如下:
- 正确依赖第三方proc-macro crate:在你的Cargo.toml里添加依赖,确保它是正常的
dependencies而不是build-dependencies,并且版本兼容。 - 导入derive函数:找到第三方crate导出的derive函数名(比如
derive_foo,对应#[proc_macro_derive(Foo)]),直接导入。 - 构造正确的输入TokenStream:
proc_macro_derive函数的输入是去掉#[derive(Foo)]属性后的结构体/枚举的TokenStream,所以你需要把要处理的AST转成TokenStream传入。 - 合并输出:把第三方生成的TokenStream和你自己的宏输出合并。
示例代码:
use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; // 导入第三方的derive函数(注意函数名要对应) use foo_derive::derive_foo; #[proc_macro] pub fn my_macro(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); // 将AST转为TokenStream,作为第三方derive函数的输入 let derive_input = quote! { #ast }.into(); // 调用第三方derive函数,生成代码 let foo_impl = derive_foo(derive_input); // 合并所有代码 let output = quote! { #ast #foo_impl // 你的自定义代码 }; output.into() }
注意:如果第三方的derive宏需要额外的属性(比如#[proc_macro_derive(Foo, attributes(foo_attr))]),你需要在构造的AST里包含这些属性,否则生成的代码可能不符合预期。
方案3:退而求其次,生成#[derive(...)]代码让编译器处理
如果以上两种方案都不可行(比如第三方完全不暴露任何可调用的API),那只能回到你提到的方法:在你的宏里生成带有#[derive(ThirdPartyMacro)]的代码,让编译器后续处理这个derive。虽然这不是你想要的直接调用,但也是可靠的方案。
示例:
use proc_macro::TokenStream; use quote::quote; use syn::DeriveInput; #[proc_macro] pub fn my_macro(input: TokenStream) -> TokenStream { let ast = syn::parse_macro_input!(input as DeriveInput); let output = quote! { // 添加第三方derive属性 #[derive(ThirdPartyMacro)] #ast // 你的自定义代码 impl MyCustomTrait for #ast.ident { // ... } }; output.into() }
关键注意事项
- 版本兼容:确保你的宏使用的
syn、quote版本和第三方proc-macro crate的版本一致,否则会出现TokenStream不兼容的问题(不同版本的syn生成的TokenStream结构可能不同)。 - 错误处理:调用第三方derive函数时,要处理可能的错误(比如用
proc_macro::Diagnostic输出清晰的错误信息),避免编译失败时出现混乱的报错。 - 文档检查:先查看第三方crate的文档,确认是否有推荐的宏复用方式——很多crate会明确说明如何在自定义宏中复用它们的逻辑。
内容的提问来源于stack exchange,提问作者synek317

