Explicitly marking unsafe macro expressions

There are multiple things to pay attention to, here:

  • Should a macro using unsafe become unsafe itself? I don’t think this is desirable. See ::std::await! or ::pin-utils::pin_mut! : safe constructions using underlying unsafe. It is the same as a non-unsafe function wrapping unsafe behavior in a (hopefully) safe manner, thus not needing to be unsafe itself.

    On the other hand, if a macro expansion cannot be proven to be always sound, then the macro has to be defined without unsafe in it, thus forcing the caller to explicitly use an unsafe scope. Sadly, the “unsafeness” of the macro would not be visible at the “header level”; it could just be documented (and maybe also with a naming convention, like unsafe_foo!), which means that the error message raised might not be great.

  • unsafe hygiene, i.e., an input argument $macro_arg:expr should not be allowed to “unsafe-ly evaluate” $macro_arg (e.g. feeding *::std::ptr::null() as $macro_arg:expr should raise an error since it requires "unsafe evaluation", but feeding unsafe { *::std::ptr::null() } as $macro_arg:expr should be allowed (unsafe being the keyword making all the evaluations in its scope be automagically tagged safe, hence its unsafety)). I’m not being very rigurous but you get the idea.

    In the meantime, the macro maker needs to be careful to never “evaluate an expression argument” (that is, use a $macro_arg:expr) inside an unsafe block, using the following pattern:

unsafe fn get_foo () {}

macro_rules! eq_to_foo {(
  $macro_arg:expr
) => (
  match $macro_arg { macro_arg => unsafe {
    // Look, no metavariables within the unsafe block !
    macro_arg == get_foo()
  }}
)}

// Note that for this very particular case, we could more simply write:
macro_rules! eq_to_foo {(
  $macro_arg:expr
) => (
  $macro_arg == unsafe { get_foo() }
)}
  • unsafe detection: there are many talks about, as a library user, being able to detect / warn and even forbid unsafe usages. The problem is that current “solutions” do not detect unsafe usage through macro invocations. For instance:
    • ::cargo-geiger, a tool to walk through the dependency graph and count the number of unsafe usages encountered in each crate (imho counting is not the best metric, but that is out of topic here):
      • Unsafe code inside macros are not detected. Needs macro expansion
    • Using #![forbid(unsafe_code)] (or RUSTFLAGS=-Funsafe_code):
      • It detects if the macro was defined within the same crate. But a macro from a different crate, even when in the same workspace, will sneak its way past the forbid(unsafe_code) restriction (See here, look at the difference between 1m48 and the end).
3 Likes