Rust組込みLinux開発:2026年ベアメタルからデバイスドライバまでの完全実践ガイド
組込み開発のペインポイント、いくつ当てはまりましたか?
C言語で組込み開発をすると、メモリリークとセグメンテーションフォールトは日常茶飯事——本番デバイスが動いているうちにクラッシュし、リモートデバッグは基本的に推測頼みです。クロスコンパイル環境の構築だけで半日かかり、ターゲットボードを変えるとやり直しです。暗黙の未定義動作は、テスト環境では問題なくても本番で間欠的にクラッシュし、再現できません。
Rustのゼロコスト抽象化と所有権システムにより、組込み開発はついに安全かつ効率的になります。2026年、Rust組込みエコシステムは十分に成熟しており、Cからの移行を真剣に検討すべき時です。
Rust組込みコア概念
| 概念 | 説明 | C言語との比較 |
|---|---|---|
| no_std | 標準ライブラリ不使用、ベアメタル・カーネル開発に適する | Cのfreestandingモード |
| 所有権システム | コンパイル時メモリ安全性保証、GC不要 | Cは手動管理、エラー発生しやすい |
| クロスコンパイル | ネイティブマルチターゲットクロスコンパイル対応 | クロスツールチェーンが必要 |
| Embedded HAL | ハードウェア抽象レイヤ、統一ペリフェラルインタフェース | CのHALに似ているが型安全 |
| defmt | リソース制限環境向け高効率ログフレームワーク | printfの軽量代替 |
主要crateエコシステム:
| crate | 用途 | スター数 |
|---|---|---|
cortex-m |
ARM Cortex-M低レベルサポート | 1.2k |
embedded-hal |
ハードウェア抽象レイヤtrait | 1.8k |
linux-embedded-hal |
LinuxプラットフォームHAL実装 | 350+ |
sysfs-gpio |
Linux sysfs GPIO操作 | 200+ |
tokio-serial |
非同期シリアル通信 | 500+ |
問題の深掘り:なぜ組込みにRustが必要なのか?
従来のC言語組込み開発が直面するコア問題:
- メモリ安全性:バッファオーバーフローが組込み脆弱性の60%以上を占める
- 並行安全性:割り込みとメインループのデータ競合は検出困難
- ツールチェーンの断片化:異なるベンダのクロスコンパイルツールチェーンが互換性なし
Rustのソリューション比較:
| 問題 | C言語ソリューション | Rustソリューション | 利点 |
|---|---|---|---|
| バッファオーバーフロー | コードレビュー+静的解析 | コンパイル時境界チェック | ゼロランタイムオーバーヘッド |
| データ競合 | volatile+割り込み無効化 | Send/Sync trait | コンパイル時保証 |
| ツールチェーン断片化 | ベンダ固有ツールチェーン | rustup統一管理 | 1ツールチェーンでマルチターゲット |
| 未定義動作 | 仕様による制約 | unsafeブロックの明示的マーク | 安全境界が明確 |
ステップバイステップ:ベアメタルからLinuxドライバまで
ステップ1:クロスコンパイル環境の構築
# Rustとクロスコンパイルターゲットのインストール
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add armv7-unknown-linux-gnueabihf
rustup target add aarch64-unknown-linux-gnu
rustup target add thumbv7em-none-eabihf
# クロスリンカのインストール
sudo apt install gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
# cargoクロスコンパイルの設定
mkdir -p .cargo
cat > .cargo/config.toml << 'EOF'
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
EOF
ステップ2:no_stdベアメタルプログラム
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use embedded_hal::digital::OutputPin;
use panic_halt as _;
#[entry]
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let cp = cortex_m::Peripherals::take().unwrap();
let mut rcc = dp.RCC.constrain();
let mut gpioa = dp.GPIOA.split(&mut rcc.ahb);
let mut led = gpioa.pa5.into_push_pull_output(&mut gpioa.moder, &mut gpioa.otyper);
let mut delay = cp.SYST.delay(&mut rcc.clocks);
loop {
led.set_high().ok();
delay.delay_ms(500u32);
led.set_low().ok();
delay.delay_ms(500u32);
}
}
ステップ3:Linuxユーザースペースデバイスドライバ
use std::fs;
use std::io::{self, Read, Write};
use std::path::Path;
use std::time::Duration;
pub struct GpioPin {
pin_num: u32,
base_path: String,
}
impl GpioPin {
pub fn new(pin_num: u32) -> io::Result<Self> {
let base_path = format!("/sys/class/gpio/gpio{}", pin_num);
if !Path::new(&base_path).exists() {
fs::write("/sys/class/gpio/export", pin_num.to_string())?;
}
std::thread::sleep(Duration::from_millis(100));
Ok(Self { pin_num, base_path })
}
pub fn set_direction(&self, direction: &str) -> io::Result<()> {
fs::write(format!("{}/direction", self.base_path), direction)
}
pub fn set_value(&self, value: u8) -> io::Result<()> {
fs::write(format!("{}/value", self.base_path), value.to_string())
}
pub fn get_value(&self) -> io::Result<u8> {
fs::read_to_string(format!("{}/value", self.base_path))?
.trim().parse::<u8>()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
}
impl Drop for GpioPin {
fn drop(&mut self) {
let _ = fs::write("/sys/class/gpio/unexport", self.pin_num.to_string());
}
}
fn main() -> io::Result<()> {
let led = GpioPin::new(17)?;
led.set_direction("out")?;
for i in 0..10 {
led.set_value(1)?;
std::thread::sleep(Duration::from_millis(200));
led.set_value(0)?;
std::thread::sleep(Duration::from_millis(200));
println!("Blink #{}", i + 1);
}
Ok(())
}
ステップ4:Cargo.toml設定
[package]
name = "embedded-linux-driver"
version = "0.1.0"
edition = "2021"
[dependencies]
embedded-hal = "1.0"
linux-embedded-hal = "0.4"
nix = { version = "0.29", features = ["ioctl", "fs"] }
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true
panic = "abort"
ステップ5:IoTデバイスMQTT通信
use std::time::Duration;
use std::thread;
struct SensorReading {
sensor_id: String,
temperature: f32,
humidity: f32,
timestamp: u64,
}
impl SensorReading {
fn to_json(&self) -> String {
format!(
r#"{{"sensorId":"{}","temperature":{:.2},"humidity":{:.2},"timestamp":{}}}"#,
self.sensor_id, self.temperature, self.humidity, self.timestamp
)
}
}
fn main() {
loop {
let reading = SensorReading {
sensor_id: "SENSOR-001".to_string(),
temperature: 23.5,
humidity: 55.0,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
};
println!("Publishing: {}", reading.to_json());
thread::sleep(Duration::from_secs(5));
}
}
完全コード:組込みLinuxデバイスモニターデーモン
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
static RUNNING: AtomicBool = AtomicBool::new(true);
struct DeviceMonitor {
gpio_pins: Vec<u32>,
i2c_bus: u8,
watch_dir: PathBuf,
report_interval: Duration,
}
impl DeviceMonitor {
fn new(gpio_pins: Vec<u32>, i2c_bus: u8, watch_dir: &str) -> Self {
Self { gpio_pins, i2c_bus, watch_dir: PathBuf::from(watch_dir), report_interval: Duration::from_secs(10) }
}
fn read_gpio(&self, pin: u32) -> io::Result<u8> {
fs::read_to_string(format!("/sys/class/gpio/gpio{}/value", pin))?
.trim().parse::<u8>().map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
fn read_cpu_temp(&self) -> io::Result<f32> {
let raw = fs::read_to_string("/sys/class/thermal/thermal_zone0/temp")?;
Ok(raw.trim().parse::<f32>().map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? / 1000.0)
}
fn run(&self) -> io::Result<()> {
let start = Instant::now();
while RUNNING.load(Ordering::Relaxed) {
let temp = self.read_cpu_temp().unwrap_or(0.0);
println!("[{:?}] CPU: {:.1}°C", start.elapsed(), temp);
std::thread::sleep(self.report_interval);
}
Ok(())
}
}
fn main() -> io::Result<()> {
ctrlc::set_handler(|| RUNNING.store(false, Ordering::Relaxed))
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
DeviceMonitor::new(vec![17, 27, 22], 1, "/var/log/device-monitor").run()
}
よくある落とし穴ガイド
落とし穴1:クロスコンパイルリンカが見つからない
linker 'arm-linux-gnueabihf-gcc' not found が最も一般的なエラー。
解決策:.cargo/config.tomlでリンカパスを明示的に指定、gcc-arm-linux-gnueabihfがインストール済みであることを確認。
落とし穴2:no_stdでprintln!が使用不可
no_std環境には標準ライブラリがないため、println!マクロは使用できません。
解決策:defmt::info!またはcortex_m_semihosting::hprintln!を使用。
落とし穴3:GPIO権限不足
/sys/class/gpioの操作にroot権限またはgpioグループが必要。
解決策:sudo usermod -aG gpio $USER、udevルールを設定。
落とし穴4:I2C/SPIデバイスオープン失敗
/dev/i2c-1が存在しない場合、カーネルモジュールが未有効。
解決策:sudo modprobe i2c-dev、sudo dtparam=i2c_arm=on
落とし穴5:リリースビルドが大きすぎる
デフォルトリリースビルドは数MBになる可能性があります。
解決策:opt-level = "z"、lto = true、strip = true、panic = "abort"を設定。
エラートラブルシューティング
| # | エラーメッセージ | 原因 | 解決方法 |
|---|---|---|---|
| 1 | linker 'arm-linux-gnueabihf-gcc' not found |
クロスコンパイルツールチェーン未インストール | sudo apt install gcc-arm-linux-gnueabihf |
| 2 | could not find native static library 'm' |
数学ライブラリ不足 | アーキテクチャ別libc-devをインストール |
| 3 | can't find crate for std |
ターゲット未インストール | rustup target add <target> |
| 4 | panic handler not found |
no_stdでpanicハンドラ不足 | panic-haltを追加 |
| 5 | Permission denied: /sys/class/gpio/export |
GPIO操作権限なし | gpioグループに追加 |
| 6 | No such file: /dev/i2c-1 |
I2Cカーネルモジュール未有効 | sudo modprobe i2c-dev |
| 7 | SPI transfer failed: Invalid argument |
SPI設定エラー | SPIパラメータを確認 |
| 8 | stack overflow in thread main |
スタックサイズ不足 | スタックサイズを増加 |
| 9 | undefined symbol: __aeabi_uidiv |
ソフト浮動小数点ターゲット | ハード浮動小数点ターゲットを使用 |
| 10 | failed to run custom command |
build.rsの外部ツール不在 | 依存関係をインストール |
高度な最適化
1. ゼロアロケーションログ:defmt
#![no_std]
use defmt_rtt as _;
use panic_probe as _;
fn process_sensor(value: f32) {
defmt::info!("Sensor: {:.2}", value);
}
2. 非同期IoT通信
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
async fn send_telemetry(host: &str, port: u16, data: &[u8]) -> std::io::Result<()> {
let mut stream = TcpStream::connect((host, port)).await?;
stream.write_all(data).await?;
Ok(())
}
3. ファームウェアOTAアップデート
use std::io::Read;
use sha2::{Sha256, Digest};
fn verify_checksum(path: &str, expected: &str) -> std::io::Result<bool> {
let mut file = std::fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buf = [0u8; 8192];
loop {
let n = file.read(&mut buf)?;
if n == 0 { break; }
hasher.update(&buf[..n]);
}
Ok(format!("{:x}", hasher.finalize()) == expected)
}
比較分析
| 次元 | Rust組込み | C組込み | Zig組込み | MicroPython |
|---|---|---|---|---|
| メモリ安全性 | コンパイル時保証 | 手動管理 | 部分保証 | ランタイムGC |
| ランタイムオーバーヘッド | ゼロコスト | なし | 極小 | 大きい |
| クロスコンパイル | rustup統一 | ベンダツールチェーン | 内蔵 | インタープリタ |
| エコシステム成熟度 | 中 | 非常に成熟 | 初期 | 中 |
| 学習曲線 | 急 | 中 | 中 | 低 |
| デバッグサポート | probe-run+defmt | GDB/OpenOCD | 内蔵 | REPL |
| バイナリサイズ | 小 | 最小 | 小 | 大 |
| リアルタイム性 | 保証可能 | 保証可能 | 保証可能 | 保証不可 |
まとめ:Rustは組込み開発にコンパイル時メモリ安全性とゼロコスト抽象化のユニークな組み合わせをもたらします。ベアメタルno_std開発からLinuxユーザースペースドライバまで、Rustの型システムはほとんどのメモリエラーと並行問題をコンパイル時に捕捉します。2026年のRust組込みエコシステムはCほど成熟していませんが、HAL抽象レイヤとクロスコンパイルツールチェーンは本番使用に十分です。Linuxユーザースペースドライバから始め、徐々にno_stdベアメタル開発に深入りすることで、安全性と開発効率の最適なバランスを見つけられます。
オンラインツールおすすめ
- JSONフォーマッター:/ja/json/format — IoTデバイスデータとセンサー読取値のフォーマット
- Base64エンコード/デコード:/ja/encode/base64 — ファームウェアアップデートパッケージとデバイス証明書の処理
- Curl to Code:/ja/dev/curl-to-code — APIデバッグcurlコマンドをRust HTTPクライアントコードに変換
ブラウザローカルツールを無料で試す →