--- 摄于 2017 年 9 月 藏川线前段
参考资料:
https://doc.rust-lang.org/proc_macro/index.html
https://doc.rust-lang.org/book/first-edition/procedural-macros.html
Rust 的宏系统,一直处于在建中,与其他语言类似的,宏的作用就是把重复代码通过宏进行整体替换,同时宏支持不定量参数。宏系统分为 1.0/1.1/2.0,几个阶段,目前 stable 的是 1.1 的宏系统,功能上来说,勉强够用,但是还是有一些操作无法满足需求,如拼接字符串作为结构名、自定义 derive等。
这时候 Rust 里面有一个更加灵活的操作,称之为过程宏(proc macro)。过程宏的操作,就相当于操作 Rust 的语法树,在编译期间把 tokenstream
解出来,加上一些东西,然后再转成 tokenstream
让编译器继续编译修改后的源码,这种写法对人更加友好,也更加美观。
proc macro
的教程相对来说比较稀少,中文的就更加匮乏,这次在 cita-cli 上使用了它,就把一些简单的逻辑写出来,权当抛砖引玉。
因为一些冗余代码的问题,cita-cli
使用了过程宏去解决,完成之后,看起源码来相当舒服,一个 derive
自动对结构体做了大量的操作,省去了很多代码,这部分实现在 tool-derive
这个 crate
里面,写得相对来说很简单,参考了 serde-derive 的实现。
首先,过程宏库的 Cargo.toml
文件需要标明这是一个 proc macro
库,并且一般来说,会使用到如下三个库的依赖:
[package]
name = "tool-derive"
version = "0.1.0"
authors = ["piaoliu <[email protected]>"]
[lib]
name = "tool_derive"
proc-macro = true
[dependencies]
proc-macro2 = "^0.4.6"
quote = "^0.6.3"
syn = "^0.14.2"
syn
:用来解析 tokenstream
,并且提供各种字符串转成 token
的方法类型quote
: 主要用到一个宏 quote!
,用来将字符串和 token
类型拼接的大字符串转换成 tokenstream
交还给编译器proc-macro2
:一些稳定的 proc macro
接口实现如果没有这三个库,单纯用标准库的 proc-macro
就真的是在拼接字符串了,会发现这东西写起来超级累,而有这三个库的话,内部进行了一些封装,提供了一些非常简单的接口,可以很快就实现想要的功能,特别是 syn
提供了大量中间类型,可以通过 string 转换,省去了很多繁琐的操作,以 Rust 的方式去操作语法树。
源码:https://github.com/driftluo/cita-cli/blob/master/tool-derive/src/lib.rs#L14
extern crate proc_macro;
extern crate proc_macro2;
extern crate syn;
#[macro_use]
extern crate quote;
use proc_macro::TokenStream;
use syn::DeriveInput;
#[proc_macro_derive(ContractExt, attributes(contract))]
pub fn contract(input: TokenStream) -> TokenStream {
let input: DeriveInput = syn::parse(input).unwrap();
...
let output = if let syn::Data::Struct(data) = input.data {
...
quote!(
...
impl #trait_name for #name {
fn create(client: Option<Client>) -> Self {
static ABI: &str = include_str!(#path);
// NOTE: This is `rootGroupAddr` address
static ADDRESS: &str = #address;
Self::new(client, ADDRESS, ABI)
}
}
)
} else {
panic!("Only impl to struct");
};
output.into()
}
上面的这一点就是核心代码了,整体流程就是:
转成 DeriveInput
=> 做各种校验 => 取出想要的 attribute 值 => 转换成 token
(quote 可识别的类型) => 构造 quote!
宏,将 token
用 #foo
这种形式表示 => 转换成 tokenstream
返回编译器。
验证的过程,就是利用了 rust 本身的机制去验证,比较不同的是,在 proc macro
内部,错误是以 panic 的形式对外展现,而不是 Result。
这个宏的使用方式如下:
源码:https://github.com/driftluo/cita-cli/blob/master/cita-tool/src/client/system_contract.rs#L12
#[macro_use]
extern crate tool_derive;
#[derive(ContractExt)]
#[contract(addr = "0x00000000000000000000000000000000013241b6")]
#[contract(path = "../../contract_abi/Group.abi")]
#[contract(name = "GroupExt")]
pub struct GroupClient {
client: Client,
address: Address,
contract: Contract,
}
把大量的重复代码都隐藏在过程宏中,这里只有 derive
的操作,非常棒。
这次探索过程宏的使用,对后面的 Rust 代码优化有非常正面的意义,抽象手段又多了一个,在宏系统相对比较简单的现在,过程宏的用法可以极大提高抽象的使用。
Rust 中还有一个更加灵活的手段,就是直接编写编译器插件,这个功能不像过程宏已经 stable 了很多功能,编译器插件目前还只能在 nightly 下使用,并且 nightly 更新版本会有极大的 break 可能,大致超过 80%,这个手段虽然更加灵活,但是维护的代价太大了,不推荐使用,如果后期我用到的话,也会将其写出来。
请登录后评论
评论区
加载更多