The Three "R"s: Rust, REPL, Runner
January 21, 2021
REPLs and runners can be a quick and easy way to iterate and execute code. Let’s try different Rust REPLs & runners to see the differences between each tool.
Intro
tl;dr Jump to the code
When evaluating a new dependency I often find myself going through the same steps:
- Do I have a pre-existing applicable project in source control?
- Yes, create a branch
- No, create a new Cargo package
- Add the dependency to
Cargo.toml
- Change some relevant code to try something out about the library
- Build
- Run
- Do I have everything I need to know to make a decision about the library
- Yes, go do what I need to do
- No, go back to Step 3
These steps can grow tedious, especially when needing to create a new cargo package. The iterations can be shortened by more work up front looking at documentation, source, and examples. But sometimes you just want to experiment in a quick and efficient way. That is when REPLs and runners can prove to be helpful.
For the uninitiated, a REPL (Read, Evaluate, Print, Loop) is typically an interactive shell that takes user input, evaluates said input, and returns the result. Assuming I can access the required dependencies, I should be able to take the contents of the fn main()
above and enter each expressions in line-by-line into a REPL.
There isn’t an official REPL, but there is at least one active community REPL:
A Rust runner allows you to run (well, compile, link, and run) Rust code without the overhead (and benefits) of creating a Cargo package.
Similar to the REPL, there isn’t an official Rust runner, besides Cargo, but there are a few community options:
Now that we have a better idea of the landscape, let’s implement the same Rust example with these tools.
Our example will use serde_json
to print a serialized and deserialized Point
struct.
Cargo Package
To start, we will create a Cargo package as a baseline.
Create a new cargo package and move into the package directory:
$ cargo new cargo-serde-example$ cd cargo-serde-example
Next, lets add the Serde dependencies we’ll need to the bottom of the Cargo.toml
under [dependencies]
:
serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"
With the proper dependencies, change ./src/main.rs
to match the code below:
use serde::{Serialize, Deserialize};#[derive(Serialize, Deserialize, Debug)]struct Point {x: i32,y: i32,}fn main() {let point = Point { x: 1, y: 2 };let serialized = serde_json::to_string(&point).unwrap();println!("serialized = {}", serialized);let deserialized: Point = serde_json::from_str(&serialized).unwrap();println!("deserialized = {:?}", deserialized);}
Finally, we can run the package:
$ cargo run
You should see the following at the bottom of the output:
serialized = {"x":1,"y":2}deserialized = Point { x: 1, y: 2 }
Now with our baseline, we can try the REPLs and runners.
Evcxr REPL
Evcxr REPL is a REPL for Rust that uses the evcxr execution context. For our purposes, we don’t need to go into depth how evcxr
works, let’s just get things setup and run.
Setup
Before you install the REPL, you must download a local copy of Rust’s source code:
$ rustup component add rust-src
Now you can go ahead and install the binary:
$ cargo install evcxr_repl
Source: evcxr/README.md at master · google/evcxr · GitHub
Run
Enter the following lines in the REPL (without the >>
) to get the same output as the Cargo package :
$ evcxr>> :dep serde = { version = "1.0", features = ["derive"] }>> :dep serde_json = { version = "1.0" }>> use serde::{Serialize, Deserialize};>> #[derive(Serialize, Deserialize, Debug)]struct Point {x: i32,y: i32,}>> let point = Point { x: 1, y: 2 };>> let serialized = serde_json::to_string(&point).unwrap();>> println!("serialized = {}", serialized);>> let deserialized: Point = serde_json::from_str(&serialized).unwrap();>> println!("deserialized = {:?}", deserialized);
A few things to note about the REPL session:
:dep
lines are equivalent to the[dependencies]
section of theCargo.toml
in the Cargo package- The session does not end until we exit the REPL. We can continue to work with the previous lines until we exit or the REPL is reset.
It isn’t hard to imagine how this REPL session would be useful when exploring the functionality of an API or doing some one-off work. Remember to save you session (copy and paste) before closing out of the REPL, otherwise your work may be lost.
Unfortunately, if you want to run previously written code in the REPL, you have to enter the code in one statement at a time. Running code in different sessions is where runners come into play.
runner
runner compiles, links, and runs a Rust script (or snippet) without the boilerplate Cargo package. Let’s setup runner
and run our example to see how it works with a script.
Setup
$ cargo install runner
Run
First we need to add the Serde dependencies to runner
’s static cache (just trust me). Run the following command to add serde
and initialize the static cache:
$ runner add serde
Now we will edit the static cache manually:
$ runner edit
At the bottom of the file you will see:
serde="*"
Replace serde=“*”
with:
serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"
Save the file and now we will rebuild the static cache:
$ runner --build
The dependencies are now available for our runner
script that we are now going to create.
Copy the following contents to a new file named runner-serde-example.rs
:
extern crate serde_json;use serde::{Serialize, Deserialize};#[derive(Serialize, Deserialize, Debug)]struct Point {x: i32,y: i32,}let point = Point { x: 1, y: 2 };let serialized = serde_json::to_string(&point).unwrap();println!("serialized = {}", serialized);let deserialized: Point = serde_json::from_str(&serialized).unwrap();println!("deserialized = {:?}", deserialized);
Finally, run the script:
$ runner -s runner-serde-example.rsserialized = {"x":1,"y":2}deserialized = Point { x: 1, y: 2 }
The -s
in the command tells runner
to “evaluate expressions against crates compiled as static libraries.” This is necessary so we can use the Serde crates we added to the static cache earlier.
In addition to running scripts, runner
also supports running expressions, similar to running one statement in a REPL.
Here is an example using the serde_json
dependency we added earlier to deserialize an Object
from a string of JSON text:
$ runner -s -Xserde_json -e 'serde_json::from_str::<serde_json::Value>(r#"{"foo":"bar"}"#).unwrap()'Object({"foo": String("bar")})
Managing the dependencies with runner
can be somewhat cumbersome. Script dependencies are managed outside the script and only one version of crate can be added to the static cache at a time.
cargo-eval
is another runner that has a different solution to managing dependencies and similar features to runner
.
cargo-eval
cargo-eval is a Cargo subcommand that enables you to run Rust scripts (or expressions). Let’s setup cargo-eval
and run our example to see how it works with a script.
Setup
$ cargo install cargo-eval
Run
Create a new file called cargo-eval-serde-example.rs
and copy in the following script contents:
//! ```cargo//! [dependencies]//! serde = { version = "1.0", features = ["derive"] }//! serde_json = "1.0"use serde::{Serialize, Deserialize};#[derive(Serialize, Deserialize, Debug)]struct Point {x: i32,y: i32,}fn main() {let point = Point { x: 1, y: 2 };let serialized = serde_json::to_string(&point).unwrap();println!("serialized = {}", serialized);let deserialized: Point = serde_json::from_str(&serialized).unwrap();println!("deserialized = {:?}", deserialized);}
The first line of cargo-eval-serde-example.rs
denotes the start of a partial Cargo manifest. In this section you can configure dependencies and manifest information. The rest of the script should look familiar at this point. We have all we need for our example, so let’s run the script.
Run the script with:
$ cargo eval cargo-eval-serde-example...serialized = {"x":1,"y":2}deserialized = Point { x: 1, y: 2 }
In addition to running scripts, cargo-eval
includes other features, such as running benchmarks and tests.
Conclusion
We have now implemented the same example with a Cargo project, a REPL, and two runners. Hopefully you feel more comfortable with each tool and have some ideas of when you can use them to decrease your iteration speed and improve your workflow.
We just touched on the capabilities these different tools. You should check out each tool to see the full list of features and options.
For more examples implemented with these tools, check out GitHub - ozbe/rust-repls-and-runners: A collection of examples implemented with different Rust REPLs & Runners.
If you have any questions on what was covered or have any thoughts or experiences with these tools, please share them in the comments!
Updates
2020-02-13
A few more runners and REPLs:
- fanzeyi/cargo-play - A local Rust playground
- fornwall/rust-script - Run Rust files and expressions as scripts without any setup or compilation step.
- murarth/rusti - (Deprecated) REPL for the Rust programming language
- kurtlawrence/papyrus - (Deprecated) Rust REPL