Pre-RFC: extension crates


#1

The Re-Rebalancing Coherence RFC raised an important observation about the unfortunate limitations introduced by the “orphan rule”:

A long standing pain point for crates like Diesel has been integration with other crates. Diesel doesn’t want to care about chrono, and chrono doesn’t want to care about Diesel. A database access library shouldn’t dictate your choice of time libraries, vice versa.

However, due to the way Rust works today, one of them has to. Nobody can create a diesel-chrono crate due to the orphan rule. Maybe if we just allowed crates to have incompatible impls, and set a standard of “don’t write orphan impls unless that’s the entire point of your crate”, it wouldn’t actually be that bad.

The orphan rule exists to prevent crates from producing overlapping trait implementations. If the orphan rule did not exist, then adding a new dependency to your crate could break compilation due to overlapping trait implementations in two of your dependencies.

If we could provide a special crate type dedicated to orphan implementations, though, then only these extension crates could introduce conflicts. Such crates could take care to provide feature flags to selectively enable/disable particular impls, or they could be limited to a single impl, in order to make resolution of such conflicts trivial. By requiring these crates to extend an existing crate, they could potentially even gain access to pub(crate) implementation details, which may be necessary for implementing external traits (although exposing pub(crate) implementation details could cause SemVer issues, and so might be undesirable).

This could potentially be implemented in Cargo, without any changes needed to the Rust compiler at all. A new [ext] crate type could be defined that essentially injects source code into another existing [lib] crate. Cargo.toml could be extended to allow adding extension crate dependencies, perhaps using a syntax similar to feature flags.

If Cargo supported this, then diesel’s chrono feature could be moved into a separate diesel-chrono [ext] crate. More importantly, though, third party users of libraries like diesel and chrono could write their own [ext] crates to enable interoperability without needing to branch the source code for either dependency.

If there is any interest in this, and anyone else wants to carry the torch of writing an RFC, that would be great (unfortunately, I don’t really have time to dedicate to it right now).


#2

I think one thing that would help is if Cargo would be able to tell you about potential coherence issues while resolving the dependency tree. For example if you include both diesel-chrono and chrono-diesel, you’d get an error.


#3

You would already get an error, but from rustc, not Cargo. We would definitely need useful error messages added either to Cargo or rustc.


#4

Indeed, ideas like this have been tossed around many times before.

The fundamental flaw I’ve seen with almost all of the ideas in this space is that they merely push the problem around without actually solving it. For instance, any flavor of solution involving a special crate for orphan impls just runs into all of the same coherence and/or dependency hell problems all over again as soon as two of those special crates end up in your dependency graph.


#5

Well, the coherence issue would still be managed by rustc's enforcement of non-overlapping implementations, so you would always get a compile-time error, never unexpected runtime behavior. Dependency hell would be pretty minimal, compared to a free-for-all, because these special crate types could (in theory) be limited to only providing impls (no public functions, structs, enums, etc). And as mentioned, you could take this to the extreme of only allowing a single impl per extension. You would end up with a lot of [ext] dependencies, but you would never have dependency hell in the sense that you need things from two conflicting crates.

Edit: ahh, I see what you mean about if they are lower in your dependency graph. Yes, I suppose that is a problem. It still might be solvable using a similar approach to feature flags, though.


#6

IMO, this kind of reasoning makes the whole concept of using crates to solve the problem suspect: how is a special kind of crate with normal impls in it fundamentally different from a normal crate with special kinds of impls? They’re both impls, in crates, and dependencies, and encapsulate all the conflict resolution problems therein.


#7

I like such idea in general.

However, the potential for conflicts has to be managed somehow, because if anyone can make an [ext] crate, then we will get conflicts.

One solution I imagine is that a crate could somehow opt-in to having other crates implement its traits, e.g. "diesel says it’s OK to implement its traits in diesel-chrono", or perhaps it could be solved at registry level that only users who own diesel may upload a crate with the [ext] option that affects diesel.