美文网首页
通过 clap3 源码学习 rust derive宏

通过 clap3 源码学习 rust derive宏

作者: gimmi7 | 来源:发表于2022-07-01 16:32 被阅读0次

    [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行解决,如果你自定义的命令行参数越多,压缩比也会越高。

    相关文章

      网友评论

          本文标题:通过 clap3 源码学习 rust derive宏

          本文链接:https://www.haomeiwen.com/subject/iavcbrtx.html