コンテンツにスキップ

組み込み C vs 組み込み Rust — 開発体験の比較

組み込み開発では長年 C 言語が主流ですが、Rust はその安全性とモダンな言語機能で注目を集めています。 この記事では、両者の違いを実際の開発フローに沿って比較します。

C 言語では以下の問題が頻発します。

  • バッファオーバーフロー: 配列の境界チェックが実行時に行われない
  • ダングリングポインタ: 解放後のメモリへのアクセス
  • データ競合: マルチスレッド・割り込みコンテキスト間での安全でない共有

Rust はコンパイル時にこれらの問題を検出します。

// コンパイルエラー: 借用チェッカーが不正なアクセスを検出
let mut buffer = [0u8; 64];
let slice = &buffer[0..32]; // `&` で不変借用(イミュータブル・レファレンス)
buffer[0] = 1; // error: cannot assign to `buffer` because it is borrowed

組み込みではデバッガが使いにくい場面が多いため、コンパイル時の検出は大きな利点になります。

項目CRust
コンパイラGCC / Clang(ターゲット別)rustc(cargo build --target で切替)
パッケージ管理手動 / PlatformIO / CubeMXCargo(依存解決・ビルド統合)
フォーマッタclang-format(設定必要)cargo fmt(デフォルトで統一)
リンタclang-tidy / PC-lintcargo clippy(豊富な lint ルール)
テストUnity / CMock 等(個別導入)cargo test(組込 + ホストテスト統合)

C: ペリフェラルレジスタ直操作

Section titled “C: ペリフェラルレジスタ直操作”

C 言語では volatile 宣言を付けることでコンパイラによる最適化を抑止し、レジスタやメモリマップドI/Oを直接 R/W アクセスすることが可能です。 この機能は低レベルなドライバーの開発やマイコンの制御においては効率的ですが、安全性という観点では課題も残ります。

// STM32 の GPIO 出力(レジスタ直叩き)
*((volatile uint32_t *)0x40020014) = (1 << 5); // GPIOA ODR

危険な例) 間違ったレジスタアドレスを書き換えてクラッシュを誘発するなど。致命的なバグにつながる可能性も。

// embedded-hal トレイトに基づく抽象化
use embedded_hal::digital::OutputPin;
led.set_high().unwrap();

embedded-hal トレイトに準拠すれば、MCU を変えてもドライバコードがそのまま使えます。

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;
// ...
}
}

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 の特徴の一つです。

  • 膨大な既存コード: 数十年の蓄積があるベンダライブラリ、ミドルウェア
  • CMSIS: ARM 公式のハードウェア抽象化標準
  • Zephyr / FreeRTOS: 成熟したリアルタイムOS
  • 広いチップサポート: ほぼ全てのマイコンに対応
  • cargo: 依存管理・ビルド・テストの統合
  • serde: 型安全なシリアライズ/デシリアライズ
  • defmt: 組み込み向けの高効率ログ
  • probe-rs: Rust 製の統合デバッグツール
  • コンパイル時検証: メモリ安全性 + 型システムによるバグ予防
  • C プロジェクトの一部モジュールを Rust で書き直す
  • FFI(Foreign Function Interface)で C ↔ Rust を連携
  • 新規モジュールから Rust を採用
// C のヘッダを Rust から呼び出し
extern "C" {
fn vendor_init_peripheral() -> i32;
}

bindgen クレートで C ヘッダから Rust バインディングを自動生成できます。

  • probe-rs でフラッシュ書き込み・デバッグ
  • svd2rust で SVD ファイルから PAC を自動生成
  • cortex-m-rt でスタートアップルーチンを自動化
観点CRust
学習コスト低(広く知られた言語)高(所有権・ライフタイム)
メモリ安全性実行時に依存コンパイル時保証
開発効率成熟したツールCargo で高い生産性
エコシステム非常に豊富急速に成長中
既存資産膨大C との連携可能

Rust は新規プロジェクト安全性が重要なプロジェクトで特に力を発揮します。C の既存資産を活かしつつ、段階的に導入するのが現実的なアプローチです。