Running Little Rust Snippets with Runner
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/main.rs
$ 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 crates.io
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 len.rs
, then runner will compile and run it directly:
$ runner len.rs
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 print.rs
println!("hello world");
$ runner print.rs
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 print.rs
.
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.
// file.rs
let mut f = File::open("file.rs")?;
let mut buf = String::new();
f.read_to_string(&mut buf)?;
println!("{}",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/file.rs
)
We can get even more concise with the -e
or --expression
flag:
$ runner -e '"¡Hola!".len()'
7
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)"
0
2
4
6
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:
// time.rs
extern crate time;
println!("{:?}", time::now());
and compile-and-go like so:
$ runner -s time.rs
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
.
Conclusion
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.