Search code examples
rustrust-macros

Is it possible to use an item arg passed to a macro as a method?


I'm trying to create a macro that generates a struct that provides a set of methods that are passed into the macro. For example, calling:

create_impl!(StructName, fn foo() -> u32 { return 432 })

should generate an empty struct StructName that provides the method foo().

My initial attempt at this uses the item macro arg type. However, when I try and use an item in the rule, I get the following compiler error:

error: expected one of `const`, `default`, `extern`, `fn`, `pub`, `type`, `unsafe`, or `}`, found `fn foo() -> u32 { return 42; }`
  --> src/lib.rs:40:13
   |
40 |           $($function)*
   |             ^^^^^^^^^

Is it possible to use item arguments to define methods in generated structs this way? Is there something I'm missing?

Here's the full macro I've defined:

macro_rules! create_impl {

  ($struct_name:ident, $($function:item),*) => {
      struct $struct_name {
      }

      impl $struct_name {
          // This is the part that fails.
          $($function)*
      }
  };

}

Solution

  • The short answer is "no, you can't use the item matcher for a method".

    According to the reference, items are the top level things in a crate or module, so functions, types, and so on. While a struct or impl block is an item, the things inside them aren't. Even though syntactically, a method definition looks identical to a top level function, that doesn't make it an item.

    The way Rust's macro system works is that once a fragment has been parsed as an item, e.g. using $foo:item, it's then forever an item; it's split back into tokens for reparsing once the macro is expanded.

    The result of this is that $foo:item can only be in the macro's output in item position, which generally means top-level.

    There are a couple of alternatives.

    The simplest is to use the good old tt (token tree) matcher. A token tree is either a non-bracket token or a sequence of tokens surrounded by balanced brackets; so $(foo:tt)* matches anything. However, that means it will gobble up commas too, so it's easier to just add braces around each item:

    macro_rules! create_impl {
    
      ($struct_name:ident, $({ $($function:tt)* }),*) => {
          struct $struct_name {
          }
    
          impl $struct_name {
              $($($function)*)*
          }
      };
    
    }
    

    Then you have to use it with the extra braces:

    create_impl!(StructName, { fn foo() -> u32 { return 432 } }, { fn bar() -> u32 { return 765 } });
    

    You can also just match the syntax you want directly, rather than delegating to the item matcher:

    macro_rules! create_impl2 {
        ($struct_name:ident, $(fn $fname:ident($($arg:tt)*) -> $t:ty $body:block),*) => {
          struct $struct_name {
          }
    
          impl $struct_name {
              $(fn $fname($($arg)*) -> $t $body)*
          }
        }
    }
    

    Of course since it's explicit, that means that if you want to support functions without a return type, you need to add another case to your macro.