Good Practices for Writing Rust Libraries

• about 2,100 words

A few tips on how to make your projects easy to work on: rustfmt, lints, clippy, lots of metadata, continuous integration, automatically generated documentation, and homu.

For a bit more than a year now, I've been inter­ested in Rust, a pro­gram­ming lan­guage by Mozilla research that “runs blaz­ingly fast, pre­vents nearly all seg­faults, and guar­an­tees thread safety.” It is as low-level as C or C++, has a nice type sys­tem (with gener­ics and traits), a help­ful com­piler, and a great pack­age man­ager called Cargo.

Since Rust 1.0 was released half a year ago (in May 2015), a lot of libraries (“crates”) have been pub­lished to Car­go's main pub­lic reg­istry crates.io (includ­ing some of mine). Here are some good prac­tices[1] that help make your library easy to find, use, and extend by oth­ers.

Keep­ing Your Code Clean

It all starts with and comes back to the code you write. Rust will check a lot of things for you by default, but there are some more things you can do to make your code nicer to work with.

I'm only dis­cussing abstract styles here. You can find advice on how to struc­ture your pro­jec­t's logic or which pat­tern to imple­ment in the offi­cial book and Rust Design Pat­terns.

rustfmt

rustfmt was writ­ten to auto­mat­i­cally refor­mat your Rust code to make it eas­ily parseable by humans[2]. It works quite well and has­n't eaten any of my laun­dry for some time now. Just do this once in a while and your code will look like most other Rust code:

$ rustfmt src/lib.rs

While there is an ongo­ing effort to define The One True Rust Style and rustfmt tries to fol­low that, you can find a list of style options here. Cur­rently, most of my pro­jects use these set­tings (save this as rustfmt.toml in your pro­jec­t's root direc­tory):

format_strings = false
reorder_imports = true

Acti­vate More Lints

“Lints” are small com­piler plu­g­ins that check your code dur­ing com­pi­la­tion for (mostly) styl­is­tic issues. By default, rustc already has a few of them set to issue warn­ings, e.g. dead-code (warns when there is unreach­able code) or non-snake-case (warns when cer­tain items are not writ­ten in snake_case).

There are some more lints built in, that are pretty use­ful, though! And you can also set them to deny, which turns the harm­less warn­ings into hard errors (more in the ref­er­ence). I like to add these to my pro­jects:

#![deny(missing_docs,
        missing_debug_implementations, missing_copy_implementations,
        trivial_casts, trivial_numeric_casts,
        unsafe_code,
        unstable_features,
        unused_import_braces, unused_qualifications)]

Espe­cially the first one, missing_docs, is really use­ful: It pre­vents any changes that add undoc­u­mented pub­lic inter­faces from com­pil­ing. Deny­ing unsafe_code is quite nice as well, it makes your library look more trust­wor­thy and there­fore increases your chance of hav­ing plush bun­nies mailed to you.

You can get a list of all avail­able lints includ­ing descrip­tions with rustc -W help.

Even More Lints

As I'm sure you've noticed, lints are good. That's why the authors of clippy have writ­ten about a hun­dred more[3]. Since clippy is a com­piler plu­gin, you cur­rently need the nightly com­piler to use it. You can invoke it in var­i­ous ways, e.g. using cargo-clippy, or by adding it as an optional depen­dency to your pro­ject.

I tend to do the lat­ter and add this to my Cargo.toml:

[dependencies]
clippy = {version = "0.0.21", optional = true}

[features]
default = []
dev = ["clippy"]

This marks clippy as optional and only includes it if the fea­ture flag dev is set. In the crate's main file, you can then con­di­tion­ally include it like this:

#![cfg_attr(feature = "dev", allow(unstable_features))]
#![cfg_attr(feature = "dev", feature(plugin))]
#![cfg_attr(feature = "dev", plugin(clippy))]

Build­ing your pro­ject with cargo build --features "dev" now auto­mat­i­cally includes clip­py's lints. (By the way, you only need to allow unstable_features if you have pre­vi­ously denyed it, of course.)

Please be aware that com­piler plu­g­ins are not sta­ble right now. If you update to a newer nightly, clippy might break. (The authors are quick to update, though.)

As with the built-in lints, there are a few clippy lints that are set to “allow” by default. Have a look at the pro­jec­t's doc­u­men­ta­tion to find what more you can enable!

Tests

Rust's inte­grated sup­port for test­ing is amaz­ing: You can quickly write unit tests inline with your mod­ules and cargo han­dles run­ning inte­gra­tion tests (Rust files in the tests/ direc­tory) auto­mat­i­cally. Oh, and exam­ples in doc­u­men­ta­tion (or in examples/) are tested as well.

There's not much more for me to say here. Just read the chap­ter in the offi­cial book!

Pro­ject Infra­struc­ture

Aside from writ­ing the code, there are a few things to think about when pub­lish­ing your pro­ject. I'm assum­ing you'll want to put you code on GitHub, but that's not a require­ment.

Cargo Meta­data

The first and quick­est thing to do to make your library easy to find is to fill out your Cargo.toml file. There are a lot of meta­data fields that will be used by crates.io. Here is an exam­ple from my HSL crate:

[package]
name = "hsl"
version = "0.1.0"
authors = ["Pascal Hertleif <my@email.address>"]
repository = "https://github.com/killercup/hsl-rs.git"
homepage = "https://github.com/killercup/hsl-rs.git"
license = "MIT"
readme = "README.md"
documentation = "http://killercup.github.io/hsl-rs/"
description = "Represent colors in HSL and convert between HSL and RGB."

By the way: Don't use wild­card ver­sions for your depen­den­cies. crates.io will reject them as they are an escape hatch from the seman­tic ver­sion­ing that Cargo assumes. You can use cargo-edit to quickly add the lat­est ver­sion of a crate as a depen­dency.

README.md

Peo­ple look­ing at your repos­i­to­ry's start page will most likely see the con­tents of your Readme.md file. Make sure it answers the usual ques­tions:

The Readme file is also a good place for small exam­ples show­ing how to use your library – peo­ple love to copy-paste these small exam­ples to get started. To ensure that the exam­ples in your Readme.md (and other doc­u­men­ta­tion writ­ten in Mark­down) com­pile, you can use skep­tic. By adding a small hook to Car­go's build process (a build.rs file), you can invoke skep­tic to trans­form code snip­pets in Mark­down files to reg­u­lar tests. (See skep­tic's doc­u­men­ta­tion for more infor­ma­tion.)

Other Meta Files

Don't for­get to include a .gitignore file that pre­vents git from track­ing the target/ direc­tory. For non-binary crates, you should also ignore the Cargo.lock file.

Another file that I try to add to every pro­ject is .editorconfig. I use these set­tings:

root = true

[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4

[*.md]
trim_trailing_whitespace = false

Con­tin­u­ous Inte­gra­tion

If your open source pro­ject is hosted on GitHub, you can get free con­tin­u­ous inte­gra­tion builds from Travis CI. This is super use­ful, as it can be con­fig­ured to run your tests on each push in var­i­ous envi­ron­ments (e.g. agains Rust sta­ble, beta, and nightly).

Even bet­ter, using travis-cargo you can have Travis run your tests and bench­marks, gen­er­ate your doc­u­men­ta­tion (and push it to GitHub Pages), and cal­cu­late your code cov­er­age (and push it to Cov­er­alls).

The basic con­fig­u­ra­tion looks like this:

sudo: false
language: rust
rust:
- nightly
- beta
- stable
matrix:
  allow_failures:
  - rust: nightly
before_script:
- |
  pip install 'travis-cargo<0.2' --user &&
  export PATH=$HOME/.local/bin:$PATH
script:
- |
  travis-cargo build &&
  travis-cargo test &&
  travis-cargo bench &&
  travis-cargo --only stable doc
addons:
  apt:
    packages:
    - libcurl4-openssl-dev
    - libelf-dev
    - libdw-dev
after_success:
- travis-cargo --only stable doc-upload
- travis-cargo coveralls --no-sudo
notifications:
  email:
    on_success: never
env:
  global:
  - TRAVIS_CARGO_NIGHTLY_FEATURE=dev
  - secure: # encrypted stuff

(This con­fig­u­ra­tion has a few options espe­cially for run­ning clippy: The suc­cess of the build using the nightly com­piler is optional (because the plu­gin inter­face might change) and the nightly com­piler is called with the dev fea­ture).

Travis builds your pro­ject on Linux by default and sup­ports Mac OS X. If you also want to test on Win­dows, you should have a look at AppVeyor (free for open source).

Auto­mat­i­cally Ren­der Doc­u­men­ta­tion

To enable the auto­matic doc­u­men­ta­tion uploads[4] sup­ported by travis-cargo, you need to add an envi­ron­ment vari­able called GH_TOKEN that con­tains an access token for your GitHub account (with lim­ited rights). You can cre­ate one here (I have one for each of my pro­jects). To encrypt the token, you can use Trav­is' CLI tool (installed with gem install travis) by run­ning this in your pro­jec­t's root direc­tory (replace 1234 with your token):

$ travis encrypt "GH_TOKEN=1234" --add env.global

When every­thing is set up cor­rectly, you should be able to view your pro­jec­t's doc­u­men­ta­tion at username.github.io/project, e.g. killercup.github.io/hsl-rs.

Homu

Using CI, you can ensure that the code in a pull request by itself works and is good to merge; and with GitHub's recent addi­tion of required sta­tus checks for branches you can ensure that all tests need to pass before you can merge a pull request. But you can­not be sure that the tests still pass after the code is merged into master!

The Rust pro­ject on GitHub uses an inte­gra­tion bot called bors to han­dle this: Instead of merg­ing pull requests them­selves, they tell bors to do it. It then takes one pull requests at a time (it has a queue), merges it into the cur­rent ver­sion on master, runs all tests, and—if they pass—pushes the new ver­sion to master. This means the merg­ing a lot of pull requests can take some time, but it ensures that your tests are always pass­ing on master.

The soft­ware that pow­ers the cur­rent iter­a­tion of bors is called homu and is also avail­able to be used in your pro­jects. Just add homu's Github user as a col­lab­o­ra­tor, reg­is­ter the pro­ject on homu.io, and merge pull requests by com­ment­ing “@homu r+”!

More Tricks

Want more? Okay, here are a few more tips:

Con­clu­sion

Thank you for read­ing this far! I hope you can take some of the tech­niques described here and apply them to your next (Rust) library.

There is some dis­cus­sion about this arti­cle on /r/​rust and Hack­erNews. I'm look­ing for­ward to read­ing your com­ment!


  1. It's a bit early to call them “best prac­tices”.

  2. rustfmt assumes machines can already parse your code.

  3. Actu­ally, there are 68 lints avail­able in clippy ver­sion 0.0.21.

  4. I.e. push­ing rust­doc's out­put to the pro­jec­t's gh-pages branch.