DEV Community

Cover image for Rust Cross-Platform Development: Build Once, Deploy Everywhere
Aarav Joshi
Aarav Joshi

Posted on

Rust Cross-Platform Development: Build Once, Deploy Everywhere

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Cross-platform development represents one of Rust's most powerful capabilities. I've worked extensively with Rust across multiple platforms, and I'm consistently impressed by how effectively it handles the complexities of creating truly portable applications.

Rust provides a unique combination of performance, safety, and portability that makes it exceptional for cross-platform development. The language was designed from the ground up with platform independence in mind, while still maintaining the low-level control that system programmers require.

The standard library serves as the foundation for Rust's cross-platform prowess. It offers abstractions that work consistently across operating systems, handling differences in file paths, environment variables, and system calls. These abstractions don't compromise on performance - the code compiles down to efficient native instructions for each target platform.

When I first started working with Rust, I was particularly impressed by how Cargo manages cross-compilation. The toolchain makes it remarkably straightforward to build for multiple targets:

# Build for Windows from Linux or macOS
cargo build --target x86_64-pc-windows-gnu

# Build for macOS from Windows or Linux
cargo build --target x86_64-apple-darwin

# Build for Linux from Windows or macOS
cargo build --target x86_64-unknown-linux-gnu
Enter fullscreen mode Exit fullscreen mode

This capability transforms what was once a complex process into a simple command. To enable these targets, you can use rustup:

rustup target add x86_64-pc-windows-gnu
rustup target add x86_64-apple-darwin
rustup target add x86_64-unknown-linux-gnu
Enter fullscreen mode Exit fullscreen mode

Conditional compilation is another core feature that supports cross-platform development. The cfg attribute and macro allow for platform-specific code sections without complex preprocessing:

#[cfg(target_os = "windows")]
fn get_home_dir() -> String {
    std::env::var("USERPROFILE").unwrap_or_default()
}

#[cfg(not(target_os = "windows"))]
fn get_home_dir() -> String {
    std::env::var("HOME").unwrap_or_default()
}
Enter fullscreen mode Exit fullscreen mode

This approach keeps platform-specific code clean and maintainable while preserving a shared codebase. The compiler only includes the relevant code for each target, eliminating runtime checks and ensuring optimal performance.

I've found Rust's platform detection capabilities particularly useful. Beyond the basic operating system detection, Rust provides fine-grained control with attributes like:

#[cfg(target_arch = "x86_64")]
// Code for x86_64 architecture

#[cfg(target_pointer_width = "64")]
// Code for 64-bit platforms

#[cfg(unix)]
// Code for Unix-like systems

#[cfg(target_env = "msvc")]
// Code specific to Microsoft Visual C environment
Enter fullscreen mode Exit fullscreen mode

These can be combined with boolean operators for complex conditions:

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
// Code specific to 64-bit Linux
Enter fullscreen mode Exit fullscreen mode

File system operations demonstrate Rust's cross-platform design. The standard library provides std::path::Path and PathBuf types that handle platform-specific path formats:

use std::path::{Path, PathBuf};

fn get_config_path(filename: &str) -> PathBuf {
    let mut path = if cfg!(windows) {
        PathBuf::from(std::env::var("APPDATA").unwrap())
    } else if cfg!(target_os = "macos") {
        let mut path = PathBuf::from(std::env::var("HOME").unwrap());
        path.push("Library/Application Support");
        path
    } else {
        let mut path = PathBuf::from(std::env::var("HOME").unwrap());
        path.push(".config");
        path
    };

    path.push(filename);
    path
}
Enter fullscreen mode Exit fullscreen mode

The ecosystem extends Rust's cross-platform capabilities with specialized crates. I regularly use these in my projects:

The dirs crate simplifies access to standard directories across platforms:

use dirs::{home_dir, config_dir, cache_dir};

fn setup_app_dirs(app_name: &str) -> std::io::Result<()> {
    let config = config_dir().unwrap().join(app_name);
    let cache = cache_dir().unwrap().join(app_name);

    std::fs::create_dir_all(&config)?;
    std::fs::create_dir_all(&cache)?;

    println!("Config directory: {}", config.display());
    println!("Cache directory: {}", cache.display());

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

For cross-platform GUI development, several frameworks offer consistent APIs:

use iced::{button, Button, Column, Element, Sandbox, Settings, Text};

struct Counter {
    value: i32,
    increment_button: button::State,
    decrement_button: button::State,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    IncrementPressed,
    DecrementPressed,
}

impl Sandbox for Counter {
    type Message = Message;

    fn new() -> Self {
        Counter {
            value: 0,
            increment_button: button::State::new(),
            decrement_button: button::State::new(),
        }
    }

    fn title(&self) -> String {
        String::from("Counter - Cross-platform App")
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::IncrementPressed => {
                self.value += 1;
            }
            Message::DecrementPressed => {
                self.value -= 1;
            }
        }
    }

    fn view(&mut self) -> Element<Message> {
        Column::new()
            .push(
                Button::new(&mut self.increment_button, Text::new("+"))
                    .on_press(Message::IncrementPressed),
            )
            .push(Text::new(self.value.to_string()))
            .push(
                Button::new(&mut self.decrement_button, Text::new("-"))
                    .on_press(Message::DecrementPressed),
            )
            .into()
    }
}

fn main() -> iced::Result {
    Counter::run(Settings::default())
}
Enter fullscreen mode Exit fullscreen mode

Thread management can vary between platforms, but Rust's standard library provides consistent abstractions:

use std::thread;
use std::sync::{Arc, Mutex};
use std::time::Duration;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
            println!("Thread: count = {}", *num);
            thread::sleep(Duration::from_millis(10));
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Final count: {}", *counter.lock().unwrap());
}
Enter fullscreen mode Exit fullscreen mode

Network programming in Rust also maintains consistency across platforms:

use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

fn handle_client(mut stream: TcpStream) {
    let mut buffer = [0; 512];

    match stream.read(&mut buffer) {
        Ok(size) => {
            let message = String::from_utf8_lossy(&buffer[0..size]);
            println!("Received: {}", message);

            let response = format!("Echo: {}", message);
            stream.write(response.as_bytes()).unwrap();
        }
        Err(e) => {
            println!("Error reading from connection: {}", e);
        }
    }
}

fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878")?;
    println!("Server listening on port 7878");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                thread::spawn(|| {
                    handle_client(stream);
                });
            }
            Err(e) => {
                println!("Connection failed: {}", e);
            }
        }
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

One of the most exciting aspects of Rust's cross-platform capabilities is WebAssembly (Wasm) support. This allows Rust code to run in web browsers with near-native performance:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    pub fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}
Enter fullscreen mode Exit fullscreen mode

To compile for Wasm, the process is straightforward:

wasm-pack build --target web
Enter fullscreen mode Exit fullscreen mode

From my experience, building cross-platform applications with Rust requires attention to certain best practices:

  1. Use the standard library's platform-agnostic APIs whenever possible.
  2. Isolate platform-specific code into separate modules.
  3. Leverage Cargo features to provide optional platform-specific functionality.
  4. Implement comprehensive testing across all target platforms.
  5. Use CI/CD pipelines to automatically verify cross-platform compatibility.

Here's an example of using Cargo features for platform-specific functionality:

# Cargo.toml
[features]
default = []
windows_notification = ["winrt"]
linux_notification = ["dbus"]
macos_notification = ["macos-notifications"]

[dependencies]
winrt = { version = "0.7.0", optional = true }
dbus = { version = "0.9.5", optional = true }
macos-notifications = { version = "0.5.0", optional = true }
Enter fullscreen mode Exit fullscreen mode
// notifications.rs
pub fn send_notification(title: &str, message: &str) -> Result<(), Box<dyn std::error::Error>> {
    #[cfg(feature = "windows_notification")]
    {
        use winrt::windows::ui::notifications::{ToastNotification, ToastNotificationManager};
        use winrt::windows::data::xml::dom::XmlDocument;

        // Windows-specific notification code
        return Ok(());
    }

    #[cfg(feature = "linux_notification")]
    {
        use dbus::{Connection, Message, BusType};

        // Linux-specific notification code
        return Ok(());
    }

    #[cfg(feature = "macos_notification")]
    {
        use macos_notifications::{Notification, NotificationCenter};

        // macOS-specific notification code
        return Ok(());
    }

    #[cfg(not(any(
        feature = "windows_notification",
        feature = "linux_notification",
        feature = "macos_notification"
    )))]
    {
        // Fallback implementation
        println!("{}: {}", title, message);
        return Ok(());
    }
}
Enter fullscreen mode Exit fullscreen mode

For embedded development, Rust excels at cross-compilation for various microcontrollers:

#![no_std]
#![no_main]

use panic_halt as _;
use cortex_m_rt::entry;
use stm32f1xx_hal::{pac, prelude::*, timer::Timer};

#[entry]
fn main() -> ! {
    let cp = cortex_m::Peripherals::take().unwrap();
    let dp = pac::Peripherals::take().unwrap();

    let mut flash = dp.FLASH.constrain();
    let mut rcc = dp.RCC.constrain();

    let clocks = rcc.cfgr.freeze(&mut flash.acr);

    let mut gpioc = dp.GPIOC.split(&mut rcc.apb2);
    let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);

    let mut timer = Timer::syst(cp.SYST, &clocks)
        .start_count_down(1.hz());

    loop {
        led.toggle();
        nb::block!(timer.wait()).unwrap();
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing across platforms is facilitated by Rust's robust testing framework:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_path_handling() {
        let path = get_platform_path("test.txt");
        assert!(path.is_absolute());

        #[cfg(windows)]
        {
            assert!(path.to_string_lossy().contains('\\'));
        }

        #[cfg(not(windows))]
        {
            assert!(path.to_string_lossy().contains('/'));
        }
    }

    #[test]
    fn test_file_operations() {
        let temp_dir = tempfile::tempdir().unwrap();
        let test_file = temp_dir.path().join("test.txt");

        std::fs::write(&test_file, "test content").unwrap();
        let content = std::fs::read_to_string(&test_file).unwrap();

        assert_eq!(content, "test content");
    }
}
Enter fullscreen mode Exit fullscreen mode

Working with environment variables across platforms:

use std::env;

fn get_environment_config() -> std::collections::HashMap<String, String> {
    let mut config = std::collections::HashMap::new();

    // Get common environment variables with platform-specific fallbacks
    let home = if cfg!(windows) {
        env::var("USERPROFILE").or_else(|_| env::var("HOMEDRIVE").and_then(|d| {
            env::var("HOMEPATH").map(|p| format!("{}{}", d, p))
        }))
    } else {
        env::var("HOME")
    };

    if let Ok(home_path) = home {
        config.insert("HOME".to_string(), home_path);
    }

    // Get temp directory
    if let Ok(temp_dir) = if cfg!(windows) {
        env::var("TEMP")
    } else {
        env::var("TMPDIR").or_else(|_| Ok("/tmp".to_string()))
    } {
        config.insert("TEMP".to_string(), temp_dir);
    }

    config
}
Enter fullscreen mode Exit fullscreen mode

Rust's performance on all platforms is exceptional, but sometimes platform-specific optimizations are necessary:

#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;

#[cfg(target_arch = "aarch64")]
use std::arch::aarch64::*;

// A function that uses SIMD instructions when available
pub fn sum_array(array: &[i32]) -> i32 {
    #[cfg(target_arch = "x86_64")]
    {
        if is_x86_feature_detected!("avx2") {
            return sum_array_avx2(array);
        }
    }

    #[cfg(target_arch = "aarch64")]
    {
        if std::arch::is_aarch64_feature_detected!("neon") {
            return sum_array_neon(array);
        }
    }

    // Fallback implementation for all platforms
    array.iter().sum()
}

#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn sum_array_avx2(array: &[i32]) -> i32 {
    // AVX2 SIMD implementation
    // ...
    array.iter().sum()
}

#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn sum_array_neon(array: &[i32]) -> i32 {
    // NEON SIMD implementation
    // ...
    array.iter().sum()
}
Enter fullscreen mode Exit fullscreen mode

Cross-platform serialization and persistence is simplified with serde and related crates:

use serde::{Serialize, Deserialize};
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;

#[derive(Serialize, Deserialize)]
struct Configuration {
    app_name: String,
    version: String,
    max_connections: u32,
    enable_logging: bool,
    paths: ConfigPaths,
}

#[derive(Serialize, Deserialize)]
struct ConfigPaths {
    data_dir: String,
    cache_dir: String,
    log_file: String,
}

impl Configuration {
    fn save_to_file(&self, path: &Path) -> std::io::Result<()> {
        let json = serde_json::to_string_pretty(self)?;
        let mut file = File::create(path)?;
        file.write_all(json.as_bytes())?;
        Ok(())
    }

    fn load_from_file(path: &Path) -> std::io::Result<Self> {
        let mut file = File::open(path)?;
        let mut contents = String::new();
        file.read_to_string(&mut contents)?;
        let config: Configuration = serde_json::from_str(&contents)?;
        Ok(config)
    }

    fn get_platform_specific_paths() -> ConfigPaths {
        let base_dir = if cfg!(windows) {
            std::env::var("APPDATA").unwrap_or_else(|_| ".".to_string())
        } else if cfg!(target_os = "macos") {
            format!("{}/Library/Application Support", 
                    std::env::var("HOME").unwrap_or_else(|_| ".".to_string()))
        } else {
            format!("{}/.config", 
                    std::env::var("HOME").unwrap_or_else(|_| ".".to_string()))
        };

        ConfigPaths {
            data_dir: format!("{}/app/data", base_dir),
            cache_dir: format!("{}/app/cache", base_dir),
            log_file: format!("{}/app/logs/app.log", base_dir),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In my years of using Rust, I've found that the cross-platform experience is remarkably smooth compared to other languages. The combination of compile-time platform detection, powerful abstractions without runtime overhead, and a supportive ecosystem makes Rust an excellent choice for portable application development.

The ability to target everything from embedded devices to web browsers with the same core codebase is particularly valuable for organizations looking to maintain a consistent experience across multiple platforms. For developers seeking both the control of a systems language and the portability of higher-level languages, Rust offers a compelling solution that doesn't force compromises on either front.


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)