参考前一章节的手写版生成代码部分,我们根据 IDL 中定义的结构,一对一地生成了结构体和其对应的 Message 实现,以及辅助的匿名结构体和 Client、Server 代码。
占比相当多的代码是 Message 实现。这部分我们尝试利用过程宏(确切说是继承式过程宏)来实现。
过程宏分为三种:(ref: #2)
- 派生宏(Derive macro):用于结构体(struct)、枚举(enum)、联合(union)类型,可为其实现函数或特征(Trait)。
- 属性宏(Attribute macro):用在结构体、字段、函数等地方,为其指定属性等功能。如标准库中的
#[inline]
、#[derive(...)]
等都是属性宏。 - 函数式宏(Function-like macro):用法与普通的规则宏类似,但功能更加强大,可实现任意语法树层面的转换功能。
要写一个派生宏,我们需要新创建一个 crate,并在 Cargo.toml 里指定 proc-macro = true
。
这里我们创建 mini-lust-macros
并在其 lib.rs
中实现这个宏:
#[proc_macro_derive(Message, attributes(mini_lust))]
pub fn message(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
// ...
}
我们可以通过 input 参数拿到 TokenStream
,这个 TokenStream 即被 derive 的 struct、enum 或 union 的代码。
函数最终返回 TokenStream,即额外生成的代码。
我们往往要根据输入结构做代码生成,比如我们要实现类似 Debug 宏,就要知道这个结构是 struct 还是 enum 还是 union,以及有哪些 field,它们的类型都是什么。
那么显然我们需要 parse 这个结构定义。我们可以利用 syn::parse_macro_input!
得到这个解析后的结果。
在我们的目标场景中,我们希望直接在生成的结构体上 derive 即可得到对应的 Message 实现。
由于我们的 Message 实现必须知道每个 field 的 id,而生成结构体里不可能包含这个信息。所以我们可以利用 attribute 来提供这个信息。
#[derive(::mini_lust_macros::Message)]
pub struct Friend {
#[mini_lust(field_id = 1, required = "true", field_type = "i32")]
id: i32,
}
目标生成代码:
impl ::mini_lust_chap6::Message for Friend {
fn encode<T: ::mini_lust_chap6::TOutputProtocol>(
&self,
cx: &::mini_lust_chap6::MsgContext,
protocol: &mut T,
) -> ::mini_lust_chap6::Result<()> {
protocol.write_struct_begin(&::mini_lust_chap6::TStructIdentifier {
name: "Friend".to_string(),
})?;
let inner = &self.id;
protocol.write_field_begin(&::mini_lust_chap6::TFieldIdentifier {
name: Some("id".to_string()),
field_type: ::mini_lust_chap6::TType::I32,
id: Some(1i16),
})?;
protocol.write_i32(*inner)?;
protocol.write_field_end()?;
protocol.write_field_stop()?;
protocol.write_struct_end()?;
Ok(())
}
fn decode<T: ::mini_lust_chap6::TInputProtocol>(
cx: &mut ::mini_lust_chap6::MsgContext,
protocol: &mut T,
) -> ::mini_lust_chap6::Result<Self> {
let mut field_id = None;
protocol.read_struct_begin()?;
loop {
let ident = protocol.read_field_begin()?;
if ident.field_type == ::mini_lust_chap6::TType::Stop {
break;
}
match ident.id {
Some(1i16) => {
::mini_lust_chap6::ttype_comparing(
ident.field_type,
::mini_lust_chap6::TType::I32,
)?;
let content = protocol.read_i32()?;
field_id = Some(content);
}
_ => {
protocol.skip(ident.field_type)?;
}
}
protocol.read_field_end()?;
}
protocol.read_struct_end()?;
let output = Self {
id: field_id.ok_or_else(|| {
::mini_lust_chap6::new_protocol_error(
::mini_lust_chap6::ProtocolErrorKind::InvalidData,
"field id is required",
)
})?,
};
Ok(output)
}
}
提取字段的 attribute 并不复杂,我们可以直接从 syn 解析到的结构中拿到 fields,并从 field 的 attrs 中读到所有的 attribute,并过滤出带有我们 mini_lust 标记的属性,再自行解析出来。
而这个相对通用的过程有一个叫 darling 的库可以快速帮我们完成。
我们定义 Receiver 结构体:
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(mini_lust))]
pub(crate) struct Receiver {
pub ident: syn::Ident,
pub generics: syn::Generics,
pub data: ast::Data<EnumReceiver, FieldReceiver>,
}
其中,EnumReceiver 对应 enum 情况下的接收器,FieldReceiver 对应 struct 情况下的接收器。
例如,我们可以定义 FieldReceiver:
#[derive(Debug, FromField)]
#[darling(attributes(mini_lust))]
pub(crate) struct FieldReceiver {
pub ident: Option<syn::Ident>,
pub ty: syn::Type,
pub field_type: String,
pub field_id: i32,
#[darling(default)]
pub required: Required,
}
之后我们就可以将 parse 后的结构丢进去继续解析:
let parsed = syn::parse_macro_input!(input as syn::DeriveInput);
let receiver = Receiver::from_derive_input(&parsed);
这时我们便能直接拿到解析后的 field_type 和 field_id 等标记。
与 rust 编译器打交道的时候,我们接收和返回的是 proc_macro::TokenStream
。
但是这个接口和实现过于简单,不易使用,我们一般会将其转换为 proc_macro2::TokenStream
使用。
当生成代码时,我们可以通过 quote 库来快速包装代码:
quote::quote! {
impl XXX for YYY {
// ...
}
}
这时便会生成 impl
代码对应的 TokenStream。
最后通过 .into()
可以快速将 proc_macro2::TokenStream
转换为需要的 proc_macro::TokenStream
。
我们通过:
let ts2 = quote::quote! {
impl ::mini_lust_chap6::Message for #name #generics {
fn encode<T: ::mini_lust_chap6::TOutputProtocol>(&self, cx: &::mini_lust_chap6::MsgContext, protocol: &mut T) -> ::mini_lust_chap6::Result<()> {
#tok_enc
}
fn decode<T: ::mini_lust_chap6::TInputProtocol>(cx: &mut ::mini_lust_chap6::MsgContext, protocol: &mut T) -> ::mini_lust_chap6::Result<Self> {
#tok_dec
}
}
};
proc_macro::TokenStream::from(ts2)
生成并返回代码。详细的生成细节可以参考本章附的代码 mini-lust-macros。
我们写好了 derive 宏之后要如何测试呢?可以利用 cargo-expand
来输出宏展开后的代码。
cargo install cargo-expand
之后我们可以使用 cargo expand
来展开代码。你可以尝试在本章附的 demo-derive-macro 包下执行这个命令。
至此,我们利用过程宏实现了对带 attribute 的结构体的 Message 实现生成;并通过 cargo expand 验证展开后的代码。