How to provide dependent code for proc macro?

EDIT: Credit to @jswrenn for suggesting a solution: have a particular version of zerocopy depend upon an exact version of zerocopy-derive. This should solve the problem.

I'm the author of zerocopy, which uses the zerocopy-derive proc macro crate to derive impls of some of its traits. I'm working on a refactor that will clean up zerocopy-derive's code and aims to make compilation faster. In particular, I'm trying to factor out some code which is independent of the type that the trait is derived on. Currently, this code is emitted once for every trait impl. I'd like to define this elsewhere in "normal" Rust code rather than in proc-macro-generated Rust code. That will allow me to define it once and then use it in each impl.

I first tried to put it in zerocopy-derive itself, but apparently that's not supported:

error: `proc-macro` crate types currently cannot export any items other than functions tagged with `#[proc_macro]`, `#[proc_macro_derive]`, or `#[proc_macro_attribute]`
  --> src/lib.rs:30:1
   |
30 | pub struct Foo;
   | ^^^^^^^^^^^^^^^

My next thought was to define it in zerocopy itself, but this raises a semver problem. Currently, zerocopy reexports zerocopy-derive's derive macros, and thus takes a dependency on zerocopy-derive. This means that zerocopy-derive cannot also take a dependency on zerocopy - this would be a dependency cycle, which is disallowed.

Without zerocopy-derive taking an explicit dependency on zerocopy, how do I ensure that the crate which uses the derive depends upon a version of zerocopy which is recent enough that it has the features in question? If an older version of zerocopy is used, the code emitted by zerocopy-derive will fail to compile. Complicating matters further, if a newer version of zerocopy is used which has made a breaking changes to these features, code emitted by zerocopy-derive will fail to compile.

I genuinely can't figure out whether it's even possible - let alone possible cleanly and without massive headache - to solve the semver problem.

I also considered introducing a third zerocopy-derive-utils crate which contains the features in question. zerocopy-derive could then take a dependency on this crate, and semver should sort everything out as normal. This should work, but introduces a third crate, which is pretty suboptimal.

Anyone dealt with anything similar or have any advice?

Why, exactly, do you need to depend from zerocopy-derive on zero-copy?

Derive can emit something like quote! { ::zerocopy::rtm::helper() } without depending on zerocpoy. After all, the result of proc macro is basically a string, it isn’t resolved until it is spliced into user’s code, where ::zerocopy would resolve properly.

We could absolutely do that, but it would implicitly assume that a particular version of zerocopy was the one being compiled (namely, a version that has added the new features). On its own, this would break if a user, e.g. depended upon a newer version of zerocopy-derive and an older copy of zerocopy.

I had a chat offline with @jswrenn about this, and he convinced me to just sync the versions of zerocopy and zerocopy-derive. That should eliminate the problem by making it so that the two crates are, for all intents and purposes, part of a single atomic codebase. I'll update the original question.

Note that this would not work. Right now, there is no way at all for proc-macros to emit paths other than ones which will be interpreted within the scope of the crate containing the macro call. The best you can possibly do is the exact-match versioning you mentioned — and that will still break if the crate depending on zerocopy decides to rename said dependency, or if it tries to reexport your macros to its dependents.

(macro_rules! macros have it a little better; they can use $crate to refer to their own crate, which is sufficient since they don't have to be in separate proc-macro crates. But any paths that don't start with $crate are still unhygienic.)

2 Likes

That's the approach I would use.

In practice, the zerocopy-derive crate is useless without pulling in zerocopy, so I would just tell people to import the derives via zerocopy then update zerocopy's Cargo.toml to use an = version constraint.

# Cargo.toml
[package]
name = "zerocopy"
version = "1.2.3"

[dependencies]
zerocopy-derive = { version = "=1.2.3", optional = true }

[features]
default = ["derive"]
derive = ["zerocopy-derive"]

For fn like macro, there is a trick: define a macro-rules wrapper in the runtime crate, which just forwards args to proc-macro, appending $crate token in front.

2 Likes

What I tend to do is I also lock the versions together, and then reëxport the macro from the non-macro crate. In this way, a user only has to depend on a single crate, so s/he doesn't have any additional invariants to ensure manually.

Yeah, I ended up going the single-version dependency route. I'm OK with code breaking which depends on zerocopy-derive directly or which reexports zerocopy-derive via another crate; it should be an unlikely situation, and one whose fix would be pretty obvious.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.