LabHub

ブログ

Rustとシステムプログラミングの帰還 完全攻略 — Ownership, Borrowing, Lifetime, Trait, Async, Tokio, Cargo, WebAssembly (2025)

한국어English日本語

"In Rust, we don't trust the programmer. We trust the compiler — and the programmer, trained by the compiler, becomes trustworthy." — Niko Matsakis (Rust core team)

2024年2月、ホワイトハウスがC/C++からメモリ安全言語への移行を公式に推奨。2025年2月、LinuxカーネルがRustドライバをメインツリーに統合。Stack Overflow「最も愛される言語」9年連続1位。これはファンダムではなく、ソフトウェア産業全体の構造的転換が進んでいる証だ。

「難しい」という評判にもかかわらず、なぜRustはこれほど影響力を持つのか。本稿ではRustの誕生から2025年のエコシステムまで、「Rustを理解する」とは何かを解説する。


1. Rustの誕生 — Mozillaの贈り物

Graydon Hoareの個人プロジェクト (2006)

Mozilla公式プロジェクト (2010)

Servo → Firefox Quantum (2017)

財団独立 (2021)

2025 — 遍在


2. Ownership — GCなしのメモリ安全

問題設定

Rustの3原則

  1. 各値は正確に1つのownerを持つ
  2. ownerがスコープを抜けると値はdropされる
  3. ownerが変われば以前のownerは使えない

基本例

fn main() {
    let s = String::from("hello");
    let t = s;  // sの所有権がtに移動
    // println!("{}", s);  // エラー! sはもう無効
    println!("{}", t);  // OK
}  // tがスコープを抜ける -> drop

Copy vs Move

let x = 5;
let y = x;  // Copy
println!("{} {}", x, y);  // OK

let s1 = String::from("hi");
let s2 = s1;  // Move
// println!("{}", s1);  // エラー!

Clone — 明示的コピー

let s1 = String::from("hi");
let s2 = s1.clone();  // ヒープまで複製
println!("{} {}", s1, s2);  // OK

.clone()を書くことでコストがコード上に見える。暗黙の性能コストは発生しない。


3. Borrowing — 借用の芸術

なぜ必要か

fn print_len(s: String) { println!("{}", s.len()) }

let s = String::from("hi");
print_len(s);  // 所有権が移動
// sはここで使えない

不便なので参照(&)が導入された。

Immutable Borrow

fn print_len(s: &String) { println!("{}", s.len()) }

let s = String::from("hi");
print_len(&s);
println!("{}", s);  // OK、所有権保持

Mutable Borrow

fn add_world(s: &mut String) {
    s.push_str(" world")
}

let mut s = String::from("hi");
add_world(&mut s);
println!("{}", s);  // "hi world"

中核ルール — 「共有xor可変」

ある瞬間に:

let mut s = String::from("hi");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;  // エラー! 既にimmutable borrow存在

なぜこのルールか


4. Lifetime — 参照の有効期間

問題

fn dangling() -> &String {
    let s = String::from("hi");
    &s
}  // sがdropされると&sはdangling

Cではセグフォ、Rustではコンパイルエラー

Lifetimeアノテーション

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

'aは関数内で共通のLifetime。戻り値はxとyの短い方のLifetimeを持つ。

Lifetime Elision — 省略規則

多くは明示不要。

fn first_word(s: &str) -> &str { /* ... */ }
// 内部: fn first_word<'a>(s: &'a str) -> &'a str

3つの規則:

  1. 各参照パラメータは自分のLifetimeを持つ
  2. パラメータが1つならreturnのLifetime = そのパラメータ
  3. &selfがある場合、returnのLifetime = selfのLifetime

'static

let s: &'static str = "hello";  // 文字列リテラル

プログラム全期間有効。濫用されがち。

Lifetimeが難しい理由


5. Trait System — Rustの中核抽象化

Trait = Interface + Mixin + Typeclass

trait Greet {
    fn hello(&self) -> String;
    fn goodbye(&self) -> String {  // デフォルト実装
        String::from("bye")
    }
}

struct Kim { name: String }

impl Greet for Kim {
    fn hello(&self) -> String {
        format!("こんにちは、私は{}", self.name)
    }
}

Genericとtrait bounds

fn greet_all<T: Greet>(people: &[T]) {
    for p in people {
        println!("{}", p.hello());
    }
}

T: Greetは「TはGreetを実装する任意の型」。Java genericsの上限境界と似るが更に強力。

impl Trait (2018)

fn make_greeter() -> impl Greet {
    Kim { name: String::from("金") }
}

「何らかのGreet実装を返す」 — 具体型を隠蔽。

Trait Object — dyn Trait

fn greet_all(people: &[Box<dyn Greet>]) {
    for p in people {
        println!("{}", p.hello());
    }
}

vtableベースの動的ディスパッチ。C++仮想関数に似るが、dynキーワードでコストが明示される。

Orphan Rule

traitか型のいずれかが現在のcrate所有であることがimplの条件。複数crateからの競合実装を防ぐ。

有名なTrait


6. Zero-cost Abstractions — Bjarneの約束、Rustの実現

哲学

"What you don't use, you don't pay for. What you do use, you couldn't hand-code better." — Bjarne Stroustrup

例1 — Iterator

let sum: i32 = (1..=100)
    .filter(|n| n % 2 == 0)
    .map(|n| n * 2)
    .sum();

高レベル関数型チェインもコンパイル後のアセンブリは手書きforループと同一。LLVM最適化が抽象化を完全に除去。

例2 — Option<T>

let x: Option<i32> = Some(5);
let y = x.unwrap_or(0);

Cのnullチェックより安全だがランタイムコストは0。Option自体がtagged unionにコンパイルされる。

例3 — Generic vs dyn Trait

// Generic — コンパイル時monomorphization
fn static_greet<T: Greet>(x: T) { /* ... */ }  // インライン可

// Trait object — ランタイムvtable
fn dynamic_greet(x: &dyn Greet) { /* ... */ }  // ポインタ参照

開発者がコストを選択する。


7. Error Handling — panicではなくResult

Result<T, E>

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("zero division"))
    } else {
        Ok(a / b)
    }
}

match divide(10, 2) {
    Ok(v) => println!("{}", v),
    Err(e) => eprintln!("{}", e),
}

?演算子 — エラー伝播

fn do_stuff() -> Result<(), MyError> {
    let data = read_file("x.txt")?;
    let parsed = parse(&data)?;
    save(parsed)?;
    Ok(())
}

try/catchなしでもエラー伝播が綺麗。呼び出し側がResultを返す必要がある。

panic! — 異常時のみ

thiserroranyhow

use anyhow::{Result, Context};

fn load_config() -> Result<Config> {
    let s = std::fs::read_to_string("config.yml")
        .context("config.yml 読み込み失敗")?;
    Ok(serde_yaml::from_str(&s)?)
}

8. Async/AwaitとTokio

async/awaitの非同期

async fn fetch_user(id: u64) -> Result<User> {
    let resp = reqwest::get(format!("/users/{}", id)).await?;
    Ok(resp.json().await?)
}

#[tokio::main]
async fn main() {
    let user = fetch_user(1).await.unwrap();
    println!("{:?}", user);
}

asyncはFutureを作る

async fnを呼ぶだけでは何も起こらない — Futureを返すだけ。ランタイムがpollして初めて実行される。

ランタイム

Tokioの強み

JSのasync/awaitとの違い

観点JSRust
デフォルト実行即開始 (Promise)awaitするまで動かない
ランタイムビルトインライブラリ
Promise<T>impl Future<Output=T>
キャンセル暗黙的で難しいdropで自動
性能V8チューニング限界ネイティブ

Async Trait — 2024年ついに

長らくasync fn in traitはワークアラウンドが必要だった。Rust 1.75 (2023年12月)から安定化が始まり2024-2025で完成。


9. Cargo — パッケージマネージャの理想形

一つのツールで全部

cargo new myapp          # 新規プロジェクト
cargo build              # ビルド
cargo run                # 実行
cargo test               # テスト
cargo bench              # ベンチマーク
cargo doc                # ドキュメント生成
cargo fmt                # フォーマット (rustfmt)
cargo clippy             # リント
cargo publish            # crates.io公開

依存管理、ビルド、テスト、ドキュメント、ベンチが標準コマンド1つずつ。Python/Node生態系の断片化(pip/poetry, npm/yarn/pnpm)と対照的。

Cargo.toml

[package]
name = "myapp"
version = "0.1.0"
edition = "2024"

[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"

[dev-dependencies]
criterion = "0.5"

Features — 選択的機能

[features]
default = ["tokio"]
tokio = ["dep:tokio"]
async-std = ["dep:async-std"]

同一crateで複数ランタイムをサポート等。条件コンパイルと組み合わせ。

Workspace — モノレポ標準対応

[workspace]
members = ["crates/*"]

複数crateを1リポジトリで管理、依存バージョン統一。

crates.io


10. 有名なRustプロジェクト 2025

インフラ

コンテナ/クラウド

フロントエンドツール

ランタイム/DB

Web/サーバ


11. Rustで書くフロントエンド — WebAssembly

wasm-bindgen

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("こんにちは、{}!", name)
}

JSからgreet("Kim")で呼び出せる。

Leptos — Solid.jsスタイル

#[component]
fn Counter() -> impl IntoView {
    let (n, set_n) = create_signal(0);
    view! {
        <button on:click=move |_| set_n.update(|v| *v += 1)>
            "カウント: " {n}
        </button>
    }
}

Dioxus — Reactスタイル

fn Counter(cx: Scope) -> Element {
    let n = use_state(cx, || 0);
    render! {
        button { onclick: move |_| n.set(n + 1), "{n}" }
    }
}

Yew

現実


12. Rust for Linux — 2025年の勝利

長い道のり

インパクト

摩擦

Asahi Linuxチーム


13. 政府と規制 — Rustの義務化

ホワイトハウスONCDレポート (2024年2月)

"Memory-safe languages, including Rust, Go, C#, Java, Swift, Python, and JavaScript, offer...the most comprehensive approach to eliminate an entire class of vulnerabilities."

国防総省、NASA、航空

Ferrocene — 認証済みRustコンパイラ

2025現在


14. Rustを学ぶ最短ルート

ステージ1 (1-2週間)

  1. The Rust Book (無料、公式)を完読
  2. rustlingsの課題100個
  3. 小さなCLI作成 (argh/clap)

ステージ2 (2-4週間)

  1. OwnershipとBorrowingに慣れる
  2. Result?を体得
  3. Iteratorチェインを習熟
  4. Trait設計を練習

ステージ3 (1-3か月)

  1. Async/Tokioでネットワークアプリ
  2. serdeでJSON/YAML
  3. エラー型設計 (thiserror/anyhow)
  4. 小規模実戦プロジェクトをデプロイ

ステージ4 (3か月以上)

  1. unsafeを理解
  2. マクロ (宣言的/手続き的)
  3. 高度なLifetime
  4. FFI (Cとの連携)
  5. オープンソースcrateに貢献

無料リソース


15. アンチパターン TOP 10

  1. .clone()濫用 — コンパイルは通るが性能浪費
  2. .unwrap()乱用 — 本番でpanic
  3. Rc<RefCell<T>>で借用規則回避 — Rustの長所を放棄
  4. 何でもasync — CPUバウンドには不要
  5. Lifetime過剰明示 — elisionで大半は処理される
  6. dyn Traitを性能考慮なしに — genericの方が速いことが多い
  7. マクロ濫用 — コンパイル時間爆発、デバッグ地獄
  8. Box<dyn Error>濫用 — 細やかなエラー型を放棄
  9. unsafe不要なのにunsafe — 安全性放棄
  10. Rustスタイルでない他言語のパラダイム — snake_case、trait中心を守る

16. Rustを賢く使うチェックリスト


おわりに — 「システムプログラミングのルネサンス」

30年間、システムプログラミングはC/C++の独占だった。誰もこの独占が崩れるとは信じていなかった。2015年にRust 1.0が出た時も、多くの専門家は「物好きの話」と片付けた。

10年が経った今、Linuxカーネルが、ホワイトハウスが、AzureとAWSがRustへ移行している。これは革命というより世代交代に近い。Cは消えないが、新しいシステムコードは次第にRustで生まれる

Rustが本当に贈ってくれるのは速度ではない。それは「コンパイラと協調して正しいプログラムを書く経験」である。borrow checkerと格闘するうちに、メモリ、並行性、生存期間に対する直観が得られる。その直観は他の言語に戻っても、あなたと共にある。

"Writing Rust is sometimes like being shouted at by an expert compiler. When you finally agree with it, your code is bulletproof." — Amos Wenger (fasterthanli.me)


"Rust is for when you need to be fast. Go is for when you need to be done. Python is for when you need both. TypeScript is for when you had a Python and want to sleep at night." — Anonymous HN comment (2024)

コメント

まだコメントはありません。

ログインするとコメントできます