Wonderfull Rust

クロスビルド

クロスコンパイルとは

クロスコンパイルとは,あるプラットフォーム上で,別のプラットフォーム向けのバイナリを生成することを指す.たとえば,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 は「ホスト環境で書いて,ターゲット環境にデプロイする」というワークフローを,他のどのシステムプログラミング言語よりも容易に実現できる.

本に戻る