Force your macro's callers to write unsafe
Discuss this post on Reddit.
Imagine you're writing this macro:
/// Types that can be frobnicated.
///
/// # Safety
///
/// It must be sound to frobnicate `Self`.
unsafe trait Frobnicatable {}
/// Implements `Frobnicatable` for `$t`.
///
/// # Safety
///
/// `$t` must satisfy the safety invariant of `Frobnicatable`.
macro_rules! unsafe_impl_frobnicatable {
($t:ty) => {
// SAFETY: The caller has promised that `$t` satisfies
// the safety invariant of `Frobnicatable`.
unsafe impl Frobnicatable for $t {}
};
}
Of course, you'd never actually write a macro to emit code this simple, but it's a meant as a stand-in for macros that emit more complex code.
A caller might write this:
struct Foo;
// SAFETY: It is sound to frobnicate `Foo`.
unsafe_impl_frobnicatable!(Foo);
But imagine that your caller wants to practice good safety hygeine, and they use
#![deny(clippy::undocumented_unsafe_blocks)]
.
Because Clippy doesn't realize that the macro call requires a safety comment, it
won't complain if your caller forgets their safety comment:
#![deny(clippy::undocumented_unsafe_blocks)]
struct Foo;
// Clippy: 🙈
unsafe_impl_frobnicatable!(Foo);
Instead, you should write your macro so that it invokes unsafe code. This also
means that we can drop the unsafe_
prefix from the macro's name – it was just
there to make it less likely for your callers to miss the safety requirement,
but that's no longer a concern. Also, make sure to have the macro's right-hand
side contain a block – ie, {{ ... }}
– see the appendix for an explanation of
why this is important.
/// Implements `Frobnicatable` for `$t`.
///
/// # Safety
///
/// `$t` must satisfy the safety invariant of `Frobnicatable`.
macro_rules! impl_frobnicatable {
($t:ty) => {{
// Force caller to wrap in an `unsafe { ... }` block.
$crate::__unsafe();
// SAFETY: The caller has promised that `$t` satisfies
// the safety invariant of `Frobnicatable`.
unsafe impl Frobnicatable for $t {}
}};
}
#[doc(hidden)]
pub const unsafe fn __unsafe() {}
Now, even when Clippy isn't used, Rust will force your caller to put the macro
invocation in an unsafe
block. In other words, this will no longer compile:
// SAFETY: It is sound to frobnicate `Foo`.
impl_frobnicatable!(Foo);
To fix this, your caller must wrap the invocation in an unsafe
block, for
example:
// SAFETY: It is sound to frobnicate `Foo`.
const _: () = unsafe { impl_frobnicatable!(Foo) };
And now Clippy will catch a missing safety comment since your caller is using
a normal unsafe
block.
Appendix: Macro expansion
Note one subtlety: You're going to want to have your macro emit a block (ie, use
(...) => {{ ... }}
rather than (...) => { ... }
). You're now requiring your
callers to call your macro in expression or statement position (ie, inside of a
block) rather than in item position (ie, at the top-level, outside of a block).
Thus, it has to play nicely with code before and after it in the same block.
Expanding to a self-contained block is a good way of ensuring that this will
happen.