6

Rust 编写 derive 宏

 2 years ago
source link: https://junhaideng.github.io/2022/09/03/rust/macro/derive/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
3 天前发表Rust / Note / Macro6 分钟读完 (大约840个字)3 次访问

Rust 编写 derive 宏

宏可以帮助我们减少重复代码的编写,在 Rust 中有两种宏定义,

其中,声明宏只是简单的 token 替换,我们无法知道代码结构中的其他信息,过程宏可以获取更加详细的数据,比如我们可以获取结构体中字段的名称,类型等等。

rust-complie-process.png

下面介绍如何编写一个简单的 derive 过程宏。

首先对于过程宏来说,不能和引用的 crate 放置在同一个 crate 中,需要单独放置在一个 crate 中,同时我们需要在 Cargo.toml 中配置

[lib]
proc-macro = true

对于 derive 宏而言,即为输入为 TokenStream,输出也是 TokenStream 的函数,且输出的内容会 append 到代码中,而不会覆盖输入的内容

fn proc_marco(input: TokenStream) -> TokenStream;

我们使用 syn 对输入进行解析,获取其中的 token 信息,使用 quote 构造输出

[dependencies]
quote = "1"
syn = "1.0"

使用 proc_macro_derive 来标注这个函数是一个 derive 宏,同时也可以指定 attributes 用于指定可以在宏范围中使用的 attr,可以指定多个使用逗号进行分割。

#[proc_macro_derive(PrintField, attributes(field, typ))]
pub fn print_field(input: TokenStream) -> TokenStream;

如果要使用该宏,使用方式如下

#[derive(PrintField)]
struct Server {
#[field="hi"]
host: String,
port: u16
}

derive 宏的具体编写步骤主要分成以下三步

  1. 使用 syn 提供的方法解析输入
#[proc_macro_derive(PrintField, attributes(field, typ))]
pub fn print_field(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);
//....
}
  1. 从输入中获取到需要的信息,进行保存
#[proc_macro_derive(PrintField, attributes(field, typ))]
pub fn print_field(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);

let mut field_names = vec![];

if let syn::Data::Struct(s) = data {
if let syn::Fields::Named(f) = s.fields {
// 获取所有的字段名字,最后进行打印
for field in f.named.iter() {
field_names.push(
field
.ident
.as_ref()
.map(|ident| ident.to_string())
.unwrap_or_default(),
);
}
}
}
println!("{:?}", field_names);
//....
}

  1. 通过 quote 构造输出
#[proc_macro_derive(PrintField, attributes(field, typ))]
pub fn print_field(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);

let mut field_names = vec![];

if let syn::Data::Struct(s) = data {
if let syn::Fields::Named(f) = s.fields {
println!("{:?}", f.to_token_stream().to_string());
for field in f.named.iter() {
field_names.push(
field
.ident
.as_ref()
.map(|ident| ident.to_string())
.unwrap_or_default(),
);
}
}
}
println!("{:?}", field_names);
quote!(
impl #ident {
pub fn hello_world(&self) {
println!("Hello World")
}
}
).into()
}

函数中的 Ident 类型变量,使用 #variable_name 的方式在 quote 中进行引用,不能直接使用字符串,可以通过下面的方式进行创建

use proc_macro2::Span;
use syn::Ident;
Ident::new("fn_name", Span::call_site());

如果需要进行循环迭代,可以使用 #()* 的方式表示

// fn_name 可以理解为 Vec<Ident> 类型
impl #ident {
#(
pub fn #fn_name() {
println!("call {}", #fn_name.to_string())
}
) *
}

因为宏是在编译的过程中进行处理的,所以即使我们宏中代码实现的不高效,不影响运行时性能。编写好的宏,可以使用 cargo expand 命令进行展开,如果提示没找到该命令,使用 cargo install cargo-expand 进行安装。

简单写了一个 derive 宏自动为结构体生成 settergetter 方法,仓库:construct,可以学习参考。


生活杂笔,学习杂记,偶尔随便写写东西。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK