メモリレベルのプログラミング
安全な Rust とその限界
ここまで見てきた Rust の安全保証 — 所有権,借用チェッカー,ライフタイム — はすべて「安全な (safe) Rust」の範囲内での話だ.しかし,システムプログラミング言語として,Rust はこの安全な境界の外側にも踏み出す必要がある場面を想定している.
ハードウェアのメモリマップドレジスタに直接アクセスしたい.OS のシステムコールを呼びたい.C ライブラリとインターフェースしたい.ロックフリーなデータ構造を実装したい.— これらは「安全な Rust」だけでは実現できない.
unsafe Rust
unsafe キーワードは,コンパイラの安全保証を部分的に無効化し,プログラマの責任で安全性を保証する領域を宣言する.
unsafe ブロック内で追加的に許可される操作は 5 つだけ だ:
- 生ポインタ (
*const T/*mut T) の参照外し unsafeな関数の呼び出しunsafeなトレイトの実装- 可変な静的変数 (
static mut) へのアクセス unionのフィールドアクセス
fn main() {
let mut num = 5;
// 生ポインタの作成自体は安全(参照外しが unsafe)
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
// 生ポインタの参照外しは unsafe ブロック内でのみ可能
println!("r1: {}", *r1);
println!("r2: {}", *r2);
}
}
重要な点: unsafe は「何でもあり」を意味しない. 借用チェッカー,型チェック,その他の安全チェックは unsafe ブロック内でも有効だ.unsafe が解除するのは上記の 5 つの操作の制限のみだ.
unsafe 関数
// この関数は呼び出し側に安全性の保証を要求する
unsafe fn dangerous() {
// ...
}
fn main() {
unsafe {
dangerous();
}
}
unsafe fn は「この関数を正しく使うための前提条件がある」ことを示す.その前提条件を満たすのは呼び出し側の責任だ.
安全な抽象化
unsafe の最も重要な使い方は,安全な API の内部実装 としてだ:
// 安全な公開 API
pub fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
assert!(mid <= len);
let ptr = values.as_mut_ptr();
// 内部で unsafe を使うが,API 自体は安全
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
この関数は 1 つの可変スライスを 2 つの重複しない可変スライスに分割する.借用チェッカーは「1 つの可変参照から 2 つの可変参照を作る」ことを許可しないが,2 つのスライスが重複しないことをプログラマが保証できる場合,unsafe を使ってこれを実現する.
公開 API は安全であり,unsafe の詳細は内部に隠蔽される.安全な Rust はこのパターンの上に成り立っている. 標準ライブラリの Vec, String, HashMap — これらはすべて内部で unsafe を使っているが,公開 API は安全だ.
FFI (Foreign Function Interface)
Rust は C 言語との相互運用を第一級でサポートしている:
C 関数の呼び出し
extern "C" {
fn abs(input: i32) -> i32;
fn strlen(s: *const std::ffi::c_char) -> usize;
}
fn main() {
unsafe {
println!("abs(-5) = {}", abs(-5));
}
}
extern "C" ブロックで宣言された関数は C の ABI (Application Binary Interface) に従って呼び出される.外部関数の呼び出しは常に unsafe だ — Rust のコンパイラは外部関数の安全性を検証できないためだ.
Rust 関数を C に公開
#[no_mangle]
pub extern "C" fn rust_function(x: i32) -> i32 {
x * 2
}
#[no_mangle] は Rust のシンボル名マングリングを無効化し,C から rust_function という名前でリンクできるようにする.extern "C" は C の呼び出し規約を使うことを指定する.
#![no_std]: 標準ライブラリなしの世界
Rust の標準ライブラリ (std) は OS の機能(ヒープメモリ割り当て,ファイル I/O,ネットワーク,スレッドなど)に依存している.OS が存在しない環境(組み込みシステム,カーネル開発など)では std を使えない.
#![no_std] 属性は,標準ライブラリへの依存を取り除く:
#![no_std]
// core ライブラリは使える(OS に依存しない基本機能)
use core::fmt;
// alloc ライブラリはアロケータがあれば使える
// extern crate alloc;
// use alloc::vec::Vec;
Rust のライブラリ構成:
| ライブラリ | 依存 | 提供する機能 |
|---|---|---|
core |
なし | プリミティブ型,基本トレイト,Option, Result |
alloc |
アロケータ | Vec, String, Box, Arc |
std |
OS | ファイル I/O, ネットワーク, スレッド, 標準入出力 |
core は OS にもアロケータにも依存しない.ベアメタル環境でも core のすべての機能が使える.
ベアメタルプログラミング
OS がない環境で Rust を動かすには,いくつかの追加的な設定が必要だ:
#![no_std]
#![no_main]
use core::panic::PanicInfo;
// パニック時のハンドラ(OS がないため自分で定義する必要がある)
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
// エントリーポイント(OS の提供する main ではなく,リンカスクリプトで指定)
#[no_mangle]
pub extern "C" fn _start() -> ! {
// ここからプログラムが始まる
loop {}
}
#![no_std]: 標準ライブラリを使わない#![no_main]: Rust の通常のエントリーポイント (fn main()) を使わない#[panic_handler]: パニック時の処理を自分で定義(OS がないため)_start: リンカのエントリーポイント
組み込みプログラミングの例
ARM Cortex-M マイクロコントローラ向けのコード例:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
#[entry]
fn main() -> ! {
// GPIO ピンの制御,タイマーの設定など
let peripherals = stm32f4::Peripherals::take().unwrap();
// LED を点滅させる
loop {
// LED ON
peripherals.GPIOA.odr.modify(|_, w| w.odr5().set_bit());
cortex_m::asm::delay(8_000_000);
// LED OFF
peripherals.GPIOA.odr.modify(|_, w| w.odr5().clear_bit());
cortex_m::asm::delay(8_000_000);
}
}
Rust の型安全性と所有権システムはベアメタル環境でもそのまま機能する.「ペリフェラルの取得は一度だけ」(シングルトンパターン) を型レベルで強制し,レジスタアクセスのビットフィールドを型安全に操作できる.
メモリレイアウトの直接制御
repr 属性
// C 互換のメモリレイアウト
#[repr(C)]
struct CStruct {
a: u8,
b: u32,
c: u8,
}
// 特定サイズの整数として表現
#[repr(u8)]
enum Color {
Red = 0,
Green = 1,
Blue = 2,
}
// パディングなし(ネットワークプロトコルのパケット定義などに使う)
#[repr(packed)]
struct Packet {
header: u8,
length: u16,
data: u32,
}
// 透過的表現(Newtype パターンでランタイムコストゼロを保証)
#[repr(transparent)]
struct Wrapper(u32);
ポインタ演算
fn main() {
let data = [10u8, 20, 30, 40, 50];
let ptr = data.as_ptr();
unsafe {
// ポインタ演算でメモリに直接アクセス
for i in 0..data.len() {
let value = *ptr.add(i);
println!("data[{}] = {}", i, value);
}
}
}
Rust が選ばれる理由
ベアメタルやシステムプログラミングの世界では,従来 C や C++ が支配的だった.Rust がこの領域で選ばれる理由は:
- 安全性と制御の両立:
unsafeを局所的に使い,大部分のコードは安全な Rust の恩恵を受けられる - ゼロコスト抽象化: 高レベルな構造(イテレータ,ジェネリクス,トレイト)がベアメタルでも使える
- 所有権によるリソース管理: GC がなくても安全なメモリ管理(RAII)が可能
- Cargo エコシステム: 組み込み向けクレート(HAL, PAC, BSP)が Cargo で管理できる
- クロスコンパイル: クロスビルドの章で見た通り,ターゲットの追加が容易
Linux カーネルが 2022 年に Rust を第二言語として採用したのは,これらの特性が実証された最も象徴的な例だ.