关于在 macro_rules 定义中调用另一个 macro_rules 的问题

Question on invoking another macro_rules in macro_rules definition

我正在实施编写 TLV 数据包以实现某种程度的实现 std::io::Write

首先我实现了WriteBE<T> trait,其write_be(&mut self, data: T)方法可以将T类型的数据写入Self。 (省略实施细节)

我正在尝试使用 macro_rules!在编译时实现总数据包长度的计算(因为在我的例子中大多数数据包都有固定长度)。宏如下:

macro_rules! len_in_expr {
    (
        self.write_be( $data: expr $(,)? ) $(?)? $(;)*
    ) => {
        std::mem::size_of_val(&$data)
    };
    (
        write_be(self, $data: expr $(,)? ) $(?)? $(;)*
    ) => {
        std::mem::size_of_val(&$data)
    };
    (
        $other: expr
    ) => {
         0
    };
}

/// calculate total write size in block
macro_rules! tlv_len_in_block {
    ({
        $( $e: expr );* $(;)?
    }) => {
        0 $(
            + ( len_in_expr!($e) )
        )*
    };
}

但是当我计算总长度时 this:

fn main() {
    let y = tlv_len_in_block!({
        write_be(self, 0u32,)?;
    });
    println!("y={}", y);
}

我得到一个结果0

如果我评论 $other: expr 匹配臂,我会得到一个编译错误:

6  |   macro_rules! len_in_expr {
   |   ------------------------ when calling this macro
...
30 |               + ( len_in_expr!($e) )
   |                                ^^ no rules expected this token in macro call
...
39 |       let y = tlv_len_in_block!({
   |  _____________-
40 | |         write_be(self, 0u32,)?;
41 | |     });
   | |______- in this macro invocation

我的代码有什么问题?我该如何解决?

一旦 macro_rules! 中的元变量被捕获到某个片段说明符(例如 expr)中,它们就不能再被分解。引用 the reference:

When forwarding a matched fragment to another macro-by-example, matchers in the second macro will see an opaque AST of the fragment type. The second macro can't use literal tokens to match the fragments in the matcher, only a fragment specifier of the same type. The ident, lifetime, and tt fragment types are an exception, and can be matched by literal tokens. The following illustrates this restriction:

macro_rules! foo {
    ($l:expr) => { bar!($l); }
// ERROR:               ^^ no rules expected this token in macro call
}

macro_rules! bar {
    (3) => {}
}

foo!(3);

The following illustrates how tokens can be directly matched after matching a tt fragment:

// compiles OK
macro_rules! foo {
    ($l:tt) => { bar!($l); }
}

macro_rules! bar {
    (3) => {}
}

foo!(3);

一旦 tlv_len_in_block!()$e 中捕获了 write_be(self, 0u32,)?,它就不能分解为 write_be(self, $data:expr $(,)? ) and thus, cannot be matched by the second case of the len_in_expr!()` 宏,因为它应该是.

这个问题一般有两种解决方法:

首先是,如果可能的话,从头开始分解它们。问题是这并不总是可能的。例如,在这种情况下,我看不到它起作用的方法。

第二种方法要复杂得多,它使用 Push-down Accumulation technique together with tt Munching.

思路如下:我们不是将输入作为一个整体来解析,而是一次一个地解析每一块。然后,递归地,我们将已解析的位和尚未解析的位转发给我们自己。我们还应该有一个空输入的停止条件。

在您的示例中它会是这样的:

macro_rules! tlv_len_in_block_impl {
    // Stop condition - no input left to parse.
    (
        parsed = [ $($parsed:tt)* ]
        rest = [ ]
    ) => {
        $($parsed)*
    };
    (
        parsed = [ $($parsed:tt)* ]
        rest = [
            self.write_be( $data:expr $(,)? ) $(?)? ;
            $($rest:tt)*
        ]
    ) => {
        tlv_len_in_block_impl!(
            parsed = [
                $($parsed)*
                + std::mem::size_of_val(&$data)
            ]
            rest = [ $($rest)* ]
        )
    };
    (
        parsed = [ $($parsed:tt)* ]
        rest = [
            write_be(self, $data:expr $(,)? ) $(?)? ;
            $($rest:tt)*
        ]
    ) => {
        tlv_len_in_block_impl!(
            parsed = [
                $($parsed)*
                + std::mem::size_of_val(&$data)
            ]
            rest = [ $($rest)* ]
        )
    };
}

/// calculate total write size in block
macro_rules! tlv_len_in_block {
    ({
        $($input:tt)*
    }) => {
        tlv_len_in_block_impl!(
            parsed = [ 0 ]
            rest = [ $($input)* ]
        )
    };
}

(请注意,这与您的宏不完全相同 - 我的 需要一个尾随分号 ,而在您的宏中,它是可选的。也可以在此处将其设为可选,但它会更复杂。

Playground.