#proc-macro #macro #magic

macro_magic

允许跨模块、文件和crate边界导出和导入项的标记

31个版本

0.5.1 2024年7月4日
0.5.0 2023年10月10日
0.4.3 2023年10月10日
0.4.2 2023年7月24日
0.2.11 2023年3月31日

#16 in 过程宏

Download history 27724/week @ 2024-05-03 32696/week @ 2024-05-10 29812/week @ 2024-05-17 40756/week @ 2024-05-24 34731/week @ 2024-05-31 28734/week @ 2024-06-07 28838/week @ 2024-06-14 36316/week @ 2024-06-21 28007/week @ 2024-06-28 31501/week @ 2024-07-05 40896/week @ 2024-07-12 40994/week @ 2024-07-19 39896/week @ 2024-07-26 38344/week @ 2024-08-02 52277/week @ 2024-08-09 31474/week @ 2024-08-16

168,072 每月下载量
340 个crate中使用 (13直接使用)

MIT 许可证

65KB
961

Macro Magic 🪄

Crates.io docs.rs Build Status MIT License

此crate提供 #[export_tokens] 宏以及一系列伴随宏,包括 #[import_tokens_proc]#[import_tokens_attr] 宏。当与 #[export_tokens] 结合使用时,这些宏允许您创建常规和属性过程宏,在这些宏中,您可以通过名称/路径引用来导入和使用在其他模块、文件甚至其他crate中标记为 #[export_tokens] 的外部/外部项的标记。

除此之外,macro_magic 引入的模式还可以用于在相同文件内以及跨文件和crate边界安全高效地导出和导入项标记。唯一的要求是您可以控制(即可以向两个位置的源代码添加属性宏)。

macro_magic 被设计用于稳定版Rust,并且完全兼容 no_std(事实上,有一个单元测试来确保一切都是 no_std 安全的)。

通用语法

您可以使用 macro_magic 构建如下所示的常规和属性过程宏

#[my_attribute(path::to::MyItem)]
trait SomeTrait {
    // ..
}

这个

do_something!(path::to::MyItem);

或者甚至是这个

let foreign_tokens = my_macro!(path::to::MyItem);
assert_eq!(foreign_tokens.to_string(), "struct MyItem {...}");

其中 path::to::MyItem 是指向一个标记有 #[export_tokens] 的项的路径。

所有这些行为都是在幕后使用 proc 宏创建的,这些宏基于 macro_rules 创建回调,但作为程序员,这种复杂性完全通过你可以应用于 proc 宏的简单属性宏来隐藏,从而赋予它们基于路径导入外部项标记的能力。

属性示例

你可以编写一个属性宏,将一个结构体的字段“注入”到另一个结构体中,如下所示

#[import_tokens_attr]
#[proc_macro_attribute]
pub fn combine_structs(attr: TokenStream, tokens: TokenStream) -> TokenStream {
    let foreign_struct = parse_macro_input!(attr as ItemStruct);
    let local_struct = parse_macro_input!(tokens as ItemStruct);
    let Fields::Named(local_fields) = local_struct.fields else {
        return Error::new(
            local_struct.fields.span(),
            "unnamed fields are not supported"
        ).to_compile_error().into()
    };
    let Fields::Named(foreign_fields) = foreign_struct.fields else {
        return Error::new(
            foreign_struct.fields.span(),
            "unnamed fields are not supported"
        ).to_compile_error().into()
    };
    let local_fields = local_fields.named.iter();
    let foreign_fields = foreign_fields.named.iter();
    let attrs = local_struct.attrs;
    let generics = local_struct.generics;
    let ident = local_struct.ident;
    let vis = local_struct.vis;
    quote! {
        #(#attrs)
        *
        #vis struct #ident<#generics> {
            #(#local_fields),
            *
            ,
            #(#foreign_fields),
            *
        }
    }
    .into()
}

然后你可以像这样使用 #[combine_structs] 属性

#[export_tokens]
struct ExternalStruct {
    foo: u32,
    bar: u64,
    fizz: i64,
}

#[combine_structs(ExternalStruct)]
struct LocalStruct {
    biz: bool,
    baz: i32,
}

这将导致以下展开输出 LocalStruct

struct LocalStruct {
    foo: u32,
    bar: u64,
    fizz: i64,
    biz: bool,
    baz: i32,
}

请注意,由于 #[import_tokens_attr] 的力量,combine_structs proc 宏上的 attr 变量将接收 ExternalStruct 项的实际标记,而不仅仅是接收 ExternalStruct 路径的标记。

这让你能够编写接收两个项标记的属性宏,一个通过第一个参数 attr 指定,以及通过第二个参数 tokens 指定的属性附加项的标记。唯一的要求是 attr 指定的项已标记为 #[export_tokens]

Proc 宏示例

你可以编写一个 PHP/ruby/crystal 风格的文本导入/require 宏,该宏盲目地将指定外部模块的标记导入当前上下文(具有所有好与坏的含义),如下所示

#[import_tokens_proc]
#[proc_macro]
pub fn require(tokens: TokenStream) -> TokenStream {
    let external_mod = parse_macro_input!(tokens as ItemMod);
    let Some((_, stmts)) = external_mod.content else {
        return Error::new(
            external_mod.span(),
            "cannot import tokens from a file-based module since custom file-level \
            attributes are not yet supported by Rust"
        ).to_compile_error().into()
    };
    quote! {
        #(#stmts)
        *
    }
    .into()
}

然后你可以像这样使用 require!

// in some external crate
#[export_tokens]
mod an_external_module {
    fn my_cool_function() -> u32 {
        567
    }

    fn my_other_function() -> u32 {
        116
    }
}
// in another crate where we will use the `require!` macro
mod my_module {
    use my_macros::require;

    fn existing_stuff() {
        println!("foo!");
    }

    require!(external_crate::an_external_module);
}

这将导致在 my_modulerequire! 的以下展开

mod my_module {
    use my_macros::require;

    fn existing_stuff() {
        println!("foo!");
    }

    fn my_cool_function() -> u32 {
        567
    }

    fn my_other_function() -> u32 {
        116
    }
}

请注意,这个假设的 require! 宏有两个危险之处

  • 你可能通过在外国模块中使用的 use 语句引入了任何类型,这些类型可能在新的上下文中可用,也可能不可用,这取决于是否有额外的使用语句。
  • 如果你在 require! 宏中使用的模块或上下文中现有的项与你导入的项冲突,你会得到编译器错误(尽管这是好事)。

这些只是 macro_magic一些 功能 🪄。

有关更多信息,请参阅 docs

功能

proc_support

在利用任何导入令牌功能(包括 #[import_tokens_attr]#[import_tokens_proc]import_tokens!)的 proc 宏 crate 中,必须启用 proc_support 功能。否则,这些宏将无法正常工作,并会引发编译器错误,抱怨在 macro_magic::mm_core 下不存在项目。宏 #[export_tokens] 不需要此功能即可正常工作,因此您可以安全地使用它而无需启用此功能。

此功能门控的原因是,像 synquoteproc_macro2 等功能并不完全兼容 no_std,并且仅应在 proc 宏 crate 中启用。因此,您不应该在仅使用 #[export_tokens] 而没有其他内容的 crate 中启用此功能。

限制

macro_magic 无法 提供在多个宏调用之间建立状态信息的能力,但可以使用 外部宏模式 或在某些情况下,在您的 proc 宏 crate 中使用静态原子和互斥锁(我们在实际中就是这样做的,以跟踪唯一标识符)来有效地解决这个问题。

破坏性变更

  • 0.4x 删除了 #[use_attr]#[use_proc](它们在新 0.4x 版本中不再需要),并且还删除了在函数内部和模块权限边界(例如在不可访问的私有模块中)等不可访问位置访问 #[export_tokens] 调用的能力。如果感兴趣,此功能可能会在未来重新添加,但删除它使我们能够统一我们的 macro_rules! 声明命名,并消除对 #[use_attr] / #[use_proc] 的需求。
  • 0.2x 删除或重写了一些依赖于 OUT_DIR 中写入/读取文件的非未来兼容行为的特性。版本 >= 0.2.0 完全安全,不再包含此行为。然而,提供在命名空间中枚举所有 #[export_tokens] 调用的能力的特性已被删除。正确的方法是使用上述的外部宏模式或在您的 proc 宏 crate 中使用全局状态互斥锁/原子,如前所述。

更详细的历史变更信息可以在 发布页面 找到。

依赖项

~0.4–1MB
~21K SLoC