Item 24: Re-export dependencies whose types appear in your API
The title of this Item is a little convoluted, but working through an example will make things clearer.1
Item 25 describes how cargo
supports different versions of the same library crate being linked into a
single binary, in a transparent manner. Consider a binary that uses the rand
crate—more specifically, one that uses some 0.8 version of the crate:
# Cargo.toml file for a top-level binary crate.
[dependencies]
# The binary depends on the `rand` crate from crates.io
rand = "=0.8.5"
# It also depends on some other crate (`dep-lib`).
dep-lib = "0.1.0"
let mut rng = rand::thread_rng(); // rand 0.8
let max: usize = rng.gen_range(5..10);
let choice = dep_lib::pick_number(max);
The final line of code also uses a notional dep-lib
crate as another dependency. This crate might be another
crate from crates.io
, or it could be a local crate that is located via Cargo's path
mechanism.
This dep-lib
crate internally uses a 0.7 version of the rand
crate:
# Cargo.toml file for the `dep-lib` library crate.
[dependencies]
# The library depends on the `rand` crate from crates.io
rand = "=0.7.3"
//! The `dep-lib` crate provides number picking functionality.
use rand::Rng;
/// Pick a number between 0 and n (exclusive).
pub fn pick_number(n: usize) -> usize {
rand::thread_rng().gen_range(0, n)
}
An eagle-eyed reader might notice a difference between the two code examples:
- In version 0.7.x of
rand
(as used by thedep-lib
library crate), therand::gen_range()
method takes two parameters,low
andhigh
. - In version 0.8.x of
rand
(as used by the binary crate), therand::gen_range()
method takes a single parameterrange
.
This is not a back-compatible change, and so rand
has increased its leftmost version component
accordingly, as required by semantic versioning (Item 21). Nevertheless, the binary that combines the two
incompatible versions works just fine—cargo
sorts everything out.
However, things get a lot more awkward if the dep-lib
library crate's API exposes a type from its dependency, making
that dependency a public
dependency.
For example, suppose that the dep-lib
entrypoint involves an Rng
item—but specifically a version-0.7 Rng
item:
/// Pick a number between 0 and n (exclusive) using
/// the provided `Rng` instance.
pub fn pick_number_with<R: Rng>(rng: &mut R, n: usize) -> usize {
rng.gen_range(0, n) // Method from the 0.7.x version of Rng
}
As an aside, think carefully before using another crate's types in your API: it intimately ties your crate to that of the dependency. For example, a major version bump for the dependency (Item 21) will automatically require a major version bump for your crate too.
In this case, rand
is a semi-standard crate that is widely used and pulls in only a small number of
dependencies of its own (Item 25), so including its types in the crate API is probably fine on balance.
Returning to the example, an attempt to use this entrypoint from the top-level binary fails:
let mut rng = rand::thread_rng();
let max: usize = rng.gen_range(5..10);
let choice = dep_lib::pick_number_with(&mut rng, max);
Unusually for Rust, the compiler error message isn't very helpful:
error[E0277]: the trait bound `ThreadRng: rand_core::RngCore` is not satisfied
--> src/main.rs:22:44
|
22 | let choice = dep_lib::pick_number_with(&mut rng, max);
| ------------------------- ^^^^^^^^ the trait
| | `rand_core::RngCore` is not
| | implemented for `ThreadRng`
| |
| required by a bound introduced by this call
|
= help: the following other types implement trait `rand_core::RngCore`:
&'a mut R
Investigating the types involved leads to confusion because the relevant traits do appear to be implemented—but
the caller actually implements a (notional) RngCore_v0_8_5
and the library is expecting an implementation of
RngCore_v0_7_3
.
Once you've finally deciphered the error message and realized that the version clash is the underlying cause, how can you fix it?2 The key observation is to realize that while the binary can't directly use two different versions of the same crate, it can do so indirectly (as in the original example shown previously).
From the perspective of the binary author, the problem could be worked around by adding an intermediate wrapper crate
that hides the naked use of rand
v0.7 types. A wrapper crate is distinct from the binary crate and so is allowed to
depend on rand
v0.7 separately from the binary crate's dependency on rand
v0.8.
This is awkward, and a much better approach is available to the author of the library crate. It can make life easier for its users by explicitly re-exporting either of the following:
- The types involved in the API
- The entire dependency crate
For this example, the latter approach works best: as well as making the version 0.7 Rng
and RngCore
types available,
it also makes available the methods (like thread_rng()
) that construct instances of the type:
// Re-export the version of `rand` used in this crate's API.
pub use rand;
The calling code now has a different way to directly refer to version 0.7 of rand
, as dep_lib::rand
:
let mut prev_rng = dep_lib::rand::thread_rng(); // v0.7 Rng instance
let choice = dep_lib::pick_number_with(&mut prev_rng, max);
With this example in mind, the advice given in the title of the Item should now be a little less obscure: re-export dependencies whose types appear in your API.
This example (and indeed Item) is inspired by the approach used in the RustCrypto crates.
This kind of error can
even appear when the dependency graph includes two alternatives for a crate with the same version, when something in
the build graph uses the
path
field to
specify a local directory instead of a crates.io
location.