Abstract
When you're writing Rust code that targets multiple platforms, like both desktop and the web, not all of your code will work on all platforms. This article will show how you can write platform-specific code alongside your platform-independent code.
Multi-Platform Support
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.