Chapter 4: Advanced Ownership - Unsafe、FFI、Pin
この章の目標
この章を読み終えると、以下のことが理解できるようになります:
- Unsafe Rustの5つの超能力と責任
- FFI(Foreign Function Interface)での所有権管理
- Pin
とUnpinの理論と実践 - 自己参照構造体の安全な実装
- メモリレイアウトと repr の使い方
- Variance(変性)と高度なライフタイム
- 低レベルシステムプログラミング: OS、ドライバ、組み込み
- C/C++とのインターフェース: 既存ライブラリの活用
- 高性能データ構造: Vecの内部実装のような最適化
- 非同期プログラミング: Future、async/awaitの内部実装
なぜAdvanced Ownershipを学ぶのか
Safe Rustの所有権システムは強力ですが、以下のような場合には不十分です:
これらの分野では、unsafeを使って所有権システムの制約を一時的に解除し、より柔軟な実装を行います。しかし、unsafeは責任を伴います。この章では、unsafeを安全に使う方法を学びます。
1. Unsafe Rustの5つの超能力
1.1 Unsafeとは何か
Rustの型システムは保守的(conservative)です。つまり、安全なコードを拒否することもあります。
┌─────────────────────────────────────────────────────────┐
│ コンパイラの判断 │
├─────────────────────────────────────────────────────────┤
│ │
│ 全てのコード │
│ ├─ 確実に安全 → Safe Rustで許可 │
│ ├─ 確実に危険 → コンパイルエラー │
│ └─ 安全かも? → Safe Rustで拒否、unsafeで許可 │
│ │
│ コンパイラは「疑わしきは罰する」 │
│ → unsafeで「私が責任を持つ」と宣言 │
│ │
└─────────────────────────────────────────────────────────┘
1.2 Unsafeの5つの超能力
unsafeブロックでは、以下の5つのことができます:
1. 生ポインタのデリファレンス
let x = 5;
let raw = &x as *const i32; // 生ポインタの作成(safeでOK)
unsafe {
println!("{}", *raw); // デリファレンスはunsafe
}
なぜunsafe?
- 生ポインタはnullかもしれない
- ダングリングポインタかもしれない
- アライメントが正しくないかもしれない
Safe Rustとの違い:
// Safe Rust: コンパイラが保証
let x = 5;
let r = &x; // 常に有効
println!("{}", *r); // 安全
// Unsafe Rust: プログラマが保証
let raw = &x as *const i32;
unsafe {
// 有効性は自分で確認
println!("{}", *raw);
}
2. Unsafe関数/メソッドの呼び出し
unsafe fn dangerous() {
// 危険な操作
}
fn main() {
unsafe {
dangerous(); // unsafeブロックが必要
}
}
標準ライブラリの例:
use std::slice;
let v = vec![1, 2, 3, 4, 5];
let ptr = v.as_ptr();
let len = v.len();
unsafe {
// from_raw_partsはunsafe関数
let slice = slice::from_raw_parts(ptr, len);
println!("{:?}", slice);
}
3. Mutableなstatic変数へのアクセス
static mut COUNTER: i32 = 0;
fn increment() {
unsafe {
COUNTER += 1; // unsafeが必要
}
}
なぜunsafe?
- データ競合の可能性
- グローバルな可変状態
より安全な代替案:
use std::sync::atomic::{AtomicI32, Ordering};
static COUNTER: AtomicI32 = AtomicI32::new(0);
fn increment() {
COUNTER.fetch_add(1, Ordering::SeqCst); // Safeで書ける!
}
4. Unsafeトレイトの実装
unsafe trait Foo {
// 実装に特定の不変条件が必要
}
unsafe impl Foo for i32 {
// 不変条件を守る責任がある
}
標準ライブラリの例:
// Sendトレイト: 値を他のスレッドに送信できる
unsafe impl<T: Send> Send for MyType<T> {}
// Syncトレイト: 複数スレッドから参照を共有できる
unsafe impl<T: Sync> Sync for MyType<T> {}
5. Unionフィールドへのアクセス
union MyUnion {
f1: u32,
f2: f32,
}
let u = MyUnion { f1: 1 };
unsafe {
println!("{}", u.f1); // どのフィールドが有効か不明
}
使用例:
// FFIでC言語のunionを表現
#[repr(C)]
union IpAddr {
ipv4: [u8; 4],
ipv6: [u8; 16],
}
1.3 Unsafeの責任
unsafeを使うと、以下を自分で保証する必要があります:
┌─────────────────────────────────────────────────────────┐
│ Unsafeで保証すべきこと │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 空間的安全性(Spatial Safety) │
│ - 全てのポインタは有効なメモリを指す │
│ - バッファオーバーフローなし │
│ │
│ 2. 時間的安全性(Temporal Safety) │
│ - Use-after-freeなし │
│ - ダングリングポインタなし │
│ │
│ 3. スレッド安全性(Thread Safety) │
│ - データ競合なし │
│ - 適切な同期 │
│ │
│ 4. 型安全性(Type Safety) │
│ - 無効な値の読み取りなし(例: bool = 2) │
│ - アライメント要件を満たす │
│ │
│ 5. 不変条件(Invariants) │
│ - データ構造の整合性を保つ │
│ - 例: Vecのlen <= capacity │
│ │
└─────────────────────────────────────────────────────────┘
2. FFI(Foreign Function Interface)
2.1 FFIとは
FFIは、Rustから他の言語(主にC)の関数を呼ぶ、または呼ばれる仕組みです。
┌─────────────────────────────────────────────────────────┐
│ FFIの2つの方向 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. Rustから他言語を呼ぶ │
│ Rust → C Library │
│ 例: zlib、OpenSSL、SQLite │
│ │
│ 2. 他言語からRustを呼ぶ │
│ C/Python/JavaScript → Rust Library │
│ 例: PyO3、WASM、Node.jsのネイティブモジュール │
│ │
└─────────────────────────────────────────────────────────┘
2.2 C関数の呼び出し
基本的な例
// C言語のabs関数を呼ぶ
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("abs(-3) = {}", abs(-3));
}
}
注意点:
extern "C": C言語のABI(Application Binary Interface)を使用- 関数呼び出しは
unsafe: Rustの不変条件を保証できない
複雑な例:構造体の受け渡し
// C言語の構造体
// struct Point {
// int x;
// int y;
// };
// void translate(struct Point* p, int dx, int dy);
#[repr(C)]
struct Point {
x: i32,
y: i32,
}
extern "C" {
fn translate(p: *mut Point, dx: i32, dy: i32);
}
fn main() {
let mut p = Point { x: 10, y: 20 };
unsafe {
translate(&mut p, 5, -3);
}
println!("Point: ({}, {})", p.x, p.y);
}
#[repr(C)] の重要性:
Rustのデフォルト構造体:
メモリレイアウトは未定義
最適化のために並び替えられる可能性
#[repr(C)]付き構造体:
Cと同じメモリレイアウト
フィールドの順序が保証される
パディングもCと同じ
2.3 文字列のFFI
C言語とRustでは文字列の扱いが大きく異なります:
┌─────────────────────────────────────────────────────────┐
│ C vs Rust の文字列 │
├─────────────────────────────────────────────────────────┤
│ │
│ C言語(char*) │
│ - ヌル終端('\0'で終わる) │
│ - UTF-8保証なし │
│ - メモリ管理は手動 │
│ - 例: "hello\0" │
│ │
│ Rust(String) │
│ - 長さを保持 │
│ - 必ずUTF-8 │
│ - 所有権で自動管理 │
│ - ヌル文字を含める可能性 │
│ │
└─────────────────────────────────────────────────────────┘
CString/CStrの使用:
use std::ffi::{CString, CStr};
use std::os::raw::c_char;
extern "C" {
fn print_string(s: *const c_char);
}
fn main() {
// Rust String → C char*
let rust_str = String::from("Hello from Rust!");
let c_str = CString::new(rust_str).expect("CString::new failed");
unsafe {
print_string(c_str.as_ptr());
}
// C char* → Rust String
let c_str_ptr: *const c_char = /* C関数から取得 */;
unsafe {
let c_str = CStr::from_ptr(c_str_ptr);
let rust_str = c_str.to_str().expect("Invalid UTF-8");
println!("{}", rust_str);
}
}
注意点:
CString::new: 内部にヌル文字があるとエラーto_str(): UTF-8検証、失敗の可能性c_str.as_ptr(): ポインタの生存期間に注意
2.4 所有権とFFI
FFIでは、所有権の境界を明確にする必要があります:
// パターン1: Rustが所有、Cは借用
pub extern "C" fn rust_owns(data: *const u8, len: usize) {
// Rustが確保したメモリをCに渡す(読み取り専用)
// Rustが解放する責任を持つ
}
// パターン2: Cが所有、Rustは借用
pub extern "C" fn c_owns(data: *mut u8, len: usize) {
// Cが確保したメモリをRustが使う
// Cが解放する責任を持つ
}
// パターン3: 所有権の移転
pub extern "C" fn transfer_ownership() -> *mut u8 {
// Rustで確保したメモリの所有権をCに渡す
// Cが解放する責任を持つ
let data = Box::new(42u8);
Box::into_raw(data) // 所有権を放棄
}
#[no_mangle]
pub extern "C" fn free_rust_memory(ptr: *mut u8) {
// Cから返されたメモリをRustで解放
unsafe {
let _ = Box::from_raw(ptr); // 所有権を取り戻してドロップ
}
}
ベストプラクティス:
// 生存期間を明示するOpaque型パターン
pub struct OpaqueHandle {
_private: [u8; 0], // ゼロサイズ型
}
#[no_mangle]
pub extern "C" fn create_handle() -> *mut OpaqueHandle {
let data = Box::new(RealData { /* ... */ });
Box::into_raw(data) as *mut OpaqueHandle
}
#[no_mangle]
pub extern "C" fn destroy_handle(handle: *mut OpaqueHandle) {
unsafe {
let _ = Box::from_raw(handle as *mut RealData);
}
}
3. PinとUnpin
3.1 なぜPinが必要か
問題: 自己参照構造体はRustで表現できない
struct SelfRef {
data: String,
ptr: &String, // エラー!dataへの参照
}
// コンパイルエラー:
// error[E0106]: missing lifetime specifier
なぜ問題?
移動すると参照が無効に:
┌─────────────────┐
│ SelfRef │
│ ┌─────────────┐ │
│ │ data: "hi" │ │
│ └─────────────┘ │
│ ↑ │
│ │ │
│ ┌────┴────────┐│
│ │ ptr: &data ││
│ └─────────────┘│
└─────────────────┘
↓ move
┌─────────────────┐
│ SelfRef │
│ ┌─────────────┐ │ ptr はまだ古いアドレスを指している!
│ │ data: "hi" │ │ → ダングリングポインタ
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ ptr: &??? │ │
│ └─────────────┘ │
└─────────────────┘
3.2 Pinの導入
Pinは、メモリ上で値を固定します。
use std::pin::Pin;
// Pin<Box<T>>: ヒープ上で固定
let pinned = Box::pin(value);
Pin の保証:
┌─────────────────────────────────────────────────────────┐
│ Pin<P<T>> の保証 │
├─────────────────────────────────────────────────────────┤
│ │
│ Tが!Unpinなら: │
│ - 値はメモリ上で移動しない │
│ - ピン留めされた参照からしかアクセスできない │
│ - ムーブはコンパイルエラー │
│ │
│ TがUnpinなら: │
│ - 制約なし(ほとんどの型) │
│ - Pin<P<T>>とP<T>は等価 │
│ │
└─────────────────────────────────────────────────────────┘
3.3 UnpinとMarker Trait
Unpin: 自動実装されるマーカートレイト
// ほとんどの型は自動的にUnpin
impl Unpin for i32 {}
impl<T: Unpin> Unpin for Vec<T> {}
impl<T: Unpin> Unpin for Box<T> {}
!Unpin(Unpinでない): オプトアウト
use std::marker::PhantomPinned;
struct NotUnpin {
data: String,
_pin: PhantomPinned, // Unpinをオプトアウト
}
3.4 自己参照構造体の実装
use std::pin::Pin;
use std::marker::PhantomPinned;
struct SelfRef {
data: String,
ptr: *const String, // 生ポインタを使用
_pin: PhantomPinned,
}
impl SelfRef {
fn new(data: String) -> Pin<Box<Self>> {
let mut boxed = Box::new(SelfRef {
data,
ptr: std::ptr::null(),
_pin: PhantomPinned,
});
// ポインタを設定
let data_ptr: *const String = &boxed.data;
unsafe {
let mut_ref = Pin::as_mut(&mut Pin::new_unchecked(&mut boxed));
Pin::get_unchecked_mut(mut_ref).ptr = data_ptr;
}
// ピン留めして返す
unsafe { Pin::new_unchecked(boxed) }
}
fn get_data(self: Pin<&Self>) -> &str {
&self.data
}
fn get_ptr_value(self: Pin<&Self>) -> &str {
unsafe { &*self.ptr }
}
}
fn main() {
let self_ref = SelfRef::new(String::from("hello"));
println!("data: {}", self_ref.as_ref().get_data());
println!("ptr: {}", self_ref.as_ref().get_ptr_value());
// これはコンパイルエラー
// let moved = *self_ref; // error: cannot move out of `Pin<Box<SelfRef>>`
}
重要な概念:
1. PhantomPinned:
- Unpinをオプトアウト
- 値の移動を防ぐ
2. Pin::new_unchecked:
- ピン留めの安全性を保証するのは自分
- unsafeでのみ使用
3. Pin::get_unchecked_mut:
- ピン留めされた値への可変参照を取得
- 移動しないことを保証する必要がある
3.5 Async/AwaitとPin
非同期プログラミングではPinが不可欠です:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
// Futureは自己参照構造体
struct MyFuture {
state: String,
ptr_to_state: *const String,
}
impl Future for MyFuture {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// selfはピン留めされている
// 安全に自己参照を扱える
Poll::Ready(())
}
}
なぜFutureにPinが必要?
async fn example() {
let data = String::from("hello");
let ptr = &data; // dataへの参照
other_async_fn().await; // ← ここで中断・再開の可能性
// awaitの前後でメモリ位置が変わると危険
println!("{}", ptr);
}
↓ コンパイラが生成
struct ExampleFuture {
data: String,
ptr: &String, // 自己参照!
state: State,
}
// Pinで固定することで安全に
4. メモリレイアウトとrepr
4.1 Rustのデフォルトレイアウト
Rustの構造体は、デフォルトで最適化されたレイアウトを持ちます:
struct Example {
a: u8, // 1バイト
b: u32, // 4バイト
c: u16, // 2バイト
}
// メモリレイアウトは未定義!
// コンパイラが最適化のために並び替える可能性
実際のレイアウト(一例):
┌───┬───────────────┬─────┬─┐
│ a │ padding (3) │ b │c│
│1B │ 3B │ 4B │2│
└───┴───────────────┴─────┴─┘
0 1 4 8 (オフセット)
合計: 10バイト(パディング含む)
4.2 repr(C)
C言語と同じレイアウトにします:
#[repr(C)]
struct Point {
x: i32,
y: i32,
}
レイアウト:
┌─────┬─────┐
│ x │ y │
│ 4B │ 4B │
└─────┴─────┘
0 4 8
合計: 8バイト
フィールドの順序が保証される
用途:
- FFIでC構造体とやり取り
- メモリマップドI/O
- バイナリフォーマットの解析
4.3 repr(transparent)
単一フィールドの構造体を、そのフィールドと同じレイアウトにします:
#[repr(transparent)]
struct Wrapper(u32);
// Wrapperとu32は同じメモリレイアウト
// FFIで型安全性を追加するのに便利
用途:
#[repr(transparent)]
struct FileDescriptor(i32);
extern "C" {
fn close(fd: i32) -> i32;
}
fn close_fd(fd: FileDescriptor) {
unsafe {
// Wrapperごと渡せる
close(fd.0);
}
}
4.4 repr(packed)
パディングを削除します:
#[repr(packed)]
struct Packed {
a: u8,
b: u32,
}
レイアウト:
┌───┬─────┐
│ a │ b │
│1B │ 4B │
└───┴─────┘
0 1 5
合計: 5バイト(パディングなし)
注意点:
- アライメント要件違反の可能性
- パフォーマンスが悪化する可能性
- 生ポインタでアクセスすべき
#[repr(packed)]
struct Packed {
a: u8,
b: u32,
}
let p = Packed { a: 1, b: 2 };
// これは危険!
// let r = &p.b; // warning: unaligned reference
// 正しい方法
let b = unsafe {
std::ptr::addr_of!(p.b).read_unaligned()
};
5. Variance(変性)
5.1 変性とは
変性は、ジェネリック型のライフタイムの関係性を定義します。
// 'aが'bより長生きする場合
'a: 'b
// &'a Tは&'b Tに代入できるか?
5.2 3種類の変性
┌─────────────────────────────────────────────────────────┐
│ 変性の種類 │
├─────────────────────────────────────────────────────────┤
│ │
│ 共変(Covariant) │
│ 'a: 'b なら T<'a> を T<'b> に代入可能 │
│ 例: &'a T, Box<T>, Vec<T> │
│ │
│ 反変(Contravariant) │
│ 'a: 'b なら T<'b> を T<'a> に代入可能(逆) │
│ 例: fn(&'a T)(関数の引数) │
│ │
│ 不変(Invariant) │
│ 代入不可 │
│ 例: &'a mut T, Cell<T> │
│ │
└─────────────────────────────────────────────────────────┘
5.3 共変の例
fn example<'a>(s: &'a str) -> &'a str {
s
}
fn main() {
let long_lived = String::from("long");
{
let short_lived = String::from("short");
// 'staticは全てのライフタイムより長い
let static_str: &'static str = "static";
// 共変: &'static strを&'aに代入可能
let result: &str = example(static_str);
}
}
5.4 不変の例(可変参照)
fn example<'a, 'b>(x: &'a mut i32, y: &'b mut i32) {
// これは不可能
// let z: &'a mut i32 = y; // エラー!
}
// なぜ不変?
// もし共変なら:
let mut x = 5;
{
let mut short = 10;
let long_ref: &'long mut i32 = &mut x;
let short_ref: &'short mut i32 = &mut short;
// もし共変なら、これが許可される
// let long_ref = short_ref; // 'short → 'long
// short_refがドロップされた後もlong_refが使える
// → ダングリングポインタ!
}
6. Real-World Case Studies
6.1 Mozilla Firefox - Stylo
背景: FirefoxのスタイルエンジンをRustで書き直し
所有権の活用:
// CSSスタイルシートの並列処理
struct StyleSheet {
rules: Vec<Rule>,
}
impl StyleSheet {
fn parallel_process(&self) {
self.rules.par_iter() // Rayon
.map(|rule| process_rule(rule))
.collect()
}
}
// 不変参照を複数スレッドで共有
// データ競合は所有権システムで防止
成果:
- パフォーマンス向上(最大30%)
- セキュリティバグの減少
- メモリ使用量の削減
6.2 Cloudflare - Pingora
背景: NginxからRustベースのProxyに移行
FFIの活用:
// OpenSSLとのFFI
extern "C" {
fn SSL_read(ssl: *mut SSL, buf: *mut c_void, num: c_int) -> c_int;
fn SSL_write(ssl: *mut SSL, buf: *const c_void, num: c_int) -> c_int;
}
// Rust側で安全なラッパー
pub struct SslStream {
ssl: *mut SSL,
}
impl SslStream {
pub fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let ret = unsafe {
SSL_read(self.ssl, buf.as_mut_ptr() as *mut c_void, buf.len() as c_int)
};
if ret < 0 {
Err(io::Error::last_os_error())
} else {
Ok(ret as usize)
}
}
}
成果:
- メモリ使用量70%削減
- CPU使用率削減
- セキュリティの向上
- 最小化: unsafeブロックは可能な限り小さく
- カプセル化: unsafeをsafe APIでラップ
- 文書化: 不変条件を明確に記述
- テスト: 徹底的にテスト(Miriツールの使用)
- 自己参照構造体の安全な実装
- Async/Awaitの基盤
- ゼロコスト抽象化
#[repr(C)]で明示的なレイアウト- 所有権の境界を明確に
- エラーハンドリングを忘れずに
- Opaqueポインタパターンを使用
- システムプログラミング
- 組み込み開発
- ハイパフォーマンスコンピューティング
- 非同期プログラミング
- The Rustonomicon
まとめ
Unsafe Rustの原則
Pinの重要性
FFIのベストプラクティス
次のステップ
この章で学んだ高度な概念は、以下の分野で活用されます:
---
参考文献
- Pin and Suffering
- The Rust FFI Omnibus
- Variance in Rust
- Oxidizing Firefox