google / zerocopy

https://discord.gg/MAvWH2R6zk
Apache License 2.0
1.13k stars 86 forks source link

Make derive macros hygienic #11

Open joshlf opened 1 year ago

joshlf commented 1 year ago

Status

Crate name annotation

Sometimes, our derives will be used in a context in which zerocopy isn't called zerocopy. For example, this might happen if zerocopy itself is re-exported from another crate, or if a crate wishes to support deriving zerocopy traits from multiple zerocopy versions (see e.g. #557).

We can take inspiration from Serde, which defines the crate attribute option:

#[serde(crate = "...")]

In other words, we can do:

#[zerocopy(crate = "...")]

However, this isn't enough. For users who wish to invoke derives from multiple versions of zerocopy-derive at once, we need a way of disambiguating which attributes are meant to be consumed by which version of zerocopy-derive. We could provide a disambiguation option like so:

#[zerocopy(crate = "...", derive-version = "...")]

This would let us write code like the following (from #557):

#[cfg_attr(feature = "zerocopy_0_7", derive(zerocopy_0_7::FromBytes))]
#[cfg_attr(feature = "zerocopy_0_8", derive(zerocopy_0_8::FromBytes))]
#[zerocopy(crate = "zerocopy_0_7", derive-version = "0.7")]
#[zerocopy(crate = "zerocopy_0_8", derive-version = "0.8")]
struct Foo {
    ...
}

In this example, each zerocopy-derive would use derive-version to filter out attributes not meant for that version.

Simpler alternative

The above-described design is a bit complex, and requires users to add a new attribute for each zerocopy version. This is especially painful for crates like libc who will have hundreds of uses.

An alternative is to not permit the user to specify the name of the crate, but instead only support a single naming scheme:

#[cfg_attr(feature = "zerocopy_0_7", derive(zerocopy_0_7::FromBytes))]
#[cfg_attr(feature = "zerocopy_0_8", derive(zerocopy_0_8::FromBytes))]
#[zerocopy_rename]
struct Foo {
    ...
}

This has the same semantics as the preceding example, but it assumes that for zerocopy-derive 0.X.Y, the zerocopy crate is imported as zerocopy_X_Y, and for zerocopy-derive X.Y.Z, that zerocopy is imported as zerocopy_X.

Core re-export

We can't rely on core being in scope (or referring to the "real" core crate). However, we can rely on zerocopy being in scope (possibly renamed, as described above). If we re-export core from zerocopy, then we can emit code that doesn't refer to ::core, but instead refers to ::zerocopy::core_reexport.

joshlf commented 1 year ago

@Kestrer, do you know if it's sufficient to instead emit ::zerocopy? My assumption is that, even if zerocopy is reexported by another crate, it still has to be available to rustc at compile time. I assume there's an issue with this approach since it's not the approach that Serde takes, but I'd be curious to know what the issue is.

Another approach that occurred to me - which I suspect also won't work for some reason - is to emit extern crate zerocopy in the generated code.

joshlf commented 1 year ago

@djkoloski Flagging this in case you'd be interested in it - specifically the #[zerocopy(crate = "...")] annotation.

Kestrer commented 1 year ago

do you know if it's sufficient to instead emit ::zerocopy?

This should be the default option, since in most cases it works. However, it fails in the following cases:

  1. The user renames zerocopy to something else, e.g. if they do foobar = { package = "zerocopy", version = "0.6.3" } in their Cargo.toml
  2. The user does not depend on zerocopy itself, but rather imports it through some other crate — e.g. foobar has pub use zerocopy; in its crate root, and the user’s crate tries #[derive(foobar::zerocopy::AsBytes)].

Supporting these cases is the motivation for adding a #[zerocopy(crate = path::to::zerocopy)] annotation.

Another approach that occurred to me - which I suspect also won't work for some reason - is to emit extern crate zerocopy in the generated code.

I believe that also fails in the two cases listed above.