組み込み C vs 組み込み Rust — 開発体験の比較
組み込み開発では長年 C 言語が主流ですが、Rust はその安全性とモダンな言語機能で注目を集めています。 この記事では、両者の違いを実際の開発フローに沿って比較します。
メモリ安全性
Section titled “メモリ安全性”C 言語では以下の問題が頻発します。
- バッファオーバーフロー: 配列の境界チェックが実行時に行われない
- ダングリングポインタ: 解放後のメモリへのアクセス
- データ競合: マルチスレッド・割り込みコンテキスト間での安全でない共有
Rust のアプローチ
Section titled “Rust のアプローチ”Rust はコンパイル時にこれらの問題を検出します。
// コンパイルエラー: 借用チェッカーが不正なアクセスを検出let mut buffer = [0u8; 64];let slice = &buffer[0..32]; // `&` で不変借用(イミュータブル・レファレンス)buffer[0] = 1; // error: cannot assign to `buffer` because it is borrowed組み込みではデバッガが使いにくい場面が多いため、コンパイル時の検出は大きな利点になります。
ツールチェーン
Section titled “ツールチェーン”| 項目 | C | Rust |
|---|---|---|
| コンパイラ | GCC / Clang(ターゲット別) | rustc(cargo build --target で切替) |
| パッケージ管理 | 手動 / PlatformIO / CubeMX | Cargo(依存解決・ビルド統合) |
| フォーマッタ | clang-format(設定必要) | cargo fmt(デフォルトで統一) |
| リンタ | clang-tidy / PC-lint | cargo clippy(豊富な lint ルール) |
| テスト | Unity / CMock 等(個別導入) | cargo test(組込 + ホストテスト統合) |
ハードウェア抽象化
Section titled “ハードウェア抽象化”C: ペリフェラルレジスタ直操作
Section titled “C: ペリフェラルレジスタ直操作”C 言語では volatile 宣言を付けることでコンパイラによる最適化を抑止し、レジスタやメモリマップドI/Oを直接 R/W アクセスすることが可能です。
この機能は低レベルなドライバーの開発やマイコンの制御においては効率的ですが、安全性という観点では課題も残ります。
// STM32 の GPIO 出力(レジスタ直叩き)*((volatile uint32_t *)0x40020014) = (1 << 5); // GPIOA ODR危険な例) 間違ったレジスタアドレスを書き換えてクラッシュを誘発するなど。致命的なバグにつながる可能性も。
Rust: 型安全な抽象化
Section titled “Rust: 型安全な抽象化”// embedded-hal トレイトに基づく抽象化use embedded_hal::digital::OutputPin;led.set_high().unwrap();embedded-hal トレイトに準拠すれば、MCU を変えてもドライバコードがそのまま使えます。
C: コールバック・状態マシン
Section titled “C: コールバック・状態マシン”C 言語の割り込み処理の場合、割り込みハンドラーやRTOSで制御するタスクによってハードウェアへのアクセスを行うケースが多いです。 これはリアルタイム性を確保しつつ効率よくハードウェアを制御するために有効な手段ですが、 コードが複雑になると、どのタスクがどのハードウェアにアクセスしているのかを把握するのが難しくなることがあります。
// コールバック地獄の例void uart_rx_handler(uint8_t byte) { switch (state) { case WAITING_HEADER: if (byte == HEADER) state = READING_DATA; break; case READING_DATA: buffer[pos++] = byte; if (pos >= EXPECTED_LEN) state = COMPLETE; break; // ... }}Rust: async/await
Section titled “Rust: async/await”embassy-rs では、async/await を使用して、CPUスレッドおよび割り込みを非同期処理として扱うことができます。
// Embassy による非同期 UART 受信let mut buf = [0u8; 64];let len = uart.read(&mut buf).await?;defmt::info!("受信: {=[u8]:x}", &buf[..len]);このようにフレームワークを適切に使用することで非同期処理が書きやすいのも、Rust の特徴の一つです。
エコシステム
Section titled “エコシステム”- 膨大な既存コード: 数十年の蓄積があるベンダライブラリ、ミドルウェア
- CMSIS: ARM 公式のハードウェア抽象化標準
- Zephyr / FreeRTOS: 成熟したリアルタイムOS
- 広いチップサポート: ほぼ全てのマイコンに対応
Rust の強み
Section titled “Rust の強み”- cargo: 依存管理・ビルド・テストの統合
- serde: 型安全なシリアライズ/デシリアライズ
- defmt: 組み込み向けの高効率ログ
- probe-rs: Rust 製の統合デバッグツール
- コンパイル時検証: メモリ安全性 + 型システムによるバグ予防
移行の現実的なアプローチ
Section titled “移行の現実的なアプローチ”1. 段階的な導入
Section titled “1. 段階的な導入”- C プロジェクトの一部モジュールを Rust で書き直す
- FFI(Foreign Function Interface)で C ↔ Rust を連携
- 新規モジュールから Rust を採用
2. 既存 C ライブラリの活用
Section titled “2. 既存 C ライブラリの活用”// C のヘッダを Rust から呼び出しextern "C" { fn vendor_init_peripheral() -> i32;}bindgen クレートで C ヘッダから Rust バインディングを自動生成できます。
3. ツールの整備
Section titled “3. ツールの整備”probe-rsでフラッシュ書き込み・デバッグsvd2rustで SVD ファイルから PAC を自動生成cortex-m-rtでスタートアップルーチンを自動化
| 観点 | C | Rust |
|---|---|---|
| 学習コスト | 低(広く知られた言語) | 高(所有権・ライフタイム) |
| メモリ安全性 | 実行時に依存 | コンパイル時保証 |
| 開発効率 | 成熟したツール | Cargo で高い生産性 |
| エコシステム | 非常に豊富 | 急速に成長中 |
| 既存資産 | 膨大 | C との連携可能 |
Rust は新規プロジェクトや安全性が重要なプロジェクトで特に力を発揮します。C の既存資産を活かしつつ、段階的に導入するのが現実的なアプローチです。