クロスビルド
クロスコンパイルとは
クロスコンパイルとは,あるプラットフォーム上で,別のプラットフォーム向けのバイナリを生成することを指す.たとえば,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 は以下を自動で行う:
- Rust コードを
.wasmにコンパイル wasm-bindgenによる JavaScript バインディングの生成- TypeScript の型定義ファイル (
.d.ts) の生成 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 のクロスコンパイルがここまでうまく機能する理由は,いくつかの設計上の特性に起因する:
- LLVM バックエンド: Rust は LLVM をコードジェネレータとして使用しているため,LLVM がサポートするすべてのターゲットに対してコード生成が可能
- 静的リンクがデフォルト: Rust のバイナリは依存ライブラリを静的リンクするため,ターゲット環境に共有ライブラリが存在する必要がない
- 最小限のランタイム: Rust には GC やランタイムがなく,OS の libc にのみ依存する(
#![no_std]なら libc すら不要) - Cargo による統一: ビルドの設定が
Cargo.tomlと.cargo/config.tomlに集約されるため,クロスコンパイルの設定も統一的に管理できる - プリコンパイル済み標準ライブラリ:
rustupを通じてターゲット向けの標準ライブラリが配布されるため,標準ライブラリの再コンパイルが不要
これらの特性により,Rust は「ホスト環境で書いて,ターゲット環境にデプロイする」というワークフローを,他のどのシステムプログラミング言語よりも容易に実現できる.