4

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)*
      }
  };

}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Donald Whyte
  • 195
  • 2
  • 9
  • 1
    I think that *methods* are not items at all. When I change `fn foo()` to `fn foo(self)`, I get *error: expected one of `::` or `:`, found `)`* (the same error happens if you write that outside a macro). – Francis Gagné Jan 14 '17 at 21:53

1 Answers1

6

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.

dimo414
  • 47,227
  • 18
  • 148
  • 244
Chris Emerson
  • 13,041
  • 3
  • 44
  • 66