Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Pod and Zeroable derives work for some generic structs and improve docs #83

Closed
wants to merge 4 commits into from
Closed

Make Pod and Zeroable derives work for some generic structs and improve docs #83

wants to merge 4 commits into from

Conversation

LukasKalbertodt
Copy link

Fixes #75
Fixes #51

  • derive(Zeroable) now works for structs with generic fields. The impl is bounded with T: Zeroable for each struct field T.
  • derive(Pod) now works for structs where all fields have the same generic type. Again, the impl is bounded with T: Pod then.
  • I improved some documentation and mentioned the derive feature in the crate-level docs.

Note: the code emitted by the derive macros is slightly different with this PR. This has the unfortunate consequence that some compiler errors (for when a struct's field does not implement Pod) are worse than before. However, this was due to a Rust issue which has been fixed and will be released on January 13 as 1.58. See #75 for more details.

Before, the struct itself already needed to have `T: Zeroable` bounds
in order for the derive to work. However, we can also add those bounds
to the `Zeroable` impl itself. Meaning that the inner types only need
to be zeroable in order to implement `Zeroable` for the outer type.

This is in line with how derives like `Clone` work.
Before, the check that all struct field types implement `Pod` was done
in some unnamed `const`. With this commit, those checks are `where`
bounds on the generated impl instead. This is mainly a preparation for
allowing generic parameters for `derive(Pod)` in a few situations. But
it is also slightly simpler than the previous version.
@Lokathor
Copy link
Owner

Lokathor commented Jan 7, 2022

I thought I had replied last week but I guess it didn't post, very sorry.

I like what I see but I literally don't use proc-macros at all so I'd like at least one other person to have a look at this and check that things seem right.

@5225225
Copy link

5225225 commented Jan 7, 2022

The error message for when the types are different looks to be wrong now. Because the derive can be done for generic types, just not ones that have different types.

/// The macro ensures that the struct follows all the safety requirements for
/// the `Zeroable` trait. If your struct has generic fields, the generated
/// `impl` will have a `T: Zeroable` bound for each generic type `T` of your
/// struct.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This says it will add a T: Zeroable bound, but for a struct like struct Foo<T: Iterator> { foo: T::Item } it looks like it will actually generate a T::Item: Zeroable bound (which is more correct, but not what derives generally do).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This. I personally prefer what this derive does, but it's important that whatever choice is made, it's done being aware that it's not the stdlib's approach (cc @Lokathor).

I personally prefer the approach here, because the stdlib one is what makes:

#[derive(PartialEq, Eq)]
struct Ptr<T>(*const T);

only yield comparable pointers when the pointees are.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a tough one.

  • I'd generally prefer that the derives be "extra picky" because this is an unsafe trait after all, and you can always just implement manually if you think you know better than the derive's reasoning.
  • I do agree that the standard library is a little bit wrong on this one, and I've been bit before by a standard library derive being too conservative (when I wrote VolAddress<T,R,W>).

However, I think that since this is becoming an increasingly used crate we should have the derives work like the stdlib derives. Don't want people to have any sudden surprises happening.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The advantage of the default approach will be that we'll have correctly-spanned error messages even in older rust versions 🙂

Copy link
Contributor

@danielhenrymantilla danielhenrymantilla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice initiative 🙂

  • I'd not recommend enabling syn's extra-traits, at least not for a syntactical type comparison which is nonetheless brittle;
  • I'd thus delegate to a static assertion that the types are equal (if you still want the nice error message, then you can use .to_string() to perform comparison without extra-traits, but there should be a const assertion that the types are nonetheless equal).
  • Regarding Pod, it could be nice to also support repr(C) / repr(transparent) wrappers, as well as repr(packed) structs (for a follow-up PR, I imagine).
    • Not necessarily a good thing, but it is possible to soundly implement Pod for a fully generic struct, by relying on post-monomorphization errors: those are better than panics, but worse than classic trait const assertions. Indeed, if the code "links" (performs codegen) correctly, then it won't panic; but it can still fail to codegen despite cargo check passing.
      If you are interested, I got a proof-of-concept for an AsBytes trait (which is the part that Pod is checking, there), in here.

@@ -15,7 +15,7 @@ name = "bytemuck_derive"
proc-macro = true

[dependencies]
syn = "1"
syn = { version = "1", features = ["extra-traits"] }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
syn = { version = "1", features = ["extra-traits"] }
syn = "1"

extra-traits lengthens compile-time quite significantly, so it should only be enabled through an opt-in feature, never unconditionally. I think it's used for a type comparison below, so another approach will be needed (emit code that asserts type equality, rather than use the syntactical input)


// Merge existing bounds with our bounds.
let mut where_clause = original_where_clause
.map(|w| if w.predicates.trailing_punct() { quote! { #w } } else { quote! { #w, }})
Copy link
Contributor

@danielhenrymantilla danielhenrymantilla Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.map(|w| if w.predicates.trailing_punct() { quote! { #w } } else { quote! { #w, }})
.cloned()

(combined with a .predicates.extend(<iterator of extra bounds>))

let field_types = get_struct_fields(input)?;
let all_fields_same_type = field_types.iter()
.zip(field_types.iter().skip(1))
.all(|(f0, f1)| f0.ty == f1.ty);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this will be unsound in the presence of hygienic macros / def_site spans: Foo and Foo could refer to distinct types should the spans be up for it (Playground).

/// The macro ensures that the struct follows all the safety requirements for
/// the `Zeroable` trait. If your struct has generic fields, the generated
/// `impl` will have a `T: Zeroable` bound for each generic type `T` of your
/// struct.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The advantage of the default approach will be that we'll have correctly-spanned error messages even in older rust versions 🙂

@LukasKalbertodt
Copy link
Author

Sorry for the late reply! Unfortunately, I don't need this change anymore and don't really have the energy to work on this right now. Since this PR would require a non-trivial rebase and has several unresolved problems, I will simply close it now. I still think the general direction of this PR is good and that the derives should be generalized. But yeah, closing, sorry!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants