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.