Wonderfull Rust

Cross-Building

What Is Cross-Compilation

Cross-compilation refers to generating binaries for a different platform on one platform. For example, building Linux binaries on macOS, or generating ARM binaries on an x86_64 machine.

In the C/C++ world, cross-compilation was nearly a nightmare. You need to obtain a cross-compiler for the target platform, configure the sysroot, specify the linker, rebuild dependent libraries for the target architecture. If a library uses autotools, you have to get the --host and --build flags right. With CMake, you need to write toolchain files.

In Rust, cross-compilation is surprisingly easy.

Target Triples

Rust (or rather, LLVM) represents target platforms using target triples. Despite the name "triple" (three elements), they can actually contain more than three components:

<arch>-<vendor>-<os>-<abi>

Examples:

Target Triple Meaning
x86_64-unknown-linux-gnu x86_64 Linux (glibc)
aarch64-unknown-linux-gnu ARM64 Linux (glibc)
x86_64-apple-darwin x86_64 macOS
aarch64-apple-darwin ARM64 macOS (Apple Silicon)
x86_64-pc-windows-msvc x86_64 Windows (MSVC)
wasm32-unknown-unknown WebAssembly
thumbv7em-none-eabihf ARM Cortex-M (bare metal)

Cross-Compilation in Rust

The basic procedure is just 2 steps:

# 1. Add the target's standard library
rustup target add aarch64-unknown-linux-gnu

# 2. Build specifying the target
cargo build --target aarch64-unknown-linux-gnu

That's it. Since Rust's compiler uses LLVM as its backend, it can generate code for every target architecture LLVM supports. Pre-compiled standard libraries are distributed through rustup, so there's no need to rebuild the standard library for the target.

However, if you're using crates that depend on C libraries, you'll need a separate C cross-compiler and linker for the target. For projects composed entirely of pure Rust code, the 2 steps above are sufficient.

Linker Configuration

When a target-specific linker is needed, add the configuration to .cargo/config.toml:

[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

WebAssembly (Wasm)

One of the most compelling use cases for Rust's cross-compilation is WebAssembly.

What Is WebAssembly

WebAssembly (Wasm) is a binary format that runs in browsers. It was designed to enable code written in languages other than JavaScript to run in the browser. Rust is one of the languages with the most mature support for WebAssembly as a compilation target.

wasm32-unknown-unknown

Compiling from Rust to WebAssembly is as simple as specifying wasm32-unknown-unknown as the target triple:

rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release

This generates a .wasm file. However, using a raw .wasm file directly in a browser is cumbersome. Enter wasm-pack.

wasm-pack

wasm-pack is a tool that compiles Rust code to WebAssembly and outputs it as a package usable from JavaScript/TypeScript:

# Install
cargo install wasm-pack

# Build (output as an npm package)
wasm-pack build --target web

wasm-pack automatically handles:

  1. Compiling Rust code to .wasm
  2. Generating JavaScript bindings via wasm-bindgen
  3. Generating TypeScript type definition files (.d.ts)
  4. Generating package.json

The generated package can be published to npm or imported and used locally.

wasm-bindgen

wasm-bindgen is a tool/library that generates the interface between Rust and JavaScript. The #[wasm_bindgen] attribute exposes Rust functions and types to JavaScript:

use wasm_bindgen::prelude::*;

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

#[wasm_bindgen]
pub struct Counter {
    value: i32,
}

#[wasm_bindgen]
impl Counter {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Counter {
        Counter { value: 0 }
    }

    pub fn increment(&mut self) {
        self.value += 1;
    }

    pub fn get(&self) -> i32 {
        self.value
    }
}

From the JavaScript side, these can be used like regular functions and classes:

import init, { greet, Counter } from './pkg/my_wasm_lib.js';

await init();

console.log(greet("World"));  // "Hello, World!"

const counter = new Counter();
counter.increment();
console.log(counter.get());  // 1

Maintaining Rust's type safety while seamlessly connecting to the JavaScript world.

WASI (WebAssembly System Interface)

WebAssembly isn't just for browsers. WASI is a standardized system interface for running WebAssembly outside the browser (server-side, CLI tools, edge computing, etc.).

rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1

Wasm binaries built with the WASI target can be executed with runtimes like Wasmtime or Wasmer:

wasmtime target/wasm32-wasip1/debug/my_app.wasm

The combination of Rust + WASI is making the vision of "write once, run anywhere" a reality.

napi-rs: Node.js Native Addons

Another important cross-building use case is napi-rs.

What Is napi-rs

napi-rs is a framework for writing Node.js native addons (N-API modules) in Rust. Traditionally, Node.js native addons were written in C/C++ and built with node-gyp. napi-rs makes it possible to replace this with Rust.

Why napi-rs

Problems with C/C++ + node-gyp for Node.js native addon development:

  • Fragile builds: node-gyp has many external dependencies like Python 2/3 and Visual Studio Build Tools, causing frequent environment setup troubles
  • Not memory-safe: C/C++ doesn't guarantee memory safety, so native addon bugs can crash the entire process
  • Difficult cross-compilation: Build environments need to be prepared for each platform

napi-rs solves these problems:

use napi_derive::napi;

#[napi]
fn fibonacci(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

#[napi]
pub struct Animal {
    pub name: String,
    pub kind: String,
}

#[napi]
impl Animal {
    #[napi(constructor)]
    pub fn new(name: String, kind: String) -> Self {
        Animal { name, kind }
    }

    #[napi]
    pub fn describe(&self) -> String {
        format!("{} is a {}", self.name, self.kind)
    }
}

TypeScript type definitions are auto-generated and can be distributed as an npm package:

// Type-safe usage based on auto-generated type definitions
import { fibonacci, Animal } from './index.js';

console.log(fibonacci(10));  // 55

const cat = new Animal("Tama", "cat");
console.log(cat.describe());  // "Tama is a cat"

Cross-Building with napi-rs

The real strength of napi-rs lies in distributing pre-built binaries. Cross-compile for multiple platforms in CI and distribute as an npm package:

# For Linux x64
napi build --platform --release --target x86_64-unknown-linux-gnu
# For macOS ARM64
napi build --platform --release --target aarch64-apple-darwin
# For Windows x64
napi build --platform --release --target x86_64-pc-windows-msvc

Users just run npm install and the pre-built binary for their platform is automatically installed. No need to set up a build environment.

In the real ecosystem, many high-performance JavaScript tools are implemented in Rust using napi-rs, including SWC (JavaScript/TypeScript compiler), Biome (linter/formatter), Rspack (webpack-compatible bundler), and Oxc (JavaScript toolchain).

Why Rust Excels at Cross-Compilation

The reason Rust's cross-compilation works so well stems from several design characteristics:

  1. LLVM backend: Since Rust uses LLVM as its code generator, it can generate code for every target LLVM supports
  2. Static linking by default: Rust binaries statically link their dependencies, so shared libraries don't need to exist on the target environment
  3. Minimal runtime: Rust has no GC or runtime, depending only on the OS's libc (#![no_std] eliminates even libc)
  4. Unified through Cargo: Build configuration is centralized in Cargo.toml and .cargo/config.toml, so cross-compilation settings are managed uniformly
  5. Pre-compiled standard libraries: Target-specific standard libraries are distributed through rustup, eliminating the need to recompile the standard library

These characteristics allow Rust to make the "write on the host, deploy to the target" workflow easier than any other systems programming language.

Back to book