クロスコンパイルとは

クロスコンパイルとは,あるプラットフォーム上で,別のプラットフォーム向けのバイナリを生成することを指す.たとえば,macOS上でLinux向けのバイナリをビルドしたり,x86_64マシン上でARM向けのバイナリを生成したりすることがこれに該当する.

C/C++の世界では,クロスコンパイルは悪夢に近い作業だった.ターゲットプラットフォーム向けのクロスコンパイラを入手し,sysrootを設定し,リンカを指定し,依存ライブラリをターゲットアーキテクチャ向けにビルドし直す必要がある.ライブラリがautotoolsを使っていれば --host--buildの指定を間違えないようにしなければならず,CMakeであればtoolchain fileを書く必要がある.

Rustでは,クロスコンパイルは驚くほど簡単だ.

ターゲットトリプル

Rust(というよりLLVM)はターゲットプラットフォームを ターゲットトリプル で表現する.名前は「トリプル(3つ組)」だが,実際には3つ以上の要素を含むことがある:

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

例:

ターゲットトリプル 意味
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 (ベアメタル)

Rustでのクロスコンパイル

基本的な手順は2ステップ:

# 1. ターゲットの標準ライブラリを追加
rustup target add aarch64-unknown-linux-gnu

# 2. ターゲットを指定してビルド
cargo build --target aarch64-unknown-linux-gnu

これだけだ.RustのコンパイラはLLVMをバックエンドに持つため,LLVMがサポートするすべてのターゲットアーキテクチャに対してコードを生成できる.標準ライブラリはプリコンパイル済みのものがrustupを通じて配布されるため,ターゲット向けに標準ライブラリをビルドし直す必要もない.

ただし,Cライブラリに依存するクレートを使っている場合は,ターゲット向けのCクロスコンパイラとリンカが別途必要になる.純粋なRustコードだけで構成されたプロジェクトであれば,上記の2ステップだけで完結する.

リンカの設定

ターゲット向けのリンカが必要な場合,.cargo/config.tomlに設定を追加する:

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

WebAssembly (Wasm)

Rustのクロスコンパイルが最も輝くユースケースの一つがWebAssemblyだ.

WebAssemblyとは

WebAssembly (Wasm)は,ブラウザ上で動作するバイナリフォーマットだ.JavaScript以外の言語で書かれたコードをブラウザで実行可能にするために設計された.Rustは,WebAssemblyのコンパイルターゲットとして最も成熟したサポートを持つ言語の一つだ.

wasm32-unknown-unknown

RustからWebAssemblyへのコンパイルは,ターゲットトリプルとしてwasm32-unknown-unknownを指定するだけだ:

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

これにより .wasmファイルが生成される.しかし,生の .wasmファイルを直接ブラウザで使うのは手間がかかる.そこで登場するのがwasm-packだ.

wasm-pack

wasm-packはRustのコードをWebAssemblyにコンパイルし,JavaScript/TypeScriptから利用できるパッケージとして出力するツールだ:

# インストール
cargo install wasm-pack

# ビルド(npm パッケージとして出力)
wasm-pack build --target web

wasm-packは以下を自動で行う:

  1. Rustコードを .wasmにコンパイル

  2. wasm-bindgenによるJavaScriptバインディングの生成

  3. TypeScriptの型定義ファイル (.d.ts)の生成

  4. package.jsonの生成

生成されたパッケージはnpmに公開することも,ローカルで直接インポートして使うこともできる.

wasm-bindgen

wasm-bindgenは,RustとJavaScriptの間のインターフェースを生成するツール/ライブラリだ.#[wasm_bindgen]属性を使って,Rustの関数や型を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
    }
}

JavaScript側からは通常の関数やクラスのように使える:

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

Rustの型安全性を保ちつつ,JavaScriptの世界とシームレスに接続できる.

WASI (WebAssembly System Interface)

WebAssemblyはブラウザだけのものではない.WASIは,WebAssemblyをブラウザ外(サーバーサイド,CLIツール,エッジコンピューティングなど)で実行するための標準化されたシステムインターフェースだ.

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

WASIターゲットでビルドされたWasmバイナリは,WasmtimeやWasmerといったランタイムで実行できる:

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

Rust + WASIの組み合わせは,「一度書いて,どこでも実行する」というビジョンを現実のものにしつつある.

napi-rs: Node.jsネイティブアドオン

もう一つの重要なクロスビルドのユースケースがnapi-rsだ.

napi-rsとは

napi-rsは,RustでNode.jsのネイティブアドオン(N-APIモジュール)を書くためのフレームワークだ.従来,Node.jsのネイティブアドオンはC/C++で書かれ,node-gypでビルドされていた.napi-rsは,これをRustで置き換えることを可能にする.

なぜnapi-rsか

Node.jsのネイティブアドオン開発におけるC/C++ + node-gypの問題点:

  • ビルドが壊れやすい: node-gypはPython 2/3, Visual Studio Build Toolsなどの外部依存が多く,環境構築でトラブルが頻発する

  • メモリ安全でない: C/C++はメモリ安全性を保証しないため,ネイティブアドオンのバグがプロセス全体をクラッシュさせうる

  • クロスコンパイルが困難: プラットフォームごとにビルド環境を用意する必要がある

napi-rsはこれらの問題を解決する:

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の型定義が自動生成され,npmパッケージとして配布できる:

// 自動生成された型定義に基づいて,型安全に使える
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"

napi-rsのクロスビルド

napi-rsの真骨頂は,プリビルドバイナリの配布 にある.CI上で複数プラットフォーム向けにクロスコンパイルし,npmパッケージとして配布できる:

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

ユーザーはnpm installするだけで,自分のプラットフォーム向けのプリビルドバイナリが自動的にインストールされる.ビルド環境を用意する必要がない.

実際のエコシステムでは,SWC (JavaScript/TypeScriptのコンパイラ),Biome (Linter/Formatter),Rspack (webpack互換のバンドラ),Oxc (JavaScriptツールチェーン) など,多くの高速なJavaScriptツールがnapi-rsを使ってRustで実装されている.

Rustがクロスコンパイルに強い理由

Rustのクロスコンパイルがここまでうまく機能する理由は,いくつかの設計上の特性に起因する:

  1. LLVMバックエンド: RustはLLVMをコードジェネレータとして使用しているため,LLVMがサポートするすべてのターゲットに対してコード生成が可能

  2. 静的リンクがデフォルト: Rustのバイナリは依存ライブラリを静的リンクするため,ターゲット環境に共有ライブラリが存在する必要がない

  3. 最小限のランタイム: RustにはGCやランタイムがなく,OSのlibcにのみ依存する(#![no_std]ならlibcすら不要)

  4. Cargoによる統一: ビルドの設定がCargo.toml.cargo/config.tomlに集約されるため,クロスコンパイルの設定も統一的に管理できる

  5. プリコンパイル済み標準ライブラリ: rustupを通じてターゲット向けの標準ライブラリが配布されるため,標準ライブラリの再コンパイルが不要

これらの特性により,Rustは「ホスト環境で書いて,ターゲット環境にデプロイする」というワークフローを,他のどのシステムプログラミング言語よりも容易に実現できる.