Unsafe-to-invoke macros that expand to items

The question of whether Rust should have macros that behave like unsafe fn - requiring the unsafe keyword at the invocation site - has been discussed several times in the past, generally with people's reaction being "meh" or "you can get this effect by not having an unsafe block inside the macro expansion":

I have just encountered a situation where it would be desirable for a macro to require the unsafe keyword at the invocation site, but the workaround suggested above doesn't work: namely, a macro that expands to an unsafe trait implementation, having discharged some but not all of the safety requirements.

pub unsafe trait Marker {}

macro_rules! impl_marker {
    ($struct:ty) => {
        unsafe impl Marker for $struct {}
    }
}

pub struct A;

impl_marker!(A);

(In the real code that this is cut down from, the unsafe trait is not a pure marker, and the macro produces a correct implementation of the unsafe trait if and only if A is a #[repr(C)] struct, which is not possible to check at compile time as far as I can tell.)

Unlike macros that expand to expressions, the unsafe keyword has to be inside the macro expansion; this alternative does not compile...

pub unsafe trait Marker {}

macro_rules! impl_marker {
    ($struct:ty) => {
        impl Marker for $struct {}
    }
}

pub struct A;

unsafe impl_marker!(A);

So I think this does indicate a real gap in the language.

4 Likes

It’s a good example, but since you could make the input syntax impl_marker!(unsafe MyStruct) the motivation is lessened a little.

That gives the wrong impression though - the unsafety is associated with the macro invocation, not with its argument.

The typical answer I see and give is "put unsafe somewhere in your macro name". It's not entirely satisfying but it's not terrible either.

4 Likes

If you control the macro definition, you could combine the struct definition and marker trait impl into 1 definition. With that you could change the expansion to force the struct to always be #[repr(C)].

If that approach is too restrictive (eg because what fields a struct is composed of can vary across invocations of the macro), one way to deal with that is to turn the macro into an attribute macro that simply parses the struct (eg using syn), makes it #[repr(C)], and implements the marker trait. As attribute macros can swallow their input and generate arbitrary output (unlike derive macros, which can only generate additional code), it can take the regular struct definition, but alter the expansion before emitting it to slap a #[repr(C)] on the struct and include the unsafe marker trait impl for that struct, similar to the macro_rules macro you have now.

Using an attribute macro could also help with this, I believe. Proc macros can expand to any token sequence IIRC, and the rest is up to rustc.

A derive macro would help with this, since it can check the struct, and fail if the struct has the wrong repr.

I think unsafe macros should exist, if there isn't some huge implementation cost preventing that.

They're not the hottest feature to have, but they make sense, and have uses. Rust has function-like macros, and ability to have unsafe macros would make them more consistent with functions.

Adding unsafe to the macro name is as unsatisfying as having fn unsafe_in_the_name().

7 Likes

I agree. That would also require a way to write unsafe blocks anywhere a macro can go, and ideally some way to switch the macro arguments back into safe mode.

There are several of these structs and their fields do vary enough that I wouldn't want to try to write a declarative macro to handle them. I currently have no uses of proc macros in this crate and this doesn't seem important enough to change that (it's a private trait anyway). Still, these techniques are good to know about.

[edited to add:] What I'm trying to say here is that I would still like to see unsafe declarative macros in the language, despite the existence of these alternatives, because they're very costly alternatives. I think it'd be (just barely) possible to generate my struct declarations + trait implementations from a declarative macro, and certainly from an attribute proc macro, but either option would be a lot more coding than what I have now (which works, it's just not optimally safe) and I have so far avoided the proc macro compile-time tax.

For the syntax, I think [Pre-RFC] Single function call `unsafe` would be a good thing to lift, we can just declare unsafe macro_call!(args..) (or whichever syntax is used) to call an unsafe-to-invoke macro macro_call, but not change the safety context of the arguments nor what the macro expands to.

this would also be nice if we ever make saftey hygiene-based