Trying out ideas quickly and iterating on the details have become central to my style of working ever since I started using Python. Exactly this kind of interactivity I miss, when I use Rust. In the absence of a proper repl, I found small scripts to be the next best thing. To support this setup, I wrote a crate called cargo-wop that I found quite helpful and would like to showcase in this blog post in the hope that it may also be helpful to others.
The idea of using Rust scripts for experiments is of course not new. There is
the Rust playground, you can use from the comfort of your browser,
or options like cargo-play and cargo-script.
However, all existing options lacked flexibility and fell short for my
WebAssembly experiments. Hence, I created my own spin on this idea, cargo wop
. Here, "wop" stands for "without project".
The basic idea is that cargo wop
aims to support for single files all
reasonable tasks cargo supports for proper projects. The list includes running
these files as scripts, but also building shared libraries, running included
tests or even installing the binaries. In this post I would like to explain how
cargo-wop
works and to showcase what you can do with it by
building a simple WebAssembly module and host application.
To install cargo-wop
, simply run cargo install cargo-wop
. After
installation, you can use the wop
subcommand. For example, given a file
example.rs
with the content
fn main() {
println!("Hello world");
}
you can run it as a script via
cargo wop example.rs
Behind the scenes, cargo-wop
will create a project in ~/.cargo/wop-cache
with a full manifest file (Cargo.toml
), build the project, and run the binary.
To write the WebAssembly host, let's adapt the the official wasmer
example. The dependencies can directly be included in the file
in a comment block at the top of the file under the [dependencies]
section.
//! ```cargo
//! [dependencies]
//! anyhow = "1"
//! wasmer = "1"
//! ```
use anyhow::{anyhow, Result};
use wasmer::{Store, Module, Instance, Value, imports};
fn main() -> Result<()> {
let module = std::env::args_os().nth(1)
.ok_or_else(|| anyhow!("Expects a module as first argument"))?;
let module = std::fs::read(module)?;
let store = Store::default();
let module = Module::new(&store, &module)?;
let import_object = imports! {};
let instance = Instance::new(&module, &import_object)?;
let func = instance.exports.get_function("add")?;
let result = func.call(&[Value::I32(21), Value::I32(21)])?;
assert_eq!(result[0], Value::I32(42));
Ok(())
}
The dependencies can also include local projects by including them as name = {path ".." }
in the dependencies section. This setup allows you to keep most of
your code inside proper rust packages, but still use them in small tests.
In addition to executable scripts, cargo-wop
also supports building shared
libraries. All that is required is to set the crate-type
in the [lib]
section of the manifest. For our example, we create a module that exposes an
add
function.
//! ```cargo
//! [lib]
//! crate-type = ["cdylib"]
//! ```
#[no_mangle]
extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
This file can be compiled to a WASM module and executed with our wrapper as in
# build the WASM module
cargo wop build add.rs --target wasm32-unknown-unknown
# execute the module
cargo wop wrapper.rs add.wasm
In addition to run and build, cargo-wop
supports many of the cargo commands
useful during development. For example, it can format the script (fmt
), lint
it (clippy
), and test it (test
). In case you are interested, you can find
the details on the project page. And as always, feel free to reach
out to me on twitter @c_prohm with feedback or comments.