Search code examples
rustrust-macrosrust-proc-macros

Cyclic package dependency while implementing proc macro


I try to implement a proc_macro Dump, which is similar to serdes Serialize.

For this purpose I have a crate foo which contains my "primitive" structs (P1 and P2 in this case) which should only be dumpable.

Next I do have a foo_derive crate which contains the procedural macro itself.

Because I want to support multiple formats I have a third crate foo_dump which contains the trait definition of Dump (e.g. this struct can be dumped) and Dumper (this is something the backend should implement). Very straight forward until this point.

When I now want to compile it, I get this error:

$ cargo build
error: cyclic package dependency: package `foo v0.1.0 (/tmp/tmp.u34pI5J6qd/example/foo)` depends on itself. Cycle:
package `foo v0.1.0 (/tmp/tmp.u34pI5J6qd/example/foo)`
    ... which is depended on by `foo_dump v0.1.0 (/tmp/tmp.u34pI5J6qd/example/foo_dump)`
    ... which is depended on by `foo_derive v0.1.0 (/tmp/tmp.u34pI5J6qd/example/foo_derive)`

I don't know what the correct way is, how to use dependencies in this crate. My current one is:

dependencies

and this certainly is not possible.

What am I missing? What do I have to do to break the dependency circle?


(mcve@github)

/Cargo.toml

[workspace]
members = [ 
    "foo",
    "foo_derive",
    "foo_dump",
]

/foo/Cargo.toml

[package]
name = "foo"
version = "0.1.0"
edition = "2018"

[dependencies]
foo_derive = { path = "../foo_derive" }

/foo/src/lib.rs

use foo_derive::Dump;

struct P1;
struct P2;

#[derive(Dump)]
struct Bar {
    primitive_one: P1,
    primitive_two: P2,
}

/foo_dump/Cargo.toml

[package]
name = "foo_dump"
version = "0.1.0"
edition = "2018"

[dependencies]
foo = { path = "../foo" }

/foo_dump/src/lib.rs

use foo::{P1, P2};

pub trait Dumper {
    fn dump_p1(&mut self, value: &P1);
    fn dump_p2(&mut self, value: &P2);
}

pub trait Dump {
    fn dump<D: Dumper>(&self, d: D);
}

impl Dump for P1 {
    fn dump<D: Dumper>(&self, d: D) {
        d.dump_p1(self);
    }
}

impl Dump for P2 {
    fn dump<D: Dumper>(&self, d: D) {
        d.dump_p2(self);
    }
}

/foo_derive/Cargo.toml

[package]
name = "foo_derive"
version = "0.1.0"
edition = "2018"

[lib]
proc-macro = true

[dependencies]
syn = "*"
quote = "*"
foo_dump = { path = "../foo_dump" }

/foo_derive/src/lib.rs

extern crate proc_macro;

use quote::quote;
use proc_macro::TokenStream;
use syn::DeriveInput;

#[proc_macro_derive(Dump)]
pub fn derive_dump(input: TokenStream) -> TokenStream {
    let input = syn::parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    quote!(
        impl foo_dump::Dump for #name {
            fn dump<D: foo_dump::Dumper>(&self, d: D) {
                unimplemented!()
            }
        }
    ).into()
}

Solution

  • Thanks to @Boiethious comment and his help in chat I was able to come up with a solution, which involes introducing a new crate foo_core which contains the structs P1 and P2.

    So what I did was:

    • Removing P1 and P2 from foo and putting them into foo_core
    • Removing the dependency foo_dump from foo_derive so it only depends on syn and quote anymore
    • Adding foo_core as a dependency in foo and foo_dump
    • Adding the dependency foo_dump to foo

    (you can see the full list of changes in the git history).

    The final dependency chain now looks like this:

    depency graph