rust 派生宏 with darling
问题分析
#[derive(CustomDebug)]
pub struct Field {
name: &'static str,
#[debug = "0b{:08b}"]
bitmask: u8,
}
你遇到了两个错误:
Name-value arguments are not supported. Use #[debug(...)]
- 这表明 Rust 无法识别#[debug = "value"]
格式,而期望#[debug(...)]
格式。cannot find attribute debug in this scope
- 这表明debug
属性没有被正确注册。
解决方案
在 Rust 的过程宏系统中,自定义属性需要特殊处理。对于派生宏上的自定义属性,我们需要使用特定的语法。让我们修改代码:
如何使用 darling 实现 CustomDebug 派生宏
你遇到的问题涉及 Rust 宏系统中对属性语法的处理方式。我已经提供了修复后的解决方案,下面我会详细解释问题的原因和解决方法。
问题根源
在 Rust 中,自定义属性有特定的语法要求:
- 原来的写法
#[debug = "0b{:08b}"]
在 Rust-darling lib 处理方式下不是有效的自定义属性语法。 - 正确的语法应该是
#[debug(format = "0b{:08b}")]
,即使用圆括号而非等号。
完整解决方案
1. 修改属性解析器
首先,我们需要修改 DebugField
结构体来正确解析属性:
#[derive(FromField)]
#[darling(attributes(debug))]
struct DebugField {
ident: Option<syn::Ident>,
ty: syn::Type,
#[darling(default)]
format: Option<String>, // 改为接收 #[debug(format = "...")] 中的格式字符串
}
这里关键的改变是将 debug_format
改为 format
,这样 darling 会尝试解析 #[debug(format = "...")]
格式的属性。
2. 正确注册属性
在派生宏的定义中,我们需要声明它使用了 debug
属性:
#[proc_macro_derive(CustomDebug, attributes(debug))]
attributes(debug)
告诉 Rust 编译器这个派生宏会处理名为 debug
的属性,这样就不会报"找不到属性"的错误。
3. 生成格式化代码
在生成代码时,我们需要使用字段的 format
属性(而非之前的 debug_format
):
match &field.format {
Some(format_str) => {
quote! {
field_parts.push(format!("{}: {}", #field_name_str,
format!(#format_str, self.#field_name)));
}
},
None => {
quote! {
field_parts.push(format!("{}: {:?}", #field_name_str, self.#field_name));
}
}
}
最终代码:
use darling::{FromDeriveInput, FromField};
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
/// 用于解析字段上的 #[debug(format = "...")] 属性
#[derive(FromField)]
#[darling(attributes(debug))]
struct DebugField {
ident: Option<syn::Ident>,
ty: syn::Type,
#[darling(default)]
format: Option<String>, // 接收 #[debug(format = "...")] 中的格式字符串
}
/// 用于解析整个结构体
#[derive(FromDeriveInput)]
#[darling(supports(struct_named, struct_tuple), attributes(debug))]
struct CustomDebugReceiver {
ident: syn::Ident,
generics: syn::Generics,
data: darling::ast::Data<(), DebugField>,
}
#[proc_macro_derive(CustomDebug, attributes(debug))]
pub fn derive_custom_debug(input: TokenStream) -> TokenStream {
// 解析输入的 TokenStream
let input = parse_macro_input!(input as syn::DeriveInput);
// 使用 darling 解析结构体及其字段
let receiver = match CustomDebugReceiver::from_derive_input(&input) {
Ok(receiver) => receiver,
Err(err) => return err.write_errors().into(),
};
// 生成 Debug 实现
let name = &receiver.ident;
let (impl_generics, ty_generics, where_clause) = receiver.generics.split_for_impl();
// 处理字段
let debug_fields = match &receiver.data {
darling::ast::Data::Struct(fields) => {
let fields = fields.iter().enumerate().map(|(i, field)| {
let field_name = match &field.ident {
Some(ident) => quote! { #ident },
None => {
let index = syn::Index::from(i);
quote! { #index }
}
};
let field_name_str = match &field.ident {
Some(ident) => ident.to_string(),
None => i.to_string(),
};
// 根据是否有自定义的格式化字符串生成不同的格式化代码
match &field.format {
Some(format_str) => {
quote! {
field_parts.push(format!("{}: {}", #field_name_str,
format!(#format_str, self.#field_name)));
}
},
None => {
quote! {
field_parts.push(format!("{}: {:?}", #field_name_str, self.#field_name));
}
}
}
});
quote! { #(#fields)* }
},
_ => panic!("CustomDebug 只支持结构体"),
};
// 生成最终的实现代码
let expanded = quote! {
impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut field_parts = Vec::new();
#debug_fields
write!(f, "{} {{ {} }}", stringify!(#name), field_parts.join(", "))
}
}
};
expanded.into()
}
正确的使用方式
有了这些修改后,你可以这样使用 CustomDebug 派生宏:
// 使用自定义的 CustomDebug 派生宏
#[derive(CustomDebug)]
pub struct Field {
name: &'static str,
#[debug(format = "0b{:08b}")] // 注意这里的语法变化
bitmask: u8,
#[debug(format = "{:.2f}°C")] // 另一个格式化示例
temperature: f32,
}
fn main() {
let field = Field {
name: "控制标志",
bitmask: 10,
temperature: 36.75,
};
println!("{:?}", field);
// 输出:Field { name: "控制标志", bitmask: 0b00001010, temperature: 36.75°C }
}
当你打印这个结构体时,输出将类似于:
Field { name: "控制标志", bitmask: 0b00001010 }
深入解释:Rust 属性系统
Rust 的属性系统基于以下语法规则:
- 外部属性:
#[attribute]
- 内部属性:
#![attribute]
属性可以有多种形式:
- 单一标识符:
#[derive]
- 标识符=字面量:
#[path = "foo.rs"]
- 标识符 (...):
#[derive(Debug)]
- 标识符 (key = value):
#[cfg(target_os = "windows")]
在过程宏中,当我们想要定义自己的属性时,必须遵循这些规则。darling 库帮助我们解析这些属性,但我们需要正确指定要解析的格式。
工作原理图解
用户代码 派生宏处理 生成代码
+----------------+ +------------------+ +------------------+
| #[derive( | | 1. 解析结构体和属性 | | impl Debug for |
| CustomDebug |---->| 2. 提取格式字符串 |---->| Field { |
| )] | | 3. 生成格式化代码 | | fmt(...) {...}|
| struct Field { | | | | } |
| #[debug(...)]| | | | |
| field: Type | | | | |
| } | | | | |
+----------------+ +------------------+ +------------------+
扩展思路
-
支持更多格式化选项:你可以扩展属性语法,例如添加对齐、填充等选项:
#[debug(format = "0b{:08b}", align = "right", width = "10")]
-
支持条件格式化:可以添加条件来决定是否格式化某个字段:
#[debug(format = "0b{:08b}", skip_if_zero = "true")]
-
支持枚举类型:当前实现只支持结构体,你可以扩展它来支持枚举。
-
自定义结构体级别的格式化:可以添加结构体级别的属性来自定义整个结构体的格式化方式。
总结
通过正确理解 Rust 的属性语法并使用 darling 库,我们成功实现了一个自定义的 CustomDebug
派生宏,它允许为每个字段指定不同的格式化字符串。关键的修复点是:
- 将属性语法从
#[debug = "..."]
更改为#[debug(format = "...")]
- 在派生宏中正确注册
debug
属性 - 在
DebugField
结构体中使用正确的字段名来接收格式字符串
这种方法使得我们可以灵活地为任何字段自定义 Debug 输出格式,提高了调试信息的可读性和实用性。
附录
同时支持 #[debug = "..."]
和 #[debug(format = "...")]
这两种语法
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::spanned::Spanned;
use syn::{parse_macro_input, Data, DeriveInput, Field, Fields};
#[proc_macro_derive(CustomDebug, attributes(debug))]
pub fn derive(input: TokenStream) -> TokenStream {
// 解析输入的 TokenStream 为 DeriveInput
let input = parse_macro_input!(input as DeriveInput);
// 获取结构体名称和泛型参数
let name = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
// 处理结构体字段
let debug_fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => {
let field_debugs = fields
.named
.iter()
.enumerate()
.map(|(i, field)| generate_field_debug(field, i, true));
quote! { #(#field_debugs)* }
}
Fields::Unnamed(fields) => {
let field_debugs = fields
.unnamed
.iter()
.enumerate()
.map(|(i, field)| generate_field_debug(field, i, false));
quote! { #(#field_debugs)* }
}
Fields::Unit => quote! {},
},
_ => {
return syn::Error::new(input.span(), "CustomDebug 只支持结构体")
.to_compile_error()
.into();
}
};
// 生成最终的实现代码
let expanded = quote! {
impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut field_parts = Vec::new();
#debug_fields
write!(f, "{} {{ {} }}", stringify!(#name), field_parts.join(", "))
}
}
};
expanded.into()
}
// 为字段生成调试代码
fn generate_field_debug(field: &Field, index: usize, is_named: bool) -> TokenStream2 {
let field_name = if is_named {
field.ident.as_ref().unwrap().to_string()
} else {
index.to_string()
};
let field_access = if is_named {
let ident = field.ident.as_ref().unwrap();
quote! { self.#ident }
} else {
let index = syn::Index::from(index);
quote! { self.#index }
};
// 查找 debug 属性并提取格式字符串
let format_str = find_debug_format(field);
match format_str {
Some(format) => {
quote! {
field_parts.push(format!("{}: {}", #field_name,
format!(#format, #field_access)));
}
}
None => {
quote! {
field_parts.push(format!("{}: {:?}", #field_name, #field_access));
}
}
}
}
// 从字段属性中查找 debug 格式字符串
fn find_debug_format(field: &Field) -> Option<String> {
for attr in &field.attrs {
if attr.path().is_ident("debug") {
// 解析 #[debug = "..."] 形式
if let Ok(value) = attr.meta.require_name_value() {
if let syn::Expr::Lit(expr_lit) = &value.value {
if let syn::Lit::Str(lit_str) = &expr_lit.lit {
return Some(lit_str.value());
}
}
continue;
}
// 解析 #[debug(format = "...")] 形式
if let Ok(meta) = attr.meta.require_list() {
let mut result = None;
let _ = meta.parse_nested_meta(|nested| {
if nested.path.is_ident("format") {
let value = nested.value()?;
let lit: syn::LitStr = value.parse()?;
result = Some(lit.value());
}
Ok(())
});
if result.is_some() {
return result;
}
}
}
}
None
}