< 返回版块

2019-03-21 12:17    责任编辑:Mike

本文转载自:https://www.driftluo.com/admin/article/view?id=348befe3-da7e-4f58-b720-a0c47e8c8165

Rust proc macro 初试

参考资料: https://doc.rust-lang.org/proc_macro/index.html

https://doc.rust-lang.org/book/first-edition/procedural-macros.html

Rust 宏系统

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 配置

首先,过程宏库的 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%,这个手段虽然更加灵活,但是维护的代价太大了,不推荐使用,如果后期我用到的话,也会将其写出来。