Open mahkoh opened 1 month ago
I'm not remembering what Derive proc macros are allowed to emit. Can they create code other code besides the trait impl they're for?
Otherwise, we might have to do the other option mentioned in the zerocopy thread and bail out on all unknown attributes. BUT that would be extremely uncool.
Yes, derive macros can emit arbitrary items, essentially. (IIRC this is already used in bytemuck_derive
, where some derive macros emit some const _: () = { some static assertion stuff };
in addition to the trait impl.)
That said, Is it documented anywhere that it is intentional for derive macros to see the "original" tokens when an item has been modified by an attribute macro? My first instinct would be to call it a Rust bug that the derive macro does not get the correct TokenStream
that defines the item it is intended to be applied to.
i believe the exact ordering of the attributes matters, unfortunately
use the_macros::*;
fn main() {
{
#[derive(PrintTokens)]
#[add_fields({y: u32})]
struct Foo {
x: u32,
}
print_tokens()
}
{
#[add_fields({y: u32})]
#[derive(PrintTokens)]
struct Foo {
x: u32,
}
print_tokens()
}
}
#[add_fields({ y: u32 })] struct Foo { x: u32, }
struct Foo { x : u32, y : u32 }
Okay, the original bug report made it sound like Rust never applied attribute macros before giving the item to derive macros, but yeah it does if it is before the derive (since it applies macros outside-in) and otherwise the attribute macro is still in the input. So erroring out if there are any unknown attributes in the input (the first case here) would be one way to make this sound, perhaps with an error message telling the user to move the attribute macro above the (see reply)derive
.
the_macros
source So erroring out if there are any unknown attributes in the input (the first case here) would be one way to make this sound, perhaps with an error message telling the user to move the attribute macro above the derive.
Actually after some testing, this won't work in general. Even ignoring all the false positives it would have, there are also false negatives: there's no way in general to differentiate between our own helper attributes for other traits (like #[zeroable(bound = "")]
) and actual unknown attribute proc-macros.
Edit: I suppose we could get around this by only allowing the helper attributes for the specific trait being derived, which would require writing the derives separately with the helpers between them, actually no that won't work either, because the helper attributes are "inert", but are not actually stripped from the input for later derives, e.g.
Another wrinkle: For fields, we can check the types using some const _: () = { /* do some stuff */ };
checks to make sure nothing changed the field types, but for enum discriminants, I don't know if there is any way to be sure that a procmacro did not change any variant discriminants, e.g.
#[derive(Zeroable)]
#[repr(u32)]
#[adversarial_macro] // changes discriminant so none are 0
enum Foo {
X(i32) = 0,
}
Is there even any way to detect this at compile time?
Edit: For a fieldless enum you can const _: () = assert!(Foo::X as int_ty == 0);
, but for fieldful enums I don't know if there is a way to do it at compile time.
That said, Is it documented anywhere that it is intentional for derive macros to see the "original" tokens when an item has been modified by an attribute macro? My first instinct would be to call it a Rust bug that the derive macro does not get the correct TokenStream that defines the item it is intended to be applied to.
It's sort of the inverse: I don't believe we have fixed the order of macro evaluation for proc macros in a way that is particularly useful or provides any real guarantees to macro authors, in any direction.
I came across https://github.com/google/zerocopy/issues/388#issuecomment-1737817682 by chance. The same applies to this crate:
Segfaults. Solutions might involve generating a function that asserts (by compiling) that the types seen by the proc macro are the same as the types in the final type and that the final type contains no additional fields.