Library crates and targets

A package with a src/lib.rs is a library, and the same package may also have a src/main.rs or files in src/bin. Cargo calls each one a target: the library is compiled once, the binaries link against it, the tests link against it, and the examples in the documentation become tests of their own.

This lesson builds one package with all of those targets, runs the library through a binary, watches the documentation example run as a test, and ends with a package that holds two binaries and cannot decide which one to run.

A library crate

The library exposes pub items and keeps the rest private. The two functions below build on each other, and the documentation comment above the first one is what the next section turns into a test.

//! Small arithmetic helpers. /// Adds two numbers. /// /// ``` /// assert_eq!(calc::add(2, 2), 4); /// ``` pub fn add(left: i32, right: i32) -> i32 { left + right } /// Doubles a number. pub fn double(value: i32) -> i32 { add(value, value) }

cargo new --vcs none --lib calc
cargo build

   Compiling calc v0.1.0 (/tmp/rw7/L085/calc)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.57s

The artefact is target/debug/libcalc.rlib, a Rust library that other crates of the workspace link against. A library has no main, so cargo run has nothing to start until a binary target exists. The lesson on crates explains the file layout.

Doc tests

A fenced block in a documentation comment is compiled and run by cargo test. The example calls the library by its crate name, which is why the docs of a library read like a user manual and stay correct: an example that no longer compiles fails the suite.

cargo test

    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.03s
     Running unittests src/lib.rs (target/debug/deps/calc-c10086937597dec3)

running 1 test
test tests::adds_two_numbers ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests calc

running 1 test
test src/lib.rs - add (line 5) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

all doctests ran in 1.03s; merged doctests compilation took 1.03s

The doc test is named after the item and the line of the comment, which is enough to find it in a log. The unit test in the module is the one from the unit test lesson; the two kinds run in one command and report separately.

A binary beside the library

A src/main.rs in the same package is a binary target that links against the library by its crate name. The library and the binary are compiled separately, so the binary sees only the public API, exactly like an outside crate.

fn main() { println!("{}", calc::double(21)); }

cargo run

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/calc`
42

The binary is named after the package, and the library artefact sits beside it in the same folder. This split is the reason the tests can call the library without starting a program: they link the library and ignore the binary.

A second binary and default-run

Every file in src/bin is a binary target named after the file. Two binaries make cargo run ambiguous, and the error lists both names; the default-run key in the manifest, or a --bin flag, chooses one.

fn main() { for value in 1..=3 { println!("{} {}", value, calc::double(value)); } }

cargo run --bin table
cargo run

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/table`
1 2
2 4
3 6
error: `cargo run` could not determine which binary to run. Use the `--bin` option to specify a binary, or the `default-run` manifest key.
available binaries: calc, table

The third line of the error is the list to choose from, and default-run = "calc" in the [package] table makes the bare command work again. A package with two binaries and no default is a common state after a refactor, and the message is the whole fix.

Integration tests

A file in tests is compiled as its own crate that depends on the library, so it can use only the public API. That is the difference from the unit tests in the module, which see the private items too.

#[test] fn doubles_through_the_public_api() { assert_eq!(calc::double(21), 42); }

cargo test

     Running tests/integration.rs (target/debug/deps/integration-103df33abdfb3e16)

running 1 test
test doubles_through_the_public_api ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

The suite now has three kinds of test: the unit test inside the library, the integration test in its own file and the doc test in the comment. Cargo runs all of them in one command and reports a separate summary for each target, which is what a continuous integration log shows. The next lesson turns the same library into one that a C program can link.

The documentation comments are also the input of cargo doc, which writes the API reference into target/doc with the examples rendered as code. That command and cargo test read the same comments, so an example that is worth writing once is worth running on every build. A file in examples is a third kind of target: it is built by cargo build --examples and run by cargo run --example name.

Exercises

  1. Add a doc test that expects a panic and run the suite.
  2. Move a private helper into the library, keep it private, and add an integration test that uses only the public functions.
  3. Add a third binary and then a default-run key for it.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What makes a package a library?

    Answer

    A src/lib.rs target

    Cargo treats src/lib.rs as the library target of the package.

  2. When does a doc test run?

    Answer

    During cargo test, with the other tests

    The examples are compiled as tests of their own and reported separately.

  3. What can an integration test in tests access?

    Answer

    Only the public API of the library

    The test file is compiled as a crate that depends on the library, so privacy applies.

  4. What does default-run choose?

    Answer

    The binary a bare cargo run starts

    The key resolves the ambiguity when a package has several binaries.

  5. What does this program print? mod calc { pub fn double(value: i32) -> i32 { value * 2 } } fn main() { println!("{}", calc::double(21)); }

    Answer

    42

    The module is in the same crate, and the function is public.

  6. What does this program print? /// Adds one. fn add_one(value: i32) -> i32 { value + 1 } fn main() { println!("{}", add_one(41)); }

    Answer

    42

    A documentation comment is a comment, and the call returns forty-two.

  7. What does this program print? fn main() { let values = [1, 2, 3]; let doubled: Vec<i32> = values.iter().map(|value| value * 2).collect(); println!("{doubled:?}"); }

    Answer

    [2, 4, 6]

    The closure doubles each element and collect builds the vector.

  8. What does this program print? fn main() { assert_eq!(2 + 2, 4); println!("the example ran"); }

    Answer

    the example ran

    The assertion holds, so the line after it runs.

  9. What is the first line of the compiler output for this program? mod calc { fn double(value: i32) -> i32 { value * 2 } } fn main() { println!("{}", calc::double(21)); }

    Answer

    error[E0603]: function double is private

    The parent module can see the child module but not its private items.

  10. What does this program print? fn main() { let values = [1, 2, 3]; let first = values.get(3); println!("{}", first.unwrap_or(&0)); }

    Answer

    0

    The index is past the end, so the default reference is used and prints zero.

The code on this page was built with rustc 1.96.1 and cargo 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev