For a bit more than a year now, I've been interested in Rust, a programming language by Mozilla research that “runs blazingly fast, prevents nearly all segfaults, and guarantees thread safety.” It is as low-level as C or C++, has a nice type system (with generics and traits), a helpful compiler, and a great package manager called Cargo.
Since Rust 1.0 was released half a year ago (in May 2015), a lot of libraries (“crates”) have been published to Cargo's main public registry crates.io (including some of mine). Here are some good practices[1] that help make your library easy to find, use, and extend by others.
Keeping 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 discussing abstract styles here. You can find advice on how to structure your project's logic or which pattern to implement in the official book and Rust Design Patterns.
rustfmt
rustfmt was written to automatically reformat your Rust code to make it easily parseable by humans[2]. It works quite well and hasn't eaten any of my laundry 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 ongoing effort to define The One True Rust Style and rustfmt tries to follow that, you can find a list of style options here. Currently, most of my projects use these settings (save this as rustfmt.toml
in your project's root directory):
format_strings = false
reorder_imports = true
Activate More Lints
“Lints” are small compiler plugins that check your code during compilation for (mostly) stylistic issues. By default, rustc already has a few of them set to issue warnings, e.g. dead-code
(warns when there is unreachable code) or non-snake-case
(warns when certain items are not written in snake_case
).
There are some more lints built in, that are pretty useful, though! And you can also set them to deny
, which turns the harmless warnings into hard errors (more in the reference). I like to add these to my projects:
#![deny(missing_docs,
missing_debug_implementations, missing_copy_implementations,
trivial_casts, trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces, unused_qualifications)]
Especially the first one, missing_docs
, is really useful: It prevents any changes that add undocumented public interfaces from compiling. Denying unsafe_code
is quite nice as well, it makes your library look more trustworthy and therefore increases your chance of having plush bunnies mailed to you.
You can get a list of all available lints including descriptions 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 written about a hundred more[3]. Since clippy is a compiler plugin, you currently need the nightly compiler to use it. You can invoke it in various ways, e.g. using cargo-clippy, or by adding it as an optional dependency to your project.
I tend to do the latter 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 feature flag dev
is set. In the crate's main file, you can then conditionally include it like this:
#![cfg_attr(feature = "dev", allow(unstable_features))]
#![cfg_attr(feature = "dev", feature(plugin))]
#![cfg_attr(feature = "dev", plugin(clippy))]
Building your project with cargo build --features "dev"
now automatically includes clippy's lints. (By the way, you only need to allow unstable_features
if you have previously deny
ed it, of course.)
Please be aware that compiler plugins are not stable 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 project's documentation to find what more you can enable!
Tests
Rust's integrated support for testing is amazing: You can quickly write unit tests inline with your modules and cargo handles running integration tests (Rust files in the tests/
directory) automatically. Oh, and examples in documentation (or in examples/
) are tested as well.
There's not much more for me to say here. Just read the chapter in the official book!
Project Infrastructure
Aside from writing the code, there are a few things to think about when publishing your project. I'm assuming you'll want to put you code on GitHub, but that's not a requirement.
Cargo Metadata
The first and quickest thing to do to make your library easy to find is to fill out your Cargo.toml
file. There are a lot of metadata fields that will be used by crates.io. Here is an example 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 wildcard versions for your dependencies. crates.io will reject them as they are an escape hatch from the semantic versioning that Cargo assumes. You can use cargo-edit to quickly add the latest version of a crate as a dependency.
README.md
People looking at your repository's start page will most likely see the contents of your Readme.md
file. Make sure it answers the usual questions:
- What is this project about? What problems does it solve? How?
- How can I install/use this?
- Where is the documentation?
- What is the license of this?
The Readme
file is also a good place for small examples showing how to use your library – people love to copy-paste these small examples to get started. To ensure that the examples in your Readme.md
(and other documentation written in Markdown) compile, you can use skeptic. By adding a small hook to Cargo's build process (a build.rs
file), you can invoke skeptic to transform code snippets in Markdown files to regular tests. (See skeptic's documentation for more information.)
Other Meta Files
Don't forget to include a .gitignore
file that prevents git from tracking the target/
directory. For non-binary crates, you should also ignore the Cargo.lock
file.
Another file that I try to add to every project is .editorconfig
. I use these settings:
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
Continuous Integration
If your open source project is hosted on GitHub, you can get free continuous integration builds from Travis CI. This is super useful, as it can be configured to run your tests on each push in various environments (e.g. agains Rust stable, beta, and nightly).
Even better, using travis-cargo you can have Travis run your tests and benchmarks, generate your documentation (and push it to GitHub Pages), and calculate your code coverage (and push it to Coveralls).
The basic configuration 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 configuration has a few options especially for running clippy: The success of the build using the nightly compiler is optional (because the plugin interface might change) and the nightly compiler is called with the dev
feature).
Travis builds your project on Linux by default and supports Mac OS X. If you also want to test on Windows, you should have a look at AppVeyor (free for open source).
Automatically Render Documentation
To enable the automatic documentation uploads[4] supported by travis-cargo, you need to add an environment variable called GH_TOKEN
that contains an access token for your GitHub account (with limited rights). You can create one here (I have one for each of my projects). To encrypt the token, you can use Travis' CLI tool (installed with gem install travis
) by running this in your project's root directory (replace 1234
with your token):
$ travis encrypt "GH_TOKEN=1234" --add env.global
When everything is set up correctly, you should be able to view your project's documentation 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 addition of required status checks for branches you can ensure that all tests need to pass before you can merge a pull request. But you cannot be sure that the tests still pass after the code is merged into master
!
The Rust project on GitHub uses an integration bot called bors to handle this: Instead of merging pull requests themselves, they tell bors to do it. It then takes one pull requests at a time (it has a queue), merges it into the current version on master
, runs all tests, and—if they pass—pushes the new version to master
. This means the merging a lot of pull requests can take some time, but it ensures that your tests are always passing on master
.
The software that powers the current iteration of bors is called homu and is also available to be used in your projects. Just add homu's Github user as a collaborator, register the project on homu.io, and merge pull requests by commenting “@homu r+”!
More Tricks
Want more? Okay, here are a few more tips:
- Don't duplicate your binary's version number in your code. Use
env!("CARGO_PKG_VERSION")
to get the version you set inCargo.toml
at compile time. - Let the highfive bot automatically greet new contributors on GitHub.
- Write commit messages in a “conventional” style and use Clog to automatically generate change logs. GitCop is a bot that can automatically check pull requests for commit message styles (free for open source).
- Install more Cargo Subcommands.
Conclusion
Thank you for reading this far! I hope you can take some of the techniques described here and apply them to your next (Rust) library.
There is some discussion about this article on /r/rust and HackerNews. I'm looking forward to reading your comment!