cargo without project

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.