Skip to content

Instantly share code, notes, and snippets.

@stevedonovan
Last active December 15, 2017 10:34
Show Gist options
  • Save stevedonovan/a2c96d7412b8fec6cc176cf3f3eb5d38 to your computer and use it in GitHub Desktop.
Save stevedonovan/a2c96d7412b8fec6cc176cf3f3eb5d38 to your computer and use it in GitHub Desktop.
Temporary staging area for article

Cargo Manages Crates

Cargo is an important feature of the Rust ecosystem, using 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' and is miles ahead from what is available for C++; it is like Maven, except TOML is easier to write and read than XML.

So suggesting that it is not ideal for some situations may come across as being perverse. I've seen people who admire Rust but don't like Cargo being downvoted into oblivion in online discussions. I started out using makefiles, because I wanted to understand the toolchain, and (once satisfied) could appreciate Cargo for what it delivers.

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

$ cargo new --bin 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). In any case, a new Cargo project will have to contact the mother-ship crates.io to resolve dependencies each time.

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 code points 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 i7 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 quotes for any Rust string within the expression.

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()'
...

It doesn't seem like such a great time saver, although it is more convenient. The 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.

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 experimentating with Rust easier. I use it to prototype bits of code, even the nucleus of new projects, which then become proper Cargo projects.

It can compile crates dynanically (that is, as DLL or .so files) but this is still experimental, and this is essentially just an optimization at the moment - see the documentation. I've found that using dynamically-compiled crates results

But the main idea is to ruthlessly 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment