How to structure a big project in Rust

If you have been working with rust you know how easy it is to start a project with cargo. But if your project is getting bigger it is time to restructure your project folder. Luckily with cargo we can easily working with multiple workspaces.

Cargo Project

Starting new project with cargo is easy just run command cargo new my-awesome-project and cargo will generate all the necessary file for us. Here is how it look like.

├── Cargo.lock
├── Cargo.toml
└── src
    └── main.rs

With this project we can easily build or run our Rust application with this command.

cargo run
cargo build
cargo build --release

And this is the folder for binary build generated by cargo.

target
├── CACHEDIR.TAG
├── debug
│   ├── build
│   ├── deps
│   │   ├── my_awesome_project-4dd09ba79bb2f517
│   │   └── my_awesome_project-4dd09ba79bb2f517.d
│   ├── examples
│   ├── incremental
│   │   └── my_awesome_project-elaw0wowg6wz
│   │       ├── s-g83xfdn5el-1xjezbg-7yyks5fhypj6
│   │       │   ├── 118s63ywbnshvaip.o
│   │       │   ├── 129a5c65rp0ll3v9.o
│   │       │   ├── 1bof6u35w0wbt322.o
│   │       │   ├── 1cci6ep0k0a1brvs.o
│   │       │   ├── 29462uigmco4mw3f.o
│   │       │   ├── 2b6p2m3or8jk8a40.o
│   │       │   ├── 3zq5b0f836txq4lf.o
│   │       │   ├── akdo6055f5ygsu8.o
│   │       │   ├── dep-graph.bin
│   │       │   ├── query-cache.bin
│   │       │   └── work-products.bin
│   │       └── s-g83xfdn5el-1xjezbg.lock
│   ├── my-awesome-project
│   └── my-awesome-project.d
└── release
    ├── build
    ├── deps
    │   ├── my_awesome_project-4004792075343d18
    │   └── my_awesome_project-4004792075343d18.d
    ├── examples
    ├── incremental
    ├── my-awesome-project
    └── my-awesome-project.d

We have this folder for debug binary file.

target
├── debug
│   ├── build
│   ├── my-awesome-project

And we can just execute the binary like this.

./target/debug/my-awesome-project

And this one for release binary.

./target/release/my-awesome-project

Cargo Workspace

With cargo project we can add multiple cargo project to current project. So here is how to add new workspace to current project.

cargo new libs/awesome-lib --lib

And now here is the folder structure.

├── Cargo.lock
├── Cargo.toml
├── libs
│   └── awesome-lib
│       ├── Cargo.toml
│       └── src
│           └── lib.rs
└── src
    └── main.rs

The next step is link the new workspace to current project. If you see here is our current Cargo.toml file.

[package]
name = "my-awesome-project"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

And here is how to add our workspace to cargo file.

[workspace]
members = [
    "libs/awesome-lib",
    "libs/other-lib",
]

Linking between workspaces

So now let's add simple function to our other-lib. Open libs/other-lib/lib.rs and add this simple function.

// libs/other-lib/lib.rs

pub fn add(a: usize, b: usize) -> usize {
    a + b
}

And now let's call those function inside libs/awesome-lib/lib.rs. First we need to add this other-lib to libs/awesome-lib/Cargo.toml.

# libs/awesome-lib/Cargo.toml

[dependencies]
other-lib = { path = "../other-lib" }

Now we can run test for awesome-lib like this, don't forget to execute this command from root project.

cargo test --package awesome-lib --lib -- tests

Building

One last thing here is how to build the library.

cargo build --release --workspace -p awesome-lib

And this is the build folder will look like.

target
├── CACHEDIR.TAG
└── release
    ├── build
    ├── deps
    │   ├── awesome_lib-4b10f631e68d6b01.d
    │   ├── libawesome_lib-4b10f631e68d6b01.rlib
    │   ├── libawesome_lib-4b10f631e68d6b01.rmeta
    │   ├── libother_lib-f4c8f38316727503.rlib
    │   ├── libother_lib-f4c8f38316727503.rmeta
    │   ├── my_awesome_project-4004792075343d18
    │   ├── my_awesome_project-4004792075343d18.d
    │   └── other_lib-f4c8f38316727503.d
    ├── examples
    ├── incremental
    ├── libawesome_lib.d
    ├── libawesome_lib.rlib
    ├── libother_lib.d
    ├── libother_lib.rlib
    ├── my-awesome-project
    └── my-awesome-project.d

Use Case

Working with cargo workspaces is very simple. So here is some story of my project and why I decided to split the project to several workspace.

Since last january I'm working on code syntax highlighting for my side project heline.dev. The code highlighting it self was inspired from github. This project is containing several module.

  1. Rules => yaml file to define token to hightlight.
  2. Generator => for generating rust code from rules files.
  3. Lexers => The code code itself.
  4. Tests => This is to test all the lexers.

Before using cargo workspace all of the code for this was inside src/ folder. In this case I want to split this project module by module, so when publishing the library we don't need to publish all unnecessary code to crates.

Here is what the directory looks like. I follow ripgrep for the folder structure.

├── crates
│   ├── core
│   │   └── src
│   │       └── lexers
│   ├── generator
│   │   └── src
│   │       └── stub
│   └── tests
│       └── src
│           └── test
│               └── testdata
│                   ├── input
│                   └── output
├── examples
└── rules

And I think that's all for today, if you interested see full source code here https://github.com/ahmadrosid/hl. Let me know if you have any question. Thank you!