如何在模块文件之间使用宏?

196

我在同一个 crate 的不同文件中有两个模块,crate 启用了 macro_rules。我想在一个模块中使用另一个模块定义的宏。

// macros.rs
#[macro_export] // or not? is ineffectual for this, afaik
macro_rules! my_macro(...)

// something.rs
use macros;
// use macros::my_macro; <-- unresolved import (for obvious reasons)
my_macro!() // <-- how?

我现在遇到了编译错误 "macro undefined: 'my_macro'"... 这很有道理;宏系统在模块系统之前运行。我该如何解决这个问题?


1
你不应该使用 module::my_macro!() 吗? - u_mulder
3
不行(据我所知不是)- 根据编译器消息,模块前缀据说被忽略了。 - user
5个回答

296

同一个 crate 中的宏

新方法(自 Rust 1.32,2019-01-17)

foo::bar!();  // works

mod foo {
    macro_rules! bar {
        () => ()
    }

    pub(crate) use bar;    // <-- the trick
}

foo::bar!();  // works

使用pub use后,宏可以像任何其他项一样被使用和导入。与旧方法不同的是,这不依赖于源代码顺序,因此您可以在定义宏之前(源代码顺序)使用它。

旧方法

bar!();   // Does not work! Relies on source code order!

#[macro_use]
mod foo {
    macro_rules! bar {
        () => ()
    }
}

bar!();    // works

如果你想在同一个crate中使用宏,你需要为定义宏的模块添加属性#[macro_use]。请注意,宏只能在已经定义之后使用!



多个crate中的宏

Crate util

#[macro_export]
macro_rules! foo {
    () => ()
}

创建用户
use util::foo;

foo!();

请注意,使用这种方法,宏始终存在于crate的顶层!因此,即使 foomod bar {} 内部,user crate 仍然必须编写 use util::foo; 而不是 use util::bar::foo;。通过使用 pub use,您可以从您的crate的模块中导出一个宏(除了在根目录下导出之外)。
在 Rust 2018之前,您必须将属性 #[macro_use] 添加到 extern crate util; 语句,以从其他crate中导入宏。那样会导入 util 的所有宏。现在应该不再需要这种语法。

60
"宏必须在定义之后才能使用" - 这很关键,因为即使你已经正确执行了所有其他步骤,你仍然可能遇到这个错误。例如,假设你有模块'宏'和'foo'(使用来自'宏'的宏),并且你按字母顺序在lib.rs或main.rs中列出它们,那么foo将在宏之前加载,代码将无法编译。 - neverfox
14
谢谢您的信任,我会尽力为您进行翻译。以下是需要翻译的内容:^ pro tip - this totally got me - semore_1267
8
还需要注意的是,如果要在内部使用宏,每个模块和父模块等都应该带有#[macro_use]属性,一直到需要使用它的那个地方。 - Ten
2
这个答案对我没用。声明宏的模块有#[macro_use],并且在lib.rs中首先声明 - 仍然不起作用。@Ten的答案有所帮助,我在lib.rs的顶部添加了#[macro_use] - 然后它就起作用了。但我仍然不确定最佳实践是什么,因为我在这里读到“您不应从其他模块导入宏;您应该从定义模块导出宏”。 - Sorin Bolos
6
我经常忘记 Rust 的宏如何与模块一起使用。这是一个糟糕的系统,希望将来有一个更好的系统。 - Hutch Moore
显示剩余3条评论

66

截至1.32.0(2018版)的备选方案

请注意,虽然@lukas-kalbertodt提供的指南仍然是最新的且有效,但是对于一些人来说,必须记住宏的特殊命名空间规则可能很烦人。

从2018版开始,在Rust的版本1.32.0及以上,有另一种方法同样有效,并且在我看来有一个好处,即更容易教授(例如,它使#[macro_use]过时)。关键思想如下:

重新导出的宏与任何其他项(函数、类型、常量等)行为相同:它在重新导出发生的模块中命名空间。

  • 然后可以使用完全限定路径引用它。

  • 也可以本地使用/引入范围,以无资格方式引用它。

示例

macro_rules! macro_name { ... }
pub(crate) use macro_name; // Now classic paths Just Work™

\{\{就是这样了。相当简单,是吧?\}\}

请随意继续阅读,只有当你不怕信息过载时 ;) 我会详细解释为什么、如何以及何时会起作用。

更详细的解释

为了重新导出 (pub(...) use ...) 一个宏,我们需要引用它!这就是原回答中提到的规则有用的地方:一个宏总是可以在定义它的模块内部命名,但只有在那个定义之后才能这样做

macro_rules! my_macro { ... }
my_macro!(...); // OK

// Not OK
my_macro!(...); /* Error, no `my_macro` in scope! */
macro_rules! my_macro { ... }

基于这一点,我们可以在定义后重新导出一个宏;因此,重新导出的名称本身是与位置无关的,在Rust中所有其他全局项也是如此。
  • In the same fashion that we can do:

    struct Foo {}
    
    fn main() {
        let _: Foo;
    }
    
  • We can also do:

    fn main() {
        let _: A;
    }
    
    struct Foo {}
    use Foo as A;
    
  • The same applies to other items, such as functions, but also to macros!

    fn main() {
        a!();
    }
    
    macro_rules! foo { ... } // foo is only nameable *from now on*
    use foo as a;            // but `a` is now visible all around the module scope!
    

    And it turns out that we can write use foo as foo;, or the common use foo; shorthand, and it still works.

唯一剩下的问题是:pub(crate)还是pub
  • 对于被#[macro_export]宏导出的宏,您可以使用任何您想要的可见性,通常为pub

  • 对于其他macro_rules!宏,您不能超过pub(crate)


详细示例

  • For a non-#[macro_export]ed macro

    mod foo {
        use super::example::my_macro;
    
        my_macro!(...); // OK
    }
    
    mod example {
        macro_rules! my_macro { ... }
        pub(crate) use my_macro;
    }
    
    example::my_macro!(...); // OK
    
  • For a #[macro_export]-ed macro

    Applying #[macro_export] on a macro definition makes it visible after the very module where it is defined (so as to be consistent with the behavior of non-#[macro_export]ed macros), but it also puts the macro at the root of the crate (where the macro is defined), in an absolute path fashion.

    This means that a pub use macro_name; right after the macro definition, or a pub use crate::macro_name; in any module of that crate will work.

    • Note: in order for the re-export not to collide with the "exported at the root of the crate" mechanic, it cannot be done at the root of the crate itself.
    pub mod example {
        #[macro_export] // macro nameable at `crate::my_macro`
        macro_rules! my_macro { ... }
        pub use my_macro; // macro nameable at `crate::example::my_macro`
    }
    
    pub mod foo {
        pub use crate::my_macro; // macro nameable at `crate::foo::my_macro`
    }
    
当使用pub / pub(crate) use macro_name;时,请注意由于Rust中命名空间的工作方式,您可能还会重新导出常量/函数或类型/模块。这也会导致全局可用的宏(例如#[test]#[allow(...)]#[warn(...)]等)存在问题。
为了解决这些问题,请记住在重新导出项目时可以重命名它:
macro_rules! __test__ { ... }
pub(crate) use __test__ as test; // OK

macro_rules! __warn__ { ... }
pub(crate) use __warn__ as warn; // OK

此外,一些误报的lint可能会触发:


1
我认为这是使用不直接位于定义宏的模块上方的本地宏的唯一方法(例如,如果您在 a :: b中有一个宏,则无需#[macro_export]即可在模块 c中使用它)。这是因为c无法声明mod a :: b,而且#[macro_use]不能与诸如use super :: a :: b之类的use语句一起使用。 - Lauren Yim
1
哇,这个答案需要更多的赞。非常感谢! - Zeyi Fan
1
“重新导出的宏与任何其他项一样行为” - 基于这一点,我希望能够在一个箱子的(公共)子模块中定义一个宏,并使用完整路径从另一个箱子引用它,即像 some_name::some_module::some_macro! 这样。但如果我定义了 macro_rules! some_macro { ... },然后 pub use some_macro; 编译器会告诉我 some_macro 是私有的。我可以使用答案中展示的 pub(crate),但那样它就只对该箱子是公开的,只能使用完整路径从该箱子调用。有没有办法可以通过完整路径从另一个箱子调用它? - user4815162342
@user4815162342 重新导出无法提供比项本身更多的可见性。而未标记为#[macro_export]的宏的内在可见性最多是pub(crate)。因此,您需要将您的宏标记为#[macro_export],尽管这将使其出现在crate的根目录中。对于这个“也在crate的根目录中”的问题,没有简单的解决方法,但可以使用文档技巧来隐藏它,或者使用额外的外部帮助器crate(例如在https://crates.io/crates/konst/0.2.4/dependencies中的`konst_macro_rules`中)。 - Daniel H-M
2
最终,这就是我一直在寻找的答案。如果我在一个模块中定义了一个宏,我希望它被命名空间化到该模块中。这可能是 Rust 中最令人困惑和文档最差的功能之一,尤其是宏。 - Randall Coding
显示剩余2条评论

26

截至 Rust 1.1.0-stable,本答案已过时。


您需要在 macros.rs 文件顶部添加 #![macro_escape],并按照“宏指南”中的说明使用 mod macros; 进行包含。

$ cat macros.rs
#![macro_escape]

#[macro_export]
macro_rules! my_macro {
    () => { println!("hi"); }
}

$ cat something.rs
#![feature(macro_rules)]
mod macros;

fn main() {
    my_macro!();
}

$ rustc something.rs
$ ./something
hi

供参考,

$ rustc -v
rustc 0.13.0-dev (2790505c1 2014-11-03 14:17:26 +0000)

我完全错过了那个属性。谢谢! - user
5
顺便提一下,这里不需要使用#[macro_export]属性。只有当宏应该被导出给外部 crate 的用户时才需要使用它。如果宏仅在 crate 内部使用,则不需要使用#[macro_export] - Vladimir Matveev
1
非常感谢您的回答。我只想补充一点,如果您的 something.rs 文件使用其他模块,例如 mod foobar;,并且这个 foobar 模块使用了 macro.rs 中的宏,则必须在 mod foobar; 之前放置 mod macro; 才能使程序编译通过。虽然这只是一个小问题,但在我看来并不明显。 - conradkleinespel
3
(注:此答案已过时;我接受了Lukas提供的最新答案) - user

12

在包含宏定义的文件顶部添加#![macro_use]将导致所有宏都被引入到main.rs中。

例如,假设这个文件名为node.rs:

#![macro_use]

macro_rules! test {
    () => { println!("Nuts"); }
}

macro_rules! best {
    () => { println!("Run"); }
}

pub fn fun_times() {
    println!("Is it really?");
}

您的 main.rs 文件可能如下所示:

mod node;  //We're using node.rs
mod toad;  //Also using toad.rs

fn main() {
    test!();
    best!();
    toad::a_thing();
}

最后假设你有一个名为toad.rs的文件,它也需要这些宏:

use node; //Notice this is 'use' not 'mod'

pub fn a_thing() {
  test!();

  node::fun_times();
}

请注意,一旦使用mod将文件引入到main.rs中,剩下的文件通过use关键字可以访问它们。


你确定吗?这个 #![macro_use](不是#[macro_use])的文档在哪里?我找不到它。它在这里不起作用。 - Markus
@Markus 注意,#![macro_use] 声明在宏模块内部而不是外部。#![...] 语法表示属性适用于包含它们的作用域,例如 #![feature(...)](显然如果写成 #[feature(...)] 就没有意义了;它会在语义上要求编译器在创建特定项时启用某些功能,而不是整个根 crate)。所以,正如 @LukeDupin 所说,模块系统是一团糟,尽管可能出现的原因与首次看到的不同。 - user
我希望这个答案提到了这种构造并不完全符合惯用语的说法(除此之外,我很喜欢这个答案)。尽管它不太符合惯用语,但它很有趣,因为将其与惯用形式放在一起,就会痛苦地意识到宏与模块系统的交互方式与通常的构造方式不同。或者至少散发出强烈的气味(正如@Markus刚才表达的不满)。 - user
这是因为在 main.rs 中,mod node; 这一行出现在 mod toad; 之前。使用 rustfmt 时要小心,因为它可能会按字典顺序重新排列这些行。对我来说,解决方法是在这些行之间加入一个空白的新行。 - Sebastian
宏只能在定义后使用。 - Lucius Hu
显示剩余3条评论

10

我在 Rust 1.44.1 中 遇到了同样的问题,而这个解决方案适用于后续版本(已知可用于 Rust 1.7)。

假设您有一个新项目:

src/
    main.rs
    memory.rs
    chunk.rs

main.rs 中,你需要注明你从源代码导入宏,否则它对你没有用。
#[macro_use]
mod memory;
mod chunk;

fn main() {
    println!("Hello, world!");
}

因此,在 memory.rs 中,您可以定义宏,而不需要注释:

macro_rules! grow_capacity {
    ( $x:expr ) => {
        {
            if $x < 8 { 8 } else { $x * 2 }
        }
    };
}

最后,您可以在 chunk.rs 中使用它,您不需要在此处包含宏,因为它已在 main.rs 中完成:

grow_capacity!(8);
这篇被点赞的回答 造成了我一些困惑,这个示例文档 可以帮助理解。
注意: 这个解决方案是可行的,但请注意,正如@ineiti 在评论中指出的,main.rs/lib.rs 中声明 mod 的顺序很重要,在宏模块声明之后声明的所有模块都将无法成功调用该宏。

4
被点赞的回答将宏定义和导入语句放在同一个位置,所以让我感到困惑。我在定义中使用了 #[macro_use]。编译器没有报错说它被放错了地方。 - knh190
1
谢谢你的回答!我也被接受的答案搞糊涂了,直到看了你的解释才明白。 - Prgrm.celeritas
@Shepmaster,你提供的链接中没有提到宏的工作原理。你是不是想链接到书中的其他部分? - detly
1
@detly 不是的,因为我的评论指出的问题比宏更广泛。这个回答者似乎对 mod { ... }mod some_file 是相同的并且都创建了一个模块感到困惑。已经有一个被接受的答案展示了 #[macro_use] 的用法,所以_这个_答案并没有提供任何新的东西。 - Shepmaster
2
还要确保在 main.rs 中正确使用 mod 的顺序。如果你写成了 mod chunk; mod memory;,那么在 memory.rs 中的宏调用将会失败。 - ineiti
我认为这也是最清晰的答案!被接受的答案对我来说不够清晰,我没有看到我必须在pub (crate)#[macro_use]中使用,并且值得注意的是您永远不需要在其他文件中包含宏。 - Sasha Kondrashov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接