Skip to main content

Crate macro_template

Crate macro_template 

Source
Expand description

macro-template is a procedural macro that generates repeated Rust code in multiple places with table-driven inputs.

§Motivation

This crate is inspired by match-template and macro_find_and_replace.

When developing ScopeDB, we introduced these two proc-macros to generate repeated code for match arms and impls. I noticed that they share a common pattern: iterating over a table of values and generating code based on it. I wanted to unify these patterns into a single, concise, but flexible macro that can handle various use cases. That’s how macro-template was born.

Last but not least, I found seq-macro and borrowed some ideas from it, such as iterating over a range of numbers, characters, or bytes, and using a syntax #( ... )* to generate partial repeated substitutions. This eliminates the need for an extra template_match! to handle repetitions in match arms, and allows for more flexible code generation.

Go back to the beginning, why do you need macro_template::template! at all? Isn’t it the same as a simple macro_rules!?

macro_rules! impl_serialize {
    ($($ty:ty),* $(,)?) => {
        $(
            impl serde_core::Serialize for BSize<$ty> {
                fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
                where
                    S: serde_core::Serializer,
                {
                    if ser.is_human_readable() {
                        ser.collect_str(self)
                    } else {
                        self.0.serialize(ser)
                    }
                }
            }
        )*
    };
}

impl_serialize!(u8, u16, u32, u64, usize);

Except that macro_template::template! supports more flexible substitution patterns as shown in the Examples section, template! has a concise syntax, and it saves you from declaring an extra macro_rules!, naming it, and invoking it.

The example above can be rewritten as:

macro_template::template! {
    for Ty in [u8, u16, u32, u64, usize] {
        impl serde_core::Serialize for BSize<Ty> {
            fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
            where
                S: serde_core::Serializer,
            {
                if ser.is_human_readable() {
                    ser.collect_str(self)
                } else {
                    self.0.serialize(ser)
                }
            }
        }
    }
}

§Examples

First, you can generate code with a template and a matrix of values:

macro_template::template! {
    for (Endian, Method) in [
        (LittleEndian, to_le_bytes),
        (BigEndian, to_be_bytes),
        (NativeEndian, to_ne_bytes)
    ],
    for (Ty, Width) in [
        (u16, 2),
        (u32, 4),
    ],
    {
        impl StoreBytes<Endian, Width> for Ty {
            fn store_bytes(&self) -> [u8; Width] {
                self.Method()
            }
        }
    }
}

Without splice syntax, the whole template body is repeated once for each row. You can also do substitutions only partially with #( ... )*. Like quote, a single separator token tree can be written before *, such as #( ... ),*. For example, to generate match arms:

macro_template::template! {
    for T in [Int, Real, Double] {
        match Foo {
            #( EvalType::T => { panic!("{}", EvalType::T); } ),*,
            EvalType::Other => unreachable!(),
        }
    }
}

When a template contains #( ... )* or #( ... ),*, template variables are substituted only inside the splice body, and surrounding tokens are emitted once. Surrounding identifiers stay literal, even when they have the same name as a template variable. If a value should vary, place it in the splice body.

Naturally, if the match arm differs left-hand side and right-hand side:

macro_template::template! {
    for (K, Value) in [
        (Apple, "apple"),
        (Banana, "banana"),
        (Cherry, "cherry"),
    ] {
        match kind {
            #( Kind::K => { println!("{}", Value); } ),*
        }
    }
}

Inputs can also be ranges of numbers, characters, or bytes. Range inputs are written directly after in, without surrounding brackets:

// sequential numeric counter
let tuple = (1000, 100, 10);
let mut sum = 0;
macro_template::template! {
    for i in 0..3 {
        sum += tuple.i;
    }
}
assert_eq!(sum, 1110);

// sequential character collector
let mut string = String::new();
macro_template::template! {
    for c in 'x'..='z' {
        string.push(c);
    }
}
assert_eq!(string, "xyz");

Integer ranges preserve the radix, suffix, and shared padding width from their bounds.

You can combine multiple iterators in a single template:

let mut values = vec![];

macro_template::template! {
    for Prefix in ["read", "write"],
    for Code in 200..=201 {
        values.push((Prefix, Code));
    }
}

assert_eq!(
    values,
    [("read", 200), ("read", 201), ("write", 200), ("write", 201)],
);

Multiple input clauses form a Cartesian product in clause order.

Macros§

template
Generates repeated Rust code from one or more table-driven input clauses.