【问题标题】:Is it possible to declare variables procedurally using Rust macros?是否可以使用 Rust 宏以程序方式声明变量?
【发布时间】:2014-05-28 12:11:28
【问题描述】:

这个问题基本上分为两部分:

  1. 您能否将未知标识符传递给Rust 中的宏?

  2. 能否在 Rust 宏中组合字符串来生成新的变量名?

例如:

macro_rules! expand(
  ($x:ident) => (
    let mut x_$x = 0;
  )
)

调用 expand!(hi) 明显失败,因为 hi 是未知标识符;但你能以某种方式做到这一点吗?

即。 C 中的等价物:

#include <stdio.h>
#define FN(Name, base) \
  int x1_##Name = 0 + base; \
  int x2_##Name = 2 + base; \
  int x3_##Name = 4 + base; \
  int x4_##Name = 8 + base; \
  int x5_##Name = 16 + base;

int main() {
  FN(hello, 10)
  printf("%d %d %d %d %d\n", x1_hello, x2_hello, x3_hello, x4_hello, x5_hello);
  return 0;
}

你为什么这么说,多么糟糕的想法。你为什么要这样做?

很高兴你问到!

考虑一下这个锈块:

{
   let marker = 0;
   let borrowed = borrow_with_block_lifetime(data, &marker); 
   unsafe {
      perform_ffi_call(borrowed);
   }
}

您现在有了一个没有使用结构生命周期的显式限定生命周期(标记)的借用值,但我们可以保证在 ffi 调用的整个范围内都存在;同时,我们不会遇到晦涩的错误,即 * 在不安全的块内被不安全地取消引用,因此编译器不会将其作为错误捕获,尽管错误是在安全的 阻止

(另见Why are all my pointers pointing to the same place with to_c_str() in rust?

使用可以为此目的声明临时变量的宏将大大减轻我与编译器的麻烦。这就是我想这样做的原因。

【问题讨论】:

    标签: macros rust


    【解决方案1】:

    是的,但这仅作为夜间实验性 API 提供,可能会被删除。

    您可以将任意标识符传递给宏,是的,您可以使用 concat_idents!() 宏将标识符连接成一个新标识符:

    #![feature(concat_idents)]
    
    macro_rules! test {
        ($x:ident) => ({
            let z = concat_idents!(hello_, $x);
            z();
        })
    }
    
    fn hello_world() {  }
    
    fn main() {
        test!(world);
    }
    

    但是,据我所知,因为concat_idents!() 本身是一个宏,所以你不能在任何地方使用这个连接标识符,你只能在某些地方使用普通标识符,例如上面的例子,在我看来,这是一个巨大的缺点。就在昨天,我尝试编写一个可以删除代码中大量样板的宏,但最终我无法做到,因为宏不支持任意放置连接标识符。

    顺便说一句,如果我正确理解您的想法,您实际上并不需要连接标识符来获得唯一名称。 Rust 宏,与 C 的相反,是 hygienic。这意味着在宏中引入的所有局部变量的名称都不会泄漏到调用该宏的范围内。例如,您可以假设以下代码可以工作:

    macro_rules! test {
        ($body:expr) => ({ let x = 10; $body })
    }
    
    fn main() {
        let y = test!(x + 10);
        println!("{}", y);
    }
    

    也就是说,我们创建一个变量x,并在其声明后放置一个表达式。然后很自然地认为test!(x + 10)中的x指的是宏声明的那个变量,一切都应该没问题,但实际上这段代码不会编译:

    main3.rs:8:19: 8:20 error: unresolved name `x`.
    main3.rs:8     let y = test!(x + 10);
                                 ^
    main3.rs:3:1: 5:2 note: in expansion of test!
    main3.rs:8:13: 8:27 note: expansion site
    error: aborting due to previous error
    

    因此,如果您只需要本地人的唯一性,那么您可以安全地什么都不做并使用您想要的任何名称,它们将自动是唯一的。这是宏教程中的explained,尽管我发现那里的示例有些混乱。

    【讨论】:

    • concat_idents 可能被我删除了,它已经是功能门控 #13295#13294
    • 我们真的需要这个类似于 C 语言中## 的功能。我经常不得不编写宏,用户给我两个名字,因为字符串 concat 非常有限。
    • Rust 宏变量不能泄漏的说法是不公平的,因为 Rust 宏声明总是需要包装 {}(或其他分隔符)。在 C 中使用相同的语法,您仍然不会泄漏符号。
    • @SOFe 请注意,上面的示例适用于 C: onlinegdb.com/SkhTzjQ-r 。这个想法是,在 C 中,只要您知道宏定义,就可以很容易地访问由宏定义的变量。在 Rust 中你不能,并且是否有任何块并不真正相关。
    • 有什么办法可以生成很多函数,比如handler_0, handler_1, handler_2, ...,下划线后面加上literal
    【解决方案2】:

    concat_idents 不起作用的情况下(大多数情况下我想使用它)将问题从连接标识符更改为使用命名空间确实有效。

    也就是说,代替非工作代码:

    macro_rules! test {
        ($x:ident) => ({
            struct concat_idents!(hello_, $x) {}
            enum contact_idents!(hello_, $x) {}
        })
    }
    

    用户可以命名命名空间,然后有如下图所示的预设名称:

    macro_rules! test {
        ($x:ident) => ({
            mod $x {
                struct HelloStruct {}
                enum HelloEnum {}
            }
        })
    }
    

    现在您有了一个基于宏参数的名称。此技术仅在特定情况下有用。

    【讨论】:

      【解决方案3】:

      还有https://github.com/dtolnay/paste,它适用于concat_idents 功能不足或无法针对夜间编译器的情况。

      macro_rules! foo_macro {
          ( $( $name:ident ),+ ) => {
              paste::item! {
                  #[test]
                  fn [<test_ $name>]() {
                      assert! false
                  }
              }
          };
      }
      

      【讨论】:

        【解决方案4】:

        如果您不想使用 nightly 和 external crate,并且您的标识符是类型,您可以将标识符收集到一个结构中。

        use std::fmt::Debug;
        
        fn print_f<T: Debug>(v: &T){
            println!("{:?}", v);
        }
        
        macro_rules! print_all {
            ($($name:ident),+) => {
                struct Values{
                    $($name: $name),+
                }
                let values = Values{
                    $(
                        $name: $name::default()
                    ),+
                };
                $(
                    print_f(&values.$name);
                )+
            };
        }
        
        fn main(){
            print_all!(String, i32, usize);
        }
        

        此代码打印

        ""
        0
        0
        

        如果您担心Value 会与某些类型名称冲突,您可以使用一些较长的 UUID 作为名称的一部分:

        struct Values_110cf51d7a694c808e6fe79bf1485d5b{
           $($name:$name),+
        }
        
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-05-02
          • 2022-01-08
          • 1970-01-01
          • 1970-01-01
          • 2023-04-10
          • 2023-04-05
          • 1970-01-01
          相关资源
          最近更新 更多