Skip to content

← Macros, FFI and Type-Driven Design step 10 of 28

Hard Primitives

Callbacks and TT bundling

Pass one macro the name of another and let it do the invoking.

pub fn dispatch() -> (i64, i64)   // (with_list!(sum_all, [1,2,3,4]), with_list!(max_all, [3,-7,9,2]))
pub fn bundled()  -> (i64, i64)   // (call_with!(sum_all, (10,20,30)), call_with!(max_all, (-1,-5,-3)))

sum_all! and max_all! are written. Your job is with_list! and call_with!.

The symptom this explains

Sooner or later you write something like

sum_all!(make_list!())

expecting make_list!() to expand into 1, 2, 3 and sum_all! to see three arguments. It does not work, and the error is confusing.

The reason is that expansion is outside-in. When the compiler reaches sum_all!(make_list!()), it expands sum_all! first, handing it the unexpanded tokens make_list ! ( ). If sum_all!‘s matcher is $($x:expr),*, that is one expression — a macro invocation is a perfectly good expression — and the expansion becomes 0 + make_list!(), which then expands to 0 + 1, 2, 3 and fails to parse.

Nothing you can write in the inner macro fixes this. The outer macro decides what it does with its tokens, and by the time the inner one runs it is too late.

Inverting control

The fix is to turn the relationship around. Instead of the inner macro producing arguments for the outer one, the outer macro takes the name of the inner one and invokes it:

macro_rules! with_list {
    ($cb:ident, [$($x:expr),* $(,)?]) => {
        $cb!($($x),*)
    };
}
```

`$cb` is captured as an `ident` — just a name, no invocation, nothing expanded.
The transcriber then writes `$cb!(...)`, and *that* invocation is expanded on the
next pass, with the arguments already unpacked. `with_list!` decides the shape;
the caller decides what gets built from it.

This is the callback pattern, and it is everywhere in real macro crates. It is
how one macro can define a data format once and let many different macros consume
it.

## TT bundling

The second half is smaller and just as useful. A delimited group is **one token
tree** (18.8), so you can pass an entire argument list around as a single `tt`
and unpack it later:

```rust
macro_rules! call_with {
    ($cb:ident, $args:tt) => {
        $cb! $args
    };
}
```

`call_with!(sum_all, (10, 20, 30))` binds `$args` to the whole group
`(10, 20, 30)`, and `$cb! $args` emits `sum_all` `!` `(10, 20, 30)` — a complete
invocation, delimiters included. The macro never looked inside the bundle.

Bundling is how you thread arbitrary user syntax through several layers of macro
without each layer having to understand it.

::: question `stringify!` and `concat!` seem to be eager — `concat!("a", stringify!(b))` works. Doesn't that contradict "outside-in"?

It looks like it, and that appearance is exactly what misleads people.

`stringify!`, `concat!`, `include_str!`, `env!` and friends are **built-in**
macros implemented inside the compiler, not `macro_rules!` definitions. Some of
them explicitly expand their arguments before doing their work, because they need
a string literal and not a token stream. That behaviour is special-cased, and it
does not generalise.

Your own `macro_rules!` macro gets no such treatment. `outer!(inner!())` always
passes `outer!` the unexpanded tokens. The community name for the eager cases is
"eager expansion", and the fact that it exists for a handful of built-ins while
being unavailable to user macros is a long-standing wart — one of the recurring
arguments for reaching for a procedural macro instead (18.17).

Incidentally, clippy has a lint for the degenerate case: `useless_concat` fires
on a `concat!` whose arguments are all literals, because you could have just
written the string.
:::

## Your job

The starter's `with_list!` calls a *function* named `$cb`, which does not exist.
`call_with!` ignores its arguments entirely. Fix both.