Multi-platform Rust and Emscripten-Specific Functions
Platform-specific code in an otherwise platform-independent Rust application

10 October 2017

Programs that are written in the Rust programming language can be compiled to run on many different computers. For example, a Rust program may be written on Linux, and compiled to run on Windows, without changes needing to be made to the Rust code itself. This usually just works without you needing to think about it as a programmer, but sometimes you need to make a distinction.

For example, when it compiles on Windows, you might need to process files in a subtly different way to take into account that file names on Windows are case-insensitive. FileName.TxT and filename.txt would be considered the same file on Windows, and different files on Linux.

For these situations, Rust provides a conditional compilation attribute, #[cfg(...)] and a conditional compilation macro cfg!(...).

Taking Rusty Microphone to the Web

Rusty Microphone is a Rust desktop application that I’ve been working on. I’ve recently decided that it would be a good idea to use Rust’s support for WebAssembly through Emscripten to make a version of Rusty Microphone that I can embed in my website. Apologies for the piles of jargon, but what it boils down to is that I’ll be writing a library in Rust and calling it from JavaScript.

Rusty Microphone currently uses GTK to create a window on the screen and PortAudio to manage audio devices. In other words, it’s a Linux desktop application. I want to replace these with HTML and the Web Audio API respectively on the web, without changing my desktop build. This is where I’m using conditional compilation in my own project.

The Emscripten Build, and the Not Emscripten Build

Making this split meant going through the project and stubbing out the parts that don’t make sense in a web application.

For functions that should only be included in a web build, I annotate the function with #[cfg(target_os="emscripten")]. The target OS of ‘emscripten’ covers both WebAssembly and an older option for compiling code to run in a web browser, Asm.js. Other valid target OS options would be things like “windows” and “linux”, but since I’m logically splitting on “emscripten” and “not emscripten”, my functions that don’t work with a web build get this attribute: #[cfg(not(target_os="emscripten"))].

#[cfg(not(target_os = "emscripten"))]
fn main() {
    // This main will be run on Windows, Mac and Linux
    use rusty_microphone::*;

    let gui_result = gui::start_gui();
    if gui_result.is_err() {
        println!("Failed to initialize");
        return;
    }
}

#[cfg(target_os = "emscripten")]
fn main() {
    // This main will be run on WebAssembly or Asm.js builds
    println!("Hello Emscripten");
}

If the functions have the same name and take the same arguments, as they do here with the two mains, you can actually create a situation where platform independent code can call platform dependent code. If you have an implementation for all platforms, it would mean that you have a platform independent program, even though some parts of it have platform specific behaviour.

You can also do this on a module level. For example, my lib.rs looks like this now:

// transforms is the core logic of my application that I want
// everywhere.
pub mod transforms;

// gui (depending on GTK and Cairo) will only be for desktop
// builds. The WebAssembly build will have HTML for this.
#[cfg(not(target_os = "emscripten"))]
extern crate gtk;
#[cfg(not(target_os = "emscripten"))]
extern crate cairo;
#[cfg(not(target_os = "emscripten"))]
pub mod gui;

// audio (depending on Portaudio) will only be used for desktop
// builds. The WebAssembly build will use the Web Audio API
#[cfg(not(target_os = "emscripten"))]
extern crate portaudio;
#[cfg(not(target_os = "emscripten"))]
pub mod audio;

Platform-Specific Dependencies in Cargo.toml

Different platforms may need to specify different dependencies. You can do this by specifying target-specific dependencies, with a similar cfg string, in your Cargo.toml. My dependencies now look like this:

[dependencies]
bencher = "0.1.2"

[target.'cfg(not(target_os = "emscripten"))'.dependencies]
portaudio = "0.7.0"
gtk = "0.1.1"
cairo-rs = "0.1.1"

What Other Conditions can I Use?

The full list of cfg attributes is available in the Rust documentation. It has things like the target architecture (32 bit, 64 bit, ARM…), the target operating system (Windows, Mac, Linux, Android, iOS…), whether or not this is a unit test build, and whether or not this is a debug build.

I’d suggest taking a read through the docs to know what the options are. I’m not sure when else these attributes will come up in my projects, but I’m keeping them in the back of my mind just in case.