How do I write a declarative macro that takes an expression with a free variable?

I'm trying to write a declarative macro for generating trait implementations that differ only in type, a literal, and the expression used to retrieve a value from an argument. My code at the moment is:

use bytes::{Buf, Bytes};
use std::net::Ipv4Addr;

pub trait TryFromBuf: Sized {
    fn try_from_buf(buf: &mut Bytes) -> Result<Self, PacketError>;
}

macro_rules! impl_tryfrombuf {
    ($t:ty, $len:literal, $get:expr) => {
        impl TryFromBuf for $t {
            fn try_from_buf(buf: &mut Bytes) -> Result<Self, PacketError> {
                if buf.remaining() >= $len {
                    Ok($get)
                } else {
                    Err(PacketError)
                }
            }
        }
    };
}

impl_tryfrombuf!(u32, 4, buf.get_u32());
impl_tryfrombuf!(Ipv4Addr, 4, buf.get_u32().into());

pub struct PacketError;

However, this fails to compile, apparently because buf is not defined at the point where the macro is invoked:

error[E0425]: cannot find value `buf` in this scope
  --> src/lib.rs:22:26
   |
22 | impl_tryfrombuf!(u32, 4, buf.get_u32());
   |                          ^^^ not found in this scope

error[E0425]: cannot find value `buf` in this scope
  --> src/lib.rs:23:31
   |
23 | impl_tryfrombuf!(Ipv4Addr, 4, buf.get_u32().into());
   |                               ^^^ not found in this scope

For more information about this error, try `rustc --explain E0425`.

I've tried changing :expr to :stmnt, changing :expr to :block and adding braces around the expressions, and changing $get:expr & Ok($get) to $($get:tt)+ & Ok($( $get )+), but none have compiled. How can I make this work?

Declarative macros are hygienic, so an identifier that is declared inside the macro cannot be used outside of it. No amount of token matcher declarations can change this.

You have a couple of options:

  1. switch to a procedural macro
  2. accept an explicit name/identifier instead of naming it buf and pass it to the macro
  3. use an immediately invoked closure

Approach #2 is demonstrated here. And approach #3 is here.

5 Likes

Another option is to match the function/method calls into token-trees and keep the buf variable completely inside the macro:

use bytes::{Buf, Bytes};
use std::net::Ipv4Addr;

pub trait TryFromBuf: Sized {
    fn try_from_buf(buf: &mut Bytes) -> Result<Self, PacketError>;
}

macro_rules! impl_tryfrombuf {
    ($t:ty, $len:literal, $($get:tt)+) => {
        impl TryFromBuf for $t {
            fn try_from_buf(buf: &mut Bytes) -> Result<Self, PacketError> {
                if buf.remaining() >= $len {
                    Ok(buf.$($get)+)
                } else {
                    Err(PacketError)
                }
            }
        }
    };
}

impl_tryfrombuf!(u32, 4, get_u32());
impl_tryfrombuf!(Ipv4Addr, 4, get_u32().into());

pub struct PacketError;
3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.