Writing Non-Trivial Macros in Rust

The other day I needed to create a pretty complex macro and thought I'd write about the process I used to design it. Think of this as a worked example to complement the canonical The Little Book of Rust Macros.

7 Likes

If your edit the macro to generate a impl<T:Trait,Ptr:Deref<Item=T>> Trait for Ptr { /* ... */ } block, it’ll cover references, boxes, and anything else that acts as a smart pointer, like lock guards. I’m not sure if it handles trait objects or not.

Well you learn something new every day.

I've tried writing those sorts of where clauses in an impl block in the past and run into various permutations of "Type parameter, T, is not used in this impl block" (E0207).

That means an alternate solution is something like this:

use std::ops::DerefMut;
use std::sync::MutexGuard;
use std::time::Duration;

trait DigitalOutput {
    fn set_state(&mut self, new_state: bool);
}

impl<D, Ptr> DigitalOutput for Ptr
where
    Ptr: DerefMut<Target = D>,
    D: DigitalOutput + ?Sized,
{
    fn set_state(&mut self, new_state: bool) {
        (**self).set_state(new_state)
    }
}

fn assert_is_digital_output<D: DigitalOutput + ?Sized>() {}

fn main() {
    assert_is_digital_output::<dyn DigitalOutput>();
    assert_is_digital_output::<&mut dyn DigitalOutput>();
    assert_is_digital_output::<Box<dyn DigitalOutput>>();
    assert_is_digital_output::<MutexGuard<dyn DigitalOutput>>();
}

It's still not ideal though because you're effectively writing the trait twice and any changes to the trait will need to be done to the generic impl. That's the main problem my macro tries to avoid.

1 Like

Trait objects don’t natively implement their own traits!

Careful, this is not true: we do have dyn Trait : Trait if Trait is object safe.

The error message or confusion comes from the fact that dyn Trait is not really usable as a type per se,

  • so we always deal with pointers to trait objects, which end up being called trait objects themselves by abuse of language;

  • and by default we do not have Box<impl Trait + ?Sized> : Trait nor
    &(impl Trait + ?Sized) : Trait (easy counter example for the latter: Trait = Send), etc.

The macro system has a couple… quirks… which make dealing with self kinda awkward.

I'd say that the &[mut] self notation is the quirk here, not the macros :wink:

If you ever get stuck and are wanting some sort of “print statement” to see what a macro is doing, have a look at the compile_error!() macro.

There is also the log_syntax! macro, which is pretty straightforward to use: log_syntax! { $($tokens) * }. Compilation fails since it requires a nightly feature ... but that happens after the tokens are displayed :upside_down_face:

  • Or my personal favorite for recursive macros: slap a trace_macros!(true); before calling your macro.

If you know of an easy way to match both &self and &mut self methods, please let me know!

I wouldn't say that the following is easy or readable, but it is short and works:

macro_rules! foo {(
    $(
        & $(@$ref:tt@)?
        $(
            mut $(@$mut:tt@)?
        )?
    )?
    self
) => (
    impl Type {
        fn method (
            $(
                & $(@$ref@)?
                $(
                    mut $(@$mut@)?
                )?
            )?
            self
        )
        {}
    }
)}
  • The idea is to have a name for an empty group by using an optional group that will never be there, which can be ensured by using an illegal token for that position, such as @.

  • simple Playground (and crazy Playground (does not support &mut self because of the impl on &_: would require search_for_mut_self to work))

Nice :ok_hand:


Also, if code readability is important, know that with #[macro_rules_attribute], you can have your macro invocation be written as:

  • #[macro_rules_attribute(trait_with_dyn_impls!)]
    /// An interesting trait.
    pub trait InterestingTrait {
        fn get_x(&self) -> u32;
    
        /// Do some sort of mutation.
        fn mutate(&mut self, y: String);
    }
    

[procedural macros] can have a negative impact on compile times

This is quite true indeed, although most of the compile time impact comes from using the syn crate. Depending on the use case, one can (more or less) easily get away with not using it (you can even go quite far).

3 Likes

Someone pointed this out on Reddit as well. I guess the more precise way of putting it is to say things which dereference to dyn Trait (e.g. Box<Dyn Trait> or &Dyn Trait) don't automatically implement that trait.

I'm guessing that's because you could want the impl for &Dyn Trait to do something other than immediately defer to the trait object it points to.

That's an interesting little crate. I'm guessing it just wraps its content in a macro invocation, with the idea that attribute syntax has less indentation and curly braces.

That's quite clever! I had tried to insert some sort of empty pattern so we'd have something nameable when matching mut, but it never quite worked.

Haha, I'm not sure whether to report that a bug or leave it as a feature.

The way I went about writing this macro I didn't really need to spend time debugging, but for the one or two times I got stuck -Z macro-backtrace worked pretty well. I ended up running RUSTFLAGS="-Z macro-backtrace" cargo watch -x test on a terminal in the background and that was usually enough to point out where a problem occurred.

1 Like