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