CS3984 Computer Systems in Rust



Crates

Crates are the smallest amount of code the Rust compiler considers at a time.

When compiling a single .rs file with rustc, the compiler considers that file a crate of one of two possible types:

  • Binary crate
  • Library crate

Library Crate

The pub keyword can be used to make a function accessible externally.

// Crates using this library can access the `add` function
pub fn add(a: i32, b: i32) -> i32 {
  a + b
}

// Crates cannot access the `multiply` function
fn multiply(a: i32, b: i32) -> i32 {
  a * b
}


When compiling a library with rustc, set the --crate-type flag to lib.

$ rustc --crate-type=lib adder.rs$ lslibadder.rlib


Binary Crate

Binary crates must have a main function.

fn main() {
    let sum = adder::add(5, 6);
    println!("Sum: {sum}");

    // Error! `private_function` is private
    //let product = adder::multiply(5, 6);
}


Libraries used must be linked with the --extern flag.

$ rustc prog.rs --extern adder=libadder.rlib$ ./progSum: 11


Package

A package is a bundle of one or more crates. We can use Cargo to simplifying building packages and their dependencies.

$ cargo new bin hello_world$ tree ./hello_world./hello_world/├── Cargo.toml└── src    └── main.rs


Cargo.toml is the manifest in the TOML format:

$ cat ./hello_world/Cargo.toml[package]name = "hello_world"version = "0.1.0"edition = "2021"[dependencies]


Library Package

A library package can be created with --lib:

$ cargo new --lib hello_world_lib$ tree ./hello_world_lib./hello_world_lib/├── Cargo.toml└── src    └── lib.rs


$ cat ./hello_world_lib/Cargo.toml[package]name = "hello_world_lib"version = "0.1.0"edition = "2021"[dependencies]


Binary + Library Package

Your package can generate both binary and library crates:

./hello_world├── Cargo.toml└── src    ├── lib.rs    └── main.rs


…Or even multiple binary crates:

./hello_world├── Cargo.toml└── src    ├── lib.rs    ├── main.rs    └── bin        ├── alt_bin.rs        └── another_bin.rs


Building Packages

Packages can be built with cargo build:

$ tree -L 3 ..├── Cargo.lock├── Cargo.toml├── src│   ├── lib.rs│   └── main.rs└── target    ├── CACHEDIR.TAG    └── debug        ├── build        ├── deps        ├── examples        ├── hello_world        ├── hello_world.d        ├── incremental        ├── libhello_world.d        └── libhello_world.rlib


Building Packages (2)

By default, packages are built with the dev profile, with low optimizations, debug symbols and more:

$ cargo build    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s


Cargo places debug builds at ./target/debug/.

Running cargo build with the --release flag compiles your package for production use and places the result at ./target/release/.

$ cargo build --release    Finished `release` profile [optimized] target(s) in 0.00s


Running packages

The cargo run command will build your program and execute it:

fn main() {
    println!("Hello, world!");
}


$ cargo run   Compiling hello_world v0.1.0 (/path/to/hello_world)    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s     Running `target/debug/hello_world`Hello, world!


The release equivalent is cargo run --release.

Modules

Modules are logical units of related code. They provide allow controlling the namespace and visibility of name.

mod adder {
    fn add(x: i32, y: i32) -> i32 {
        x + y
    }
}

mod sub {
    fn sub(x: i32, y: i32) -> i32 {
        x - y
    }
}

// Here, the visible names are `adder` and `sub`

fn main() {
    println!("Hello, world!");
}


Visibility

Visibility is a property that determines whether a module can refer to the name of an item.

By default, items in modules have private visibility, but can be made public with the pub keyword.

If an item is visible, it can be accessed through a path, which are components of the module hierarchy separated by :: double colons.

mod adder {
    pub fn add(x: i32, y: i32) -> i32 {
        x + y
    }
}

fn main() {
    let x = adder::add(5, 6);
    println!("Sum: {x}");
}


Nested modules

Modules can be nested. The use keyword allows making the name of an item directly accessible in the current namespace by specifying its path:

mod arithmetic {
  pub mod adder {
    pub fn add(x: i32, y: i32) -> i32 { x + y }
  }

  pub mod multiplier {
    pub fn mul(x: i32, y: i32) -> i32 { x * y }
  }
}

use arithmetic::adder::add;

fn main() {
    let x = add(5, 6);
    println!("Sum: {x}");
}


Relative Path

In a module, self refers to the current module, and super refers to its parent module.

mod arithmetic {
  pub mod adder {
    pub fn add(x: i32, y: i32) -> i32 {
      x + y
    }
  }

  pub fn add_one(x: i32) -> i32 {
    self::adder::add(x, 1)
  }

  pub mod subber {
    pub fn sub_one(x: i32) -> i32 {
      super::adder::add(x, -1)
    }
  }
}


Absolute Paths

Absolute paths start with crate, which refers to the crate root. This is usually src/lib.rs for a library crate and src/main.rs for a binary crate.

mod arithmetic {
  pub mod adder {
    pub fn add(x: i32, y: i32) -> i32 {
      x + y
    }
  }

  pub fn add_one(x: i32) -> i32 {
    crate::arithmetic::adder::add(x, 1)
  }

  pub mod subber {
    pub fn sub_one(x: i32) -> i32 {
      crate::arithmetic::adder::add(x, -1)
    }
  }
}


use Declarations

use declarations can be bind multiple names, rename items or made public.

mod arithmetic {
  pub use self::{adder::add, multiplier::multiply};

  mod adder {
    pub fn add(x: i32, y: i32) -> i32 { x + y }
  }

  mod multiplier {
    pub fn multiply(x: i32, y: i32) -> i32 { x * y }
  }
}

use arithmetic::multiply as mul;

fn main() {
    let x = mul(5, 6);
    println!("Product: {x}");
}


Modules in Separate Files

Modules can be in placed in separate files:

Current File Module Declaration Possible modules
src/lib.rs mod arithmetic; src/arithmetic.rs
src/arithmetic/mod.rs
src/arithmetic.rs
src/arithmetic/mod.rs
mod adder; src/arithmetic/adder.rs
src/arithmetic/adder/mod.rs
.├── arithmetic│   └── adder.rs├── arithmetic.rs└── lib.rs


.├── arithmetic│   ├── adder.rs│   └── mod.rs└── lib.rs


.├── arithmetic│   ├── adder│   │   └── mod.rs│   └── mod.rs└── lib.rs


Binary + Library Package

For packages with both a library and binary crate, the binary crate can access the library crate through the name of your package:

pub fn adder(x: i32, y: i32) -> i32{
    x + y
}


fn main() {
    let sum = hello_world::adder(1, 2);
    println!("{sum}");
}


[package]name = "hello_world"version = "0.1.0"edition = "2021"[dependencies]


Package Dependencies

Package dependencies are specified in the manifest Cargo.toml:

[package]name = "djot"version = "0.1.0"edition = "2021"[dependencies]regex = "1.10.6"quick-js = { version = "0.4.1", features = ["patched"] }tree-sitter-toml-ng = { path = "../../vendor/tree-sitter-toml" }minijinja = { git = "https://github.com/mitsuhiko/minijinja.git", rev = "1f119ec" }


Cargo will take care of compiling and linking your program, and you get access to the library crates of the listed dependencies in your package.