Rust library

Now that we have hello world let's do something more real by creating a library (crate), unit testing it, and making our binary call it.

Creating the library

We're going to use multiple files for our trivial library to demonstrate how that is handled in Bazel.

One thing we'll need to think about is what we want the crate name to be. In the C++ code above you can see that the header file location is based on the path to the file in the repo. If we had Java files, we'd also see this there, where package names are nested and based on their path.

To make something that resembles this for Rust crate names in the monorepo I've decided to name my crates based on the path to them. I also have them all have the same top-level prefix to ensure I avoid classes with third-party crates. This also makes it easy for me to see a crate name in a source file (like in a use statement) and know where to find it in the monorepo.

Having settled the naming delima let's add the library to $HOME/repo/src/summation/BUILD by adding these lines to the file (the name attribute is what the crate name defaults to):

load("@rules_rust//rust:defs.bzl", "rust_library")
rust_library(
    name = "src_summation",
    srcs = [
        "lib.rs",
        "f64.rs",
        "u32.rs",
    ],
    deps = [],
)

We have to list all the files we want to compile against here. Otherwise Bazel won't copy them into the sandbox where our library is compiled. I like listing all the files explictly, but if you want to include all "*.rs" files Bazel provides a glob() to do this.

Now let's make $HOME/repo/src/summation/lib.rs:

#![allow(unused)]
fn main() {
pub mod f64;
pub mod u32;
}

If we don't have the mod lines, when bazel runs rustc it'll ignore the f64.rs and u32.rs files since rustc uses the crate root source file to figure out what to compile. Including them in the BUILD file gets them copied over to the sandbox rustc is run in, adding them to lib.rs gets rustc to compile them.

And lets make $HOME/repo/src/summation/f64.rs:

#![allow(unused)]
fn main() {
pub fn summation_f64(values: &[f64]) -> f64 {
    values.iter().sum()
}
}

Wow, that's a boring function. Clearly I picked a simple example. Let's make a boring $HOME/repo/src/summation/u32.rs file as well:

#![allow(unused)]
fn main() {
pub fn summation_u32(values: &[u32]) -> u32 {
    values.iter().sum()
}
}

Now let's build it. From anywhere in the workspace we can build this using it's full path:

bazel build //src/summation:src_summation

The // maps to the root of the workspace which is $HOME/repo in our example, //src/summation says we are talking about that path from the workspace root, and then src_summation is the target inside the build file that we are trying to build. If we're already in $HOME/repo/src/summation we can omit the path and just use bazel build :src_summation for short.

We can also run bazel build :all to build all the targets in the directory we are in. This should be a no-op if you manually built the executable and lib already since none of the source files have changed so bazel just uses the cached build and doesn't need to remake them.