rust 我如何有条件地导入模块,并添加struct的示例到vec!,只有当模块(和struct)存在?

i2byvkas  于 2023-01-02  发布在  其他
关注(0)|答案(1)|浏览(237)

我不知道如何导入和示例化行,使它们能够容忍不存在的文件/模块和结构体。
我试着创建一个宏,根据它在目录中找到的文件展开成这样的行,使用我发现的一个有承诺的crate-include_optional-它允许在编译时检查已经存在的文件(因为它是一个宏)。但是,我不知道如何在宏中正确地使用它,我也没有设法使用它没有宏使用的例子在底部的docs conditional compilation一章。
if cfg!(unix) { "unix" } else if cfg!(windows) { "windows" } else { "unknown" }(来自文档)
对比
if include_optional::include_bytes_optional!("day1.rs").is_some() { Some(&day01::Day01 {}) } else { None } // assume day1.rs and thus Day01 are non-existent(我尝试做同样的事情)
我的if语句编译了这两种情况,包括无法访问的代码(导致编译错误),尽管根据文档,它应该不适用于cfg!(“条件编译”)。
本质上,我想要的是这种形式的东西:

// Macro to generate code based on how many files/structs has been created
// There are anywhere between 1-25 days
get_days_created!;
/* // Let's assume 11 have been created so far, then the macro should evaluate to this code:
 * mod day1;
 * use day1 as day0#;
 * // ...
 * mod day11;
 * use day11 as day11;
 *
 * // ...
 * fn main() -> Result<(), i32> {
 *     let days : Vec<&dyn Day> = vec![
 *         &day01::Day01 {},
 *         // ...
 *         &day11::Day11 {},
 *     ];
 *     // ...
 * }
*/
omhiaaxx

omhiaaxx1#

解决方案是创建一个proc_macro.这些函数类似于常规宏,只是它们允许您编写一个实际代码的函数,而不是给定(并返回)一个'TokenStream'来解析给定的标记(以及宏应该扩展到哪些标记)。
要创建proc_macro,你需要知道的第一条也是最重要的一条信息是你不能在任何地方都这样做。相反,你需要创建一个新的库,并在它的Cargo.toml文件中设置proc-macro = true。然后你可以在它的lib.rs中声明它们。一个TOML的例子如下所示:

[package]
name = "some_proc_macro_lib"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
glob = "0.3.0"
regex = "1.7.0"

然后你可以在这个库中创建宏作为常规函数,使用#[proc_macro]属性/注解。下面是一个lib.rs * 的例子,依赖性尽可能少 *。对于我的问题,输入TokenStream是不相关的,可以忽略,相反,你想生成并返回一个新的:

use proc_macro::TokenStream;
use glob::glob;
use regex::Regex;

#[proc_macro]
pub fn import_days(_: TokenStream) -> TokenStream {
    let mut stream = TokenStream::new();

    let re = Regex::new(r".+(\d+)").unwrap();
    for entry in glob("./src/day*.rs").expect("Failed to read pattern") {
        if let Ok(path) = entry {
            let prefix = path.file_stem().unwrap().to_str().unwrap();
            let caps = re.captures(prefix);
            if let Some(caps) = caps {
                let n: u32 = caps.get(1).unwrap().as_str().parse().unwrap();
                let day = &format!("{}", prefix);
                let day_padded = &format!("day{:0>2}", n);

                stream.extend(format!("mod {};", day).parse::<TokenStream>().unwrap());
                if n < 10 {
                    stream.extend(format!("use {} as {};", day, day_padded).parse::<TokenStream>().unwrap());
                }
            }
        }
    }

    return proc_macro::TokenStream::from(stream);
}

这个问题可以认为已经得到了回答,但我认为答案可以而且应该进一步扩展,因此我会这样做。

一些超出问题范围的其他解释和建议

但是除了proc_macro之外还有很多其他的crate可以帮助你解析输入流和构建输出流,值得注意的是依赖项synquote,为了帮助它们,有crate proc_macro2

合成器板条箱

使用syn,你可以得到有用的类型,方法和宏来解析输入Tokenstream。本质上,使用一个结构体Foo实现syn::parse::Parse和宏let foo = syn::parse_macro_input!(input as Foo),你可以更容易地将其解析为一个定制的结构体,这要归功于syn::parse::ParseStream

use proc_macro2::Ident;
use syn;
use syn::parse::{Parse, ParseStream};

#[derive(Debug, Default)]
struct Foo {
    idents: Vec<Ident>,
}
impl syn::parse::Parse for Foo {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let mut foo= Foo::default();

        while !input.is_empty() {
            let fn_ident = input.parse::<Ident>()?;
            foo.idents.push(fn_ident);
            // Optional comma: Ok vs Err doesn't matter. Just consume if it exists and ignore failures.
            input.parse::<syn::token::Comma>().ok();
        }
        return Ok(foo);
    }
}

注意,当使用sugary ?语法时,syn::Result返回类型允许解析错误的良好传播:input.parse::<SomeType>()?

报价箱

使用quote,你可以得到一个有用的宏来生成一个更类似于macro_rules的tokenstream,作为一个参数,你可以编写基本上常规的代码,并通过前缀#告诉它使用变量的值。
请注意,您不能只传递包含字符串的变量,然后期望它扩展为标识符,因为字符串解析为值"foo"(包括引号),即mod "day1";而不是mod day1;。您需要将它们转换为:

  • 一个proce_macro2::Ident
    • 一米二十七分一x
  • 或者一个proc_macro2::TokenStream
    • 1米29英寸1x

后者还允许转换具有多个Ident的较长字符串,并管理诸如文字等内容,从而可以完全跳过quote!宏,直接使用此tokenstream(如import_days中所示)。
下面的例子创建了一个动态名称的struct,并为它实现了一个特定的trait:

use proc_macro2::TokenStream;
use quote::quote;
// ...
let mut stream = TokenStream::new();
stream.extend(quote!{
    #[derive(Debug)]
    pub struct #day_padded_upper {}
    impl Day for #day_padded_upper {
        #trait_parts
    }
});

return proc_macro::TokenStream::from(stream);

最后,关于如何实施我的问题

这"章"有点多余,因为我基本上用前两个代码片段来回答它(.tomlfn import_days),其余的可以被认为是读者的练习。然而,尽管问题 * 是关于在编译时在宏中读取文件系统以"动态地"改变其扩展 (某种程度上),我写了一个更一般的形式,问如何达到一个特定的结果(因为以前的我不知道宏的可以做到这一点)。
还有一个事实是,本"章"中的最后一个宏-impl_day(问题中根本没有提到)-作为如何完成两个相邻但重要且相关的任务的一个很好的例子。
1.正在检索和使用调用站点的文件名。
1.使用syn依赖项解析input TokenStream,如上所示。
换句话说:了解以上所有内容后,
这就是如何创建宏来导入所有目标文件、示例化所有目标文件的结构体,以及从当前文件名声明+定义结构体。*

导入所有目标文件:

请参见上面开头的import_days

使用所有目标文件中的结构体示例化Vec:
#[proc_macro]
pub fn instantiate_days(_: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let re = Regex::new(r".+(\d+)").unwrap();

    let mut stream = TokenStream::new();

    let mut block  = TokenStream::new();
    for entry in glob("./src/day*.rs").expect("Failed to read pattern") {
        match entry {
            Ok(path) => {
                let prefix = path.file_stem().unwrap().to_str().unwrap();
                let caps = re.captures(prefix);
                if let Some(caps) = caps {
                    let n: u32 = caps.get(1).unwrap().as_str().parse().unwrap();
                    let day_padded = &format!("day{:0>2}", n);
                    let day_padded_upper = &format!("Day{:0>2}", n);
                    let instance = &format!("&{}::{} {{}}", day_padded, day_padded_upper).parse::<TokenStream>().unwrap();
                    block.extend(quote!{
                        v.push( #instance );
                    });
                }

            },
            Err(e) => println!("{:?}", e),
        }
    }
    stream.extend(quote!{
        {
            let mut v: Vec<&dyn Day> = Vec::new();
            #block
            v
        }
    });

    return proc_macro::TokenStream::from(stream);
}
正在为调用此宏的当前文件声明和定义结构:
#[derive(Debug, Default)]
struct DayParser {
    parts: Vec<Ident>,
}
impl Parse for DayParser {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut day_parser = DayParser::default();

        while !input.is_empty() {
            let fn_ident = input.parse::<Ident>()?;
            // Optional, Ok vs Err doesn't matter. Just consume if it exists.
            input.parse::<syn::token::Comma>().ok();
            day_parser.parts.push(fn_ident);
        }

        return Ok(day_parser);
    }
}

#[proc_macro]
pub fn impl_day(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let mut stream = TokenStream::new();

    let span = Span::call_site();
    let binding = span.source_file().path();
    let file = binding.to_str().unwrap();
    let re = Regex::new(r".*day(\d+).rs").unwrap();
    let caps = re.captures(file);
    if let Some(caps) = caps {
        let n: u32 = caps.get(1).unwrap().as_str().parse().unwrap();
        let day_padded_upper = format!("Day{:0>2}", n).parse::<TokenStream>().unwrap();

        let day_parser = syn::parse_macro_input!(input as DayParser);

        let mut trait_parts = TokenStream::new();
        for (k, fn_ident) in day_parser.parts.into_iter().enumerate() {
            let k = k+1;
            let trait_part_ident = format!("part_{}", k).parse::<TokenStream>().unwrap();
            // let trait_part_ident = proc_macro::Ident::new(format!("part_{}", k).as_str(), span);
            trait_parts.extend(quote!{
                fn #trait_part_ident(&self, input: &str) -> Result<String, ()> {
                    return Ok(format!("Part {}: {:?}", #k, #fn_ident(input)));
                }
            });
        }

        stream.extend(quote!{
            #[derive(Debug)]
            pub struct #day_padded_upper {}

            impl Day for #day_padded_upper {
                #trait_parts
            }
        });

    } else {
        // don't generate anything
        let str = format!("Tried to implement Day for a file with malformed name: file = \"{}\" , re = \"{:?}\"", file, re);
        println!("{}", str);
        // compile_error!(str); // can't figure out how to use these
    }

    return proc_macro::TokenStream::from(stream);
}

相关问题