【问题标题】:How to generate complex enum variants with a macro in rust如何使用 rust 中的宏生成复杂的枚举变体
【发布时间】:2017-05-24 14:04:15
【问题描述】:

我正在编写一个用于解析 OVPN 配置文件的小库。 OVPN 配置文件有这种格式

command arg1 arg2
othercommand arg1 arg2

有一组固定的命令,其中一些具有可选参数。我想将解析的命令表示为枚举。所以上面的内容最终可能会这样表示:

enum ConfigDirective{
    Command{arg1: String},
    OtherCommand{arg1: String, optinal_arg1: Option<String>},
}

fn parse_line(command: String, args: Vec<String>) -> ConfigDirective {
    match command {
        "command" => ConfigDirective::Command{arg1: args[0]},
        "other_command" => ConfigDirective:OtherCommand{arg1: args[0], optional_arg1: args.get(1),
    }
}

我喜欢这种结构,但有很多可能的命令(大约 280 个区域)。所以我想写一个宏来生成大部分样板文件。理想情况下,我会写如下内容:

define_config_directive!{
    {command => "command1", rust_name => CommandOne, args => [arg1], optional_args => []},    
    {command => "other_command", rust_name => OtherCommand, args => [arg1], optional_args => [optional_arg1]},
}

到目前为止,我能做到的最接近的是:

macro_rules! define_config_directives {
    ($({
        rust_name => $rust_name:ident,
        required => [$($required:ident),*],
        optional => [$($optional:ident),*]
    }),*) => {
        #[derive(PartialEq, Eq, Debug)]
        pub enum ConfigDirective {
            $($rust_name{
                $($required: String),*,
                $($optional: Option<String>),*,
            }),*
        }
    };
}

所以我有几个问题:

  1. 我不知道如何在这个宏中实现 parse_line 函数,我需要遍历每个必需的参数,以便编写一些代码将相应的参数拉出行外,对于可选参数也是如此李>
  2. 我不知道如何处理根本没有参数的情况,理想情况下这将是一个没有字段的简单枚举变体。

有谁知道是否有办法解决稳定生锈的问题?还是我应该只使用 python 脚本生成代码?

【问题讨论】:

  • 注意:宏和外部脚本之间的中间步骤是build.rs 文件。使用cargo build 时,cargo 将首先编译并执行build.rs(如果存在),然后再编译其余的 crate,因此您可以轻松使用build.rs 生成 Rust 代码,而无需涉及任何 3rd 方工具/makefile。跨度>

标签: macros rust


【解决方案1】:

这是一个有点病态的案例。首先,您希望以不同的方式处理输入的部分,这是宏不擅长的。更糟糕的是,您希望与生成枚举变体一起执行此操作,而宏也不擅长这些变体。合起来只剩下一种方法,据我所知:完全下推生成。

简短的版本是:将其分解为简单的匹配步骤,其中每个步骤处理一件事,并将该一件事的输出添加到累加器(在本例中为 $eout$pout)。当您没有输入时,将累加器转储到您的输出中。

macro_rules! define_config_directive {
    // Start rule.
    // Note: `$(,)*` is a trick to eat any number of trailing commas.
    ( $( {$($cmd:tt)*} ),* $(,)*) => {
        // This starts the parse, giving the initial state of the output
        // (i.e. empty).  Note that the commands come after the semicolon.
        define_config_directive! { @parse {}, (args){}; $({$($cmd)*},)* }
    };

    // Termination rule: no more input.
    (
        @parse
        // $eout will be the body of the enum.
        {$($eout:tt)*},
        // $pout will be the body of the `parse_line` match.
        // We pass `args` explicitly to make sure all stages are using the
        // *same* `args` (due to identifier hygiene).
        ($args:ident){$($pout:tt)*};
        // See, nothing here?
    ) => {
        #[derive(PartialEq, Eq, Debug)]
        enum ConfigDirective {
            $($eout)*
        }

        fn parse_line(command: &str, $args: &[&str]) -> ConfigDirective {
            match command {
                $($pout)*
                _ => panic!("unknown command: {:?}", command)
            }
        }
    };

    // Rule for command with no arguments.
    (
        @parse {$($eout:tt)*}, ($pargs:ident){$($pout:tt)*};
        {
            command: $sname:expr,
            rust_name: $rname:ident,
            args: [],
            optional_args: [] $(,)*
        },
        $($tail:tt)*
    ) => {
        define_config_directive! {
            @parse
            {
                $($eout)*
                $rname,
            },
            ($pargs){
                $($pout)*
                $sname => ConfigDirective::$rname,
            };
            $($tail)*
        }
    };

    // Rule for other commands.
    (
        @parse {$($eout:tt)*}, ($pargs:ident){$($pout:tt)*};
        {
            command: $sname:expr,
            rust_name: $rname:ident,
            args: [$($args:ident),* $(,)*],
            optional_args: [$($oargs:ident),* $(,)*] $(,)*
        },
        $($tail:tt)*
    ) => {
        define_config_directive! {
            @parse
            {
                $($eout)*
                $rname { $( $args: String, )* $( $oargs: Option<String>, )* },
            },
            ($pargs){
                $($pout)*
                $sname => {
                    // This trickery is because macros can't count with
                    // regular integers.  We'll just use a mutable index
                    // instead.
                    let mut i = 0;
                    $(let $args = $pargs[i].into(); i += 1;)*
                    $(let $oargs = $pargs.get(i).map(|&s| s.into()); i += 1;)*
                    let _ = i; // avoid unused assignment warnings.

                    ConfigDirective::$rname {
                        $($args: $args,)*
                        $($oargs: $oargs,)*
                    }
                },
            };
            $($tail)*
        }
    };
}

define_config_directive! {
    {command: "command1", rust_name: CommandOne, args: [arg1], optional_args: []},    
    {command: "other_command", rust_name: OtherCommand, args: [arg1], optional_args: [optional_arg1]},
}

fn main() {
    println!("{:?}", parse_line("command1", &["foo"]));
    println!("{:?}", parse_line("other_command", &["foo"]));
    println!("{:?}", parse_line("other_command", &["foo", "bar"]));
}

不,你不能避免累加器的事情,因为宏不能直接扩展为枚举变体。因此,您必须在一个步骤中扩展整个枚举定义。

【讨论】:

    猜你喜欢
    • 2021-01-04
    • 1970-01-01
    • 2021-09-26
    • 1970-01-01
    • 1970-01-01
    • 2023-02-14
    • 1970-01-01
    • 2023-01-03
    • 1970-01-01
    相关资源
    最近更新 更多