Leopold Luley

Introducing cargo-futhark

This article introduces the cargo-futhark library (GitHub, crates.io) and elaborates a bit on its implementation. cargo-futhark is both a CLI-tool and a build-tool to make the integration of Futhark into Rust trivial. This way, you can easily use Futhark for intensive computations while using Rust for logic and IO tasks.

Why using Futhark with Rust is useful

Rust is by itself already a fast language. Why would you want to use another language which goes fast?

In my opinion, there are two advantages Futhark has over Rust.

  1. Its syntax makes it effortless to express complex computations and makes working with n-dimensional-vectors much easier than Rust.
  2. Futhark can run on GPUs. It not only compiles to CPU code, but also to OpenCL, CUDA or ISPC.

These are also the reasons, why I chose Futhark when I implemented coco-accelerated. The ability to run code on both the CPU and the GPU is incredible, and the performance was great.

What cargo-futhark offers

As the name suggests, cargo-futhark provides a sub-command for Cargo. But at the same time, it can also be used as a build dependency.

The cargo futhark CLI

You can install it like this:

cargo install cargo-futhark

And then use it to generate a new Futhark module in your library / app like this:

cargo futhark new my_futhark_library

This will generate a Cargo project, but instead of .rs files, you can put .fut files into the src directory. Neat, isn’t it? The generated project looks like this:

- my_futhark_library
    - src
        - lib.fut
        - lib.rs
    - build.rs
    - Cargo.toml

Let’s take a look at the generated code and the build-tool next.

The cargo-futhark build-tool

First, take a look at the Cargo.toml file:

[package]
name = "my_futhark_library"
version = "0.1.0"
edition = "2021"

[features]
default = ["c"]
c = []
multicore = []
opencl = []
cuda = []
ispc = []

[build-dependencies]
cargo-futhark = "0.1.0"

As you can see, it is a normal Cargo manifest file. There are two interesting things, though:

The latter allows you to choose the included targets without having to modify the build.rs file.

Now, let’s see what the build.rs file looks like:

use cargo_futhark::{Generator, Result, Target};

fn main() -> Result<()> {
	Generator::new("src/lib.fut")
		.with_target_if(Target::C, cfg!(feature = "c"))
		.with_target_if(Target::MultiCore, cfg!(feature = "multicore"))
		.with_target_if(Target::OpenCL, cfg!(feature = "opencl"))
		.with_target_if(Target::Cuda, cfg!(feature = "cuda"))
		.with_target_if(Target::ISPC, cfg!(feature = "ispc"))
		.run()
}

Very straightforward, I’d say! It simply creates a Generator and adds each target if the feature flag is enabled. If you never want to support a certain target, you can simply remove the with_target_if clause and the respective feature from the manifest. If you always want to include a certain target, with_target can be used instead, and the feature can be removed as well.

In the end it calls Generator::run, which does all the magic, such as:

Implementation details

When writing cargo-futhark, I had to make some design decisions which I thought might be interesting.

Compiling multiple targets in one binary

The most exciting feature is probably the ability to compile multiple Futhark targets into a single library. I had two options to go about this.

The first option is creating one crate per target, generating the target-specific code, and then generating another crate combining the others. When building all five targets, this would result in 6 crates in total. Because this would require generating many crates, I tried a different approach, which I ended up using for cargo-futhark. The reason, why multiple crates were necessary in the first place, was that multiple Futhark targets have name collisions.

So how can this be solved? By making all the symbols unique! So, after Futhark generates the .c file, cargo-futhark renames all symbols, replacing the futhark_ prefix with futhark_{target}, thus making all symbols unique. This allows a single crate to include all the different targets.

Code generation using quote

For code generation, I chose to use quote. Turns out, that quote can be used for code generation in general, and not just for proc-macros. When the goal is generating Rust code, quote felt like a great solution. Compared to normal templating, it had the advantage of solid syntax highlighting, Rust-Analyzer integration, and the ability to do conditional and repetitive generation using Rust as well. The only real annoyance was the need to create identifiers using format_ident!, but that was fine too, once I got accustomed to it.

Integrating a clap CLI into Cargo

As mentioned in the beginning, cargo-futhark provides both a build tool and a cli. To integrate clap with Cargo properly, some tricks are necessary. This is also explained in the Clap Cargo derive example, but I found that it lacked a bit of explanation. This is what the clap code looks like:

#[derive(Parser)]
#[command(name = "cargo")]
#[command(bin_name = "cargo")]
#[command(author = "Leopold Luley")]
#[command(version = "0.1")]
#[command(about = "Use `cargo futhark` instead")]
enum Cli {
    #[command(about = "Manage Cargo-Futhark projects")]
    Futhark {
        #[command(subcommand)]
        command: Commands,
    },
}

#[derive(Subcommand)]
enum Commands {
    #[command(about = "Create a new Cargo-Futhark project")]
    New { name: String },
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    match &cli {
        Cli::Futhark {
            command: Commands::New { name },
        } => new_project(name),
    }
}

fn new_project(name: &str) -> Result<()> {
	/// the actual implementation...
}

Let’s break this apart. First, we have the Cli struct:

#[derive(Parser)]
#[command(name = "cargo")]
#[command(bin_name = "cargo")]
#[command(author = "Leopold Luley")]
#[command(version = "0.1")]
#[command(about = "Use `cargo futhark` instead")]
enum Cli {
    #[command(about = "Manage Cargo-Futhark projects")]
    Futhark {
        #[command(subcommand)]
        command: Commands,
    },
}

This is the additional indirection, which is required when integrating with Cargo. The reason, why this is necessary, is that when the user runs cargo futhark [params], Cargo will call the cargo-futhark binary as cargo-futhark futhark [params]. The first parameter is as such completely uninteresting for the cargo-futhark CLI, and this little indirection allows us to skip it.

Next, we have the Command struct, which is the actual CLI definition.

Lastly, in main, we have to skip over the futhark parameter once more:

fn main() -> Result<()> {
    let cli = Cli::parse();

    match &cli {
        Cli::Futhark {
            command: Commands::New { name },
        } => new_project(name),
    }
}

With all this in place, calling cargo futhark will behave just as one would expect.

There have been several efforts to bridge the gap between Futhark and Rust. The genfut crate can also generate Rust bindings, and the futhark-bindgen crate can generate Futhark bindings for both Rust and OCaml. However, neither of the two can compile multiple targets into a single library. And unlike cargo-futhark, they do not provide a crate template to separate Futhark bindings conveniently from application code.