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.
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 which uses some 0.8 version of the crate:
# Top-level binary crate
[dependencies]
dep-lib = "0.1.0"
rand = "0.8.*"
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, and this crate internally uses1 a 0.7 version of the rand
crate:
# dep-lib library crate
[dependencies]
rand = "0.7.3"
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 non-self
parameters,low
andhigh
. - In version 0.8.x of
rand
(as used by the binary crate), therand::gen_range()
method takes a single non-self
parameterrange
.
This is a non-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 out2.
However, things get a lot more awkward if the library crate's API exposes a type from its dependency, making that
dependency a public
dependency.
In the example,
this involves an Rng
item – but specifically a version-0.7 Rng
item:
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 high quality and widely used, and which only pulls in 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
--> re-export/src/main.rs:17:48
|
17 | 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
|
= note: required because of the requirements on the impl of `rand::Rng` for `ThreadRng`
note: required by a bound in `pick_number_with`
--> /Users/dmd/src/effective-rust/examples/dep-lib/src/lib.rs:17:28
|
17 | pub fn pick_number_with<R: Rng>(rng: &mut R, n: usize) -> usize {
| ^^^ required by this bound in `pick_number_with`
Investigating the types involved leads to confusion because the relevant traits do appear to be implemented –
but the caller actually implements a RngCore_v0_8_3
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 clash3 is the underlying cause, how can you fix it? The key observation is to realize that the while the binary can't directly use two different versions of the same crate, it can do so indirectly (as in the original example above).
From the perspective of the binary author, the problem can 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:
- the types involved in the API
- the entire dependency crate.
For the example, the latter approach work 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 of the title should now be a little less obscure: re-export dependencies whose types appear in your API.
1: This example (and indeed Item) is inspired by the approach used in the RustCrypto crates.
2: This is possible because the Rust toolchain handles linking, and does not have the constraint that C++ inherits from C of needing to support separate compilation.
3: 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.