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.
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.
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
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
- 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 requiresearch_for_mut_self
to work))
Nice
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).
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.
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.