通过 clap3 源码学习 rust derive宏

[TOC]

通过 clap3学习rust derive宏

tldr: 本文主要介绍了 clap3 的 builder api, derive api 的使用;然后通过 derive api 源码为例学习 derive macro;

clap一个用来编写cli app的 rust 库,主要用来解析命令行的参数;下面是一个使用 clap3 builder api的例子:

clap3 builder api example (V1)

use clap::{Arg, App, Parser};

fn main() {
    let matches = App::new("Novice Program")
        .version("1.0")
        .author("cywang.master@gmail.com")
        .arg(Arg::new("hostname")
            .short('h')
            .long("hostname")
            .help("Server hostname")
            .takes_value(true)
        )
        .arg(Arg::new("port")
            .short('p')
            .long("port")
            .help("Server port (default: 6379)")
            .default_value("6379")
            .takes_value(true)
        )
        .arg(Arg::new("prod")
            .long("prod")
            .help("Active prod mode")
            .takes_value(false)
        )
        .get_matches();
    let hostname = matches.value_of("hostname").unwrap();
    let port = matches.value_of("port").unwrap();
    let prod = matches.is_present("prod");
    println!("hostname={}, port={}, prod={}", hostname, port, prod);
}

通过 clap::builder::command::App::new() 初始化一个App struct; 最后调用 get_matches() 方法去解析命令行参数到App struct;
App struct 定义如下: (只展示了常用的部分字段)

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct App<'help> {
    id: Id,
    name: String,
    author: Option<&'help str>,
    version: Option<&'help str>,
    usage_str: Option<&'help str>,
    usage_name: Option<String>,
    settings: AppFlags,
    args: MKeyMap<'help>,
}

执行 cargo run -- --help 时显示结果如下图:
第一行是 name version;第二行是 author 信息;接下来是usage信息;最后的 OPTIONS 就是我们定义的args, --help, --version是clap默认定义的;


help.png

提升 builder api 的开发体验 (V2)

上面的builder api example已经能够把参数解析到App struct了, 但开发体验特别差;
试想一下你定义完args,合作开发的同伴还需要看上面的 Arg::new("name")...才能知道
该怎么使用你定义的args; 自然而然的一个提升开发体验的方法就是把所有定义的args放到
一个struct里面,同伴只需要使用实例化的struct就可以直接使用预先定义的args.
优化后的代码只是添加了一个 Config struct,然后手动从App args里面取值然后赋值给Config的成员;

use clap::{Arg, App, Parser};

#[derive(Debug)]
#[allow(dead_code)]
struct Config {
    hostname: String,
    port: String,
    prod: bool,
}

fn main() {
    let matches = App::new("Novice Program")
        .version("1.0")
        .author("cywang.master@gmail.com")
        .arg(Arg::new("hostname")
            .short('h')
            .long("hostname")
            .help("Server hostname")
            .takes_value(true)
        )
        .arg(Arg::new("port")
            .short('p')
            .long("port")
            .help("Server port (default: 6379)")
            .default_value("6379")
            .takes_value(true)
        )
        .arg(Arg::new("prod")
            .long("prod")
            .help("Active prod mode")
            .takes_value(false)
        )
        .get_matches();
    // let hostname = matches.value_of("hostname").unwrap();
    // let port = matches.value_of("port").unwrap();
    // let prod = matches.is_present("prod");
    // println!("hostname={}, port={}, prod={}", hostname, port, prod);

    let config = Config {
        hostname: matches.value_of("hostname").unwrap().to_string(),
        port: matches.value_of("port").unwrap().to_string(),
        prod: matches.is_present("prod"),
    };
    println!("{:#?}", config)
}

使用 derive api 提升开发体验 (V3)

上面的写法太啰嗦了,定义args和定义struct是工作量的上的重复,从App struct取值手动赋值到Config也是一个很人肉的方法;于是就有了derive api来提升开发体验;代码如下:

use clap::{Arg, App, Parser};

#[derive(Debug, Parser)]
#[clap(version, author)]
struct DeriveConfig {
    #[clap(short, long, help = "Server hostname")]
    hostname: String,
    #[clap(short, long, default_value = "6379", help = "Server port (default: 6379)")]
    port: String,
    #[clap(long, help = "Active prod mode")]
    prod: bool,
}

fn main() {
    let config: DeriveConfig = DeriveConfig::parse();
    println!("{:#?}", config);
}

上面的代码非常简洁,怎么做到的呢?其实就是通过rust的 derive macro来实现上面V2版本中我们手动写的那堆builder api的代码;
上面的#[derive(Parser)] #[clap(long...)]... 相当于做了一些配置,用于编写derive宏时根据配置生成代码。

查看宏生成的代码

我们可以对 【使用 derive api 提升开发体验 (V3)】中的代码使用cargo expand 来查看rust 宏生成的代码;
这里有一个知识点,使用宏和手动解析赋值的性能完全一致,因为宏的代码生成发生在编译阶段;

通过宏代码查看整个解析赋值过程

main方法里面调用了parse方法,DeriveConfig::parse(),可以从宏生成代码里面发现这一行

impl clap::Parser for DeriveConfig {}

<span id="parse"></span>
这里给 DeriveConfig 实现了 Parser trait; 然后看看clap3源码的 parse()方法的默认实现如下:

    fn parse() -> Self {
    let mut matches = <Self as CommandFactory>::command().get_matches();
    let res = <Self as FromArgMatches>::from_arg_matches_mut(&mut matches)
        .map_err(format_error::<Self>);
    match res {
        Ok(s) => s,
        Err(e) => {
            // Since this is more of a development-time error, we aren't doing as fancy of a quit
            // as `get_matches`
            e.exit()
        }
    }
}

这里首先调用了clap::derive::CommandFactory的command方法,看看clap3的源码command方法的实现里面调用了自身的into_app方法

    fn command<'help>() -> Command<'help> {
    #[allow(deprecated)]
    Self::into_app()
}

然后看derive宏生产的代码里面对into_app的实现

impl clap::CommandFactory for DeriveConfig {
    fn into_app<'b>() -> clap::Command<'b> {
        let __clap_app = clap::Command::new("clap3");
        <Self as clap::Args>::augment_args(__clap_app)
    }
    fn into_app_for_update<'b>() -> clap::Command<'b> {
        let __clap_app = clap::Command::new("clap3");
        <Self as clap::Args>::augment_args_for_update(__clap_app)
    }
}

into_app首先调用了clap::Command::new, 和我们builder api里面的App::new等价;
然后调用了augment_args方法,看看derive宏生产的augment_args方法

 fn augment_args<'b>(__clap_app: clap::Command<'b>) -> clap::Command<'b> {
    {
        let __clap_app = __clap_app;
        let __clap_app = __clap_app.arg({
            #[allow(deprecated)]
                let arg = clap::Arg::new("hostname")
                .takes_value(true)
                .value_name("HOSTNAME")
                .required(true && clap::ArgAction::StoreValue.takes_values())
                .validator(|s| ::std::str::FromStr::from_str(s).map(|_: String| ()))
                .value_parser(clap::builder::ValueParser::string())
                .action(clap::ArgAction::StoreValue);
            let arg = arg.short('h').long("hostname").help("Server hostname");
            arg
        });
        let __clap_app = __clap_app.arg({
            #[allow(deprecated)]
                let arg = clap::Arg::new("port")
                .takes_value(true)
                .value_name("PORT")
                .required(false && clap::ArgAction::StoreValue.takes_values())
                .validator(|s| ::std::str::FromStr::from_str(s).map(|_: String| ()))
                .value_parser(clap::builder::ValueParser::string())
                .action(clap::ArgAction::StoreValue);
            let arg = arg
                .short('p')
                .long("port")
                .default_value("6379")
                .help("Server port (default: 6379)");
            arg
        });
        let __clap_app = __clap_app.arg({
            #[allow(deprecated)]
                let arg = clap::Arg::new("prod").takes_value(false);
            let arg = arg.long("prod").help("Active prod mode");
            arg
        });
        __clap_app.version("0.1.0").author("gimmi7")
    }
}

这段代码很熟悉把,就是我们通过builder api构造App struct时手动写的代码;
再看看 <a href="#parse">clap3源码的 parse()方法的默认实现</a>
执行完 command方法后和我们在 builder api 里面做的一样,调用get_matches()解析命令行参数;
按照我们优化版本的builder api,接下来需要做的事情,就是从App struct把arg取出来赋值给定义struct;
parse()方法接下来调用了 from_arg_matches_mut(&mut matches), 看看derive宏生成的from_arg_matches_mut;

    fn from_arg_matches_mut(
    __clap_arg_matches: &mut clap::ArgMatches,
) -> ::std::result::Result<Self, clap::Error> {
    #![allow(deprecated)]
    let v = DeriveConfig {
        hostname: __clap_arg_matches
            .get_one::<String>("hostname")
            .map(|s| ::std::ops::Deref::deref(s))
            .ok_or_else(|| {
                clap::Error::raw(clap::ErrorKind::MissingRequiredArgument, {
                    let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
                        &["The following required argument was not provided: "],
                        &[::core::fmt::ArgumentV1::new_display(&"hostname")],
                    ));
                    res
                })
            })
            .and_then(|s| {
                ::std::str::FromStr::from_str(s).map_err(|err| {
                    clap::Error::raw(clap::ErrorKind::ValueValidation, {
                        let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
                            &["Invalid value for ", ": "],
                            &[
                                ::core::fmt::ArgumentV1::new_display(&"hostname"),
                                ::core::fmt::ArgumentV1::new_display(&err),
                            ],
                        ));
                        res
                    })
                })
            })?,
        port: __clap_arg_matches
            .get_one::<String>("port")
            .map(|s| ::std::ops::Deref::deref(s))
            .ok_or_else(|| {
                clap::Error::raw(clap::ErrorKind::MissingRequiredArgument, {
                    let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
                        &["The following required argument was not provided: "],
                        &[::core::fmt::ArgumentV1::new_display(&"port")],
                    ));
                    res
                })
            })
            .and_then(|s| {
                ::std::str::FromStr::from_str(s).map_err(|err| {
                    clap::Error::raw(clap::ErrorKind::ValueValidation, {
                        let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
                            &["Invalid value for ", ": "],
                            &[
                                ::core::fmt::ArgumentV1::new_display(&"port"),
                                ::core::fmt::ArgumentV1::new_display(&err),
                            ],
                        ));
                        res
                    })
                })
            })?,
        prod: ::std::convert::From::from(__clap_arg_matches.is_present("prod")),
    };
    ::std::result::Result::Ok(v)
}

这段代码还是很熟悉把,就是我们在优化版本的 builder api 实现里面从 matches 取出args赋值到定义的struct的过程;
至此,clap3 derive宏做了生么事情已经完全清晰了,接下来我们就要看看怎么利用derive宏来实现自动生成这些代码;



首先是clap::Parse的proc_macro_derive定义;将TokenStream解析为AST(abstract syntax tree),然后调用derives::derive_parser(&input),生成代码

#[proc_macro_derive(Parser, attributes(clap, structopt))]
#[proc_macro_error]
pub fn parser(input: TokenStream) -> TokenStream {
    let input: DeriveInput = parse_macro_input!(input);
    derives::derive_parser(&input).into()
}

derive_parse主要调用了 gen_for_struct 方法

fn gen_for_struct(
    name: &Ident,
    generics: &Generics,
    fields: &Punctuated<Field, Comma>,
    attrs: &[Attribute],
) -> TokenStream {
    let into_app = into_app::gen_for_struct(name, generics, attrs);
    let args = args::gen_for_struct(name, generics, fields, attrs);

    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

    quote! {
        impl #impl_generics clap::Parser for #name #ty_generics #where_clause {}

        #into_app
        #args
    }
}

let into_app = into_app::gen_for_struct(name, generics, attrs);生成了 clap::CommandFactory::into_app()的代码;
let args = args::gen_for_struct(name, generics, fields, attrs);生成了 clap::FromArgMatches::from_arg_matches_mut()以及clap::Args::augment_args的代码;

生成代码的逻辑其实就是根据我们定义的struct的 fields 以及在每个filed上面写的clap attribute,来构造App struct 以及为我们自定义的 struct 赋值;

整个宏的编写过程就是: 从TokenStream -> 通过syn::parse解析得到 AST -> 根据AST编写需要生成的rust代码 -> 通过 quote::quote!(rust代码) 将rust代码转换成TokenStream;

当然这里面的难点在于没有一个足够好用的IDE能够在编写宏代码时自动提示,自动补全,所以编写宏的过程开发体验不会太好。
好处当然就很大了,就像clap derive api,可以把一个50多行的代码压缩到20行解决,如果你自定义的命令行参数越多,压缩比也会越高。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容