Cargo is Cool, But…

Cargo is an important feature of the Rust ecosystem, providing a central repository of versioned packages and ensuring reproducible builds. It has learned important lessons from more ad-hoc solutions like Go’s ‘go get’. It is widely considered one of the great strengths of Rust.

So suggesting that it is not ideal for all situations may come across as being difficult.

The recommended go-to tool for testing out snippets is the Rust Playground, (which is in fact how I got sucked into Rust in the first place). It is particularly good for showing other people your illustrative little programs. But I have decent hardware and like using my editor of choice, so I prefer to run trivial programs locally.

When writing Rust programs, no matter how trivial, the advice to beginners is to create a Cargo project:

$ cargo new myprog
$ cd myprog
$ edit src/
$ cargo run

You do get a lot for free once something is a Cargo project - cargo run will rebuild if needed, if the source changes or the compiler changes. (This is useful in a language where a new stable version comes out every six weeks.) The Rust standard library is deliberately kept compact and much functionality is in the repository. If you have a cargo project, then adding a crate reference after “[dependencies]” in Cargo.toml will cause that dependency to be downloaded and built for your project. By default, it creates a Git repo.

There are some downsides. You will notice with non-trivial dependencies (like the regex crate) that a lot of code is downloaded and compiled. The source will be cached, but the compiled libraries are not. So the next dinky program you write that uses regex will require those libraries to be recompiled. This is pretty much the only way you can get the desired state of reproducibility (the problem is harder than you might think).

However, if you are willing to relax about reproducibility, those “build artifacts” can be made once and thereafter reused to build other programs. Small exploratory programs need not be built with the rigour necessary for large-scale systems.

Running ‘Snippets’

runner is mostly a clever wrapper around normal Cargo operations. To install, just say cargo install runner.

The word ‘snippet’ here means an abbreviated way to write Rust programs. The original inspiration was the form of documentation tests. Here is the example for the len method of the primitive str type:

let len = "foo".len();
assert_eq!(3, len);

let len = "ƒoo".len(); // fancy f!
assert_eq!(4, len);

cargo test will make such examples into proper little programs, and run them as pass-fail tests. It’s an elegant way to both test an API and ensure that all examples compile.

If I save this as, then runner will compile and run it directly:

$ runner

There’s no output, but if you don’t appreciate the difference between bytes and characters and insist that the last line should be assert_eq!(3, len) then the assertion will fail and the result will show a runtime panic.

We can communicate with the world in the time-honoured way:

$ cat
println!("hello world");
$ runner
hello world

This certainly involves less typing - runner is making up a main function for us. By default, it compiles the program dynamically - i.e. not linking in the Rust stdlib statically. The result is not portable and not something you can hand out to your friends, but it is faster. On my work machine, the default dynamic compile is 187ms versus 374ms for runner -s

Refugees from dynamic ‘scripting’ languages will find this refreshingly familiar - runner acts like an interpreter. There is no forced directory structure, just source. But it’s just using rustc under the hood in the most direct way possible. If it was a true interpreter, then I could provide you with a read-eval-loop (REPL) as with Python. (This would be very cool, but also very hard.) I don’t personally miss having a REPL that much, since running snippets is usually fast enough and you can write your code in a proper editor.

runner does more than wrap code in fn main - it puts the code in a function returning a boxed error result. So the question-mark operator works, and you can write real little programs without ugly unwrap calls.

let mut f = File::open("")?;
let mut buf = String::new();
f.read_to_string(&mut buf)?;

How is File available? Because the generated program has a prelude written at the top, which you can view (and edit) with runner --edit-prelude.

(If you are curious, the expanded source will be ~/.cargo/.runner/bin/

We can get even more concise with the -e or --expression flag:

$ runner -e '"¡Hola!".len()'

On a Unix command-prompt, I need to quote the whole expression in single quotes; for Windows (where quoting is mostly broken) you would use double-quotes for the expression, and single quoted strings will be massaged into double-quotes internally.

For the -i or --iterator flag, the expression is an iterator and runner will make up a little program to print out all the values of the iterator:

$ runner -i "(0..4).map(|n| 2*n)"

Keeping a Cache

runner can link in external crates. What it does is keep a static cache of crates managed by Cargo.

Say you are curious about the old-fashioned but useful crate time. You can add time to the cache with runner --add time - it will do the usual Cargo dance.

Write a little snippet like so, anywhere you like:

extern crate time;

println!("{:?}", time::now());

and compile-and-go like so:

$ runner -s
Tm { tm_sec: 16, tm_min: 39, tm_hour: 10, tm_mday: 15, tm_mon: 11,
tm_year: 117, tm_wday: 5, tm_yday: 348, tm_isdst: 0,
tm_utcoff: 7200, tm_nsec: 917431093 }

The -s (or --static) flag is important, since the crates in the cache will be linked in statically to make a program, not the usual dynamic default.

The documentation for time is also built locally, and you can open it in your browser with runner --doc time.

Even less typing - this is almost exactly the same program (-x or --extern for bringing in an external crate.)

$ runner -s -xtime -e 'time::now()'

This is certainly more convenient. But the big time-saving comes from the cache saving built libraries, both for debug and for release.

You can now write multiple little programs referencing time and not have to rebuild time each time as with Cargo projects.

(runner will accept proper programs if it detects fn main, but you may have to give it hints with --extern to resolve crates.)

Furthermore, after filling your cache with interesting and useful crates, you can take your laptop on the subway or on your favourite connection-challenged getaway, and still play with your programs. The built crates are in the cache, and so is the documentation - no constant need for internet. Consider also that many people out there simply don’t have the consistent always-on internet connection that modern tools now take for granted. This is not necessarily such a Third World problem either, considering that corporate firewalls and policies are also hostile to such tools.

Rust is otherwise well suited for off-line work, since you get the stdlib documentation (and several books!) installed for local browsing. runner furthermore makes the documentation of all cached crates available through runner --doc, complementing rustup doc --std.


runner makes experimenting with Rust easier. I use it to prototype bits of code, even the nucleus of new projects, which then become proper Cargo projects. This particularly matters when dealing with larger projects where a build can take many seconds or even minutes. Debugging the logic of a tricky dozen lines can then get seriously slow.

It can compile crates dynanically (that is, as DLL or .so files) but this is still experimental, and is essentially just an optimization at the moment - see the documentation.

But the main idea is to reduce the amount of boilerplate and bureaucracy needed to test out small bits of example code. I would certainly have found this very useful when learning Rust, and I hope runner will be useful to others as well, whether beginners or seasoned pros.