Rust

メモリ安全性とパフォーマンスを両立する、GC なしのシステムプログラミング言語

低レベルメモリ管理
Rust」の技術書を見る →

Rust とは

Rust は、Mozilla の支援のもとで開発が始まったシステムプログラミング言語で、所有権システムによりガベージコレクション (GC) なしでメモリ安全性を保証する。2010 年に公開され、2015 年 5 月 15 日に 1.0 に到達した。公式リリース告知はこの 1.0 を安定性への取り組みの正式な出発点と位置づけており、以降は破壊的変更を原則として行わない方針が続いている。2026 年 8 月時点の安定版は 1.97 系。

保証が効くのは安全な Rust (unsafe を使わない範囲) で、解放後参照やデータ競合はコンパイル時に弾かれる。一方でメモリリークを完全に防ぐことは保証に含まれない。参照カウントの循環で値が破棄されなくなる例があり、規則の詳細とこの落とし穴は 所有権と借用 にまとめている。

所有権システム

値を破棄する責任を持つ変数を 1 つに限定するのが所有権で、コンパイラはその変数がスコープを抜ける位置に解放処理を埋め込む。実行時に生存状況を追跡する機構が要らないため、既定では GC も参照カウントも動かない。次の例では代入で所有権が移る。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;        // s1 の所有権が s2 に移動 (ムーブ)
    // println!("{}", s1); // ❌ コンパイルエラー: s1 はもう使えない
    println!("{}", s2);   // ✅ OK
}

公式ドキュメントが挙げる規則は 3 つある。

  1. 各値には所有者と呼ばれる変数がある
  2. 所有者は同時に 1 つだけ
  3. 所有者がスコープを抜けると値は破棄される

移動 (ムーブ) はこの 3 つから導かれる帰結で、規則そのものには数えられていない。所有者が 1 つに限られるため、代入や関数呼び出しで所有権が移った側は元の変数を使えなくなる。ただし整数のようにスタックだけで完結し Copy を実装する型は代入で複製されるので、ムーブは起きない。

借用 (Borrowing)

所有権を渡さずに値を読みたいときは参照を渡す。これが借用で、不変参照は同時に何本でも作れるが、可変参照は同じ範囲に 1 本しか作れない。この非対称な制約がデータ競合をコンパイル時に排除する仕組みになっている。

fn print_length(s: &String) {  // 不変参照 (借用)
    println!("Length: {}", s.len());
}

fn main() {
    let s = String::from("hello");
    print_length(&s);  // 借用: 所有権は移動しない
    println!("{}", s);  // ✅ まだ使える
}

エディション (edition)

言語に破壊的な変更を入れたいとき、Rust はエディションという仕組みで切り替える。2015 / 2018 / 2021 / 2024 の 4 つがあり、2024 エディションは 1.85.0 で導入された。エディションはクレート単位の任意選択で、Cargo.toml に明示して移行しない限り既存のコードは古いエディションのまま動き続ける。移行の時期もクレートごとに独立して決められる。

肝は、異なるエディションでコンパイルされたクレート同士が問題なく相互運用できると公式ガイドが定めている点だ。依存ライブラリが 2015 エディションのままでも、自分のクレートだけ 2024 に上げられる。「言語の世代が変わったら依存ごと全部書き直し」が起きない設計になっている。

Go / TypeScript との比較

選定で迷いやすい 3 つを、実務で効いてくる観点で並べる。

観点RustGoTypeScript
メモリ管理所有権 (GC なし)GCGC (実行環境側)
実行形態ネイティブコード (GC の停止時間なし)ネイティブコード (GC あり)実行環境上で JIT
メモリ安全性の検査安全な Rust の範囲でコンパイル時実行時 (境界検査など)実行時
学習コスト高い (借用規則が壁)低い中程度
ビルド時間遅い速い速い (トランスパイル)
用途CLIWebAssembly、組み込みWeb サーバー、CLIWeb アプリ

判断の目安は単純で、停止時間の予測可能性か資源効率が要件になる面 (組み込み、エッジ、大量のリクエストをさばくプロキシ) では Rust が効き、それ以外の業務ロジックでは開発速度で GC を持つ側が勝ちやすい。学習コストの差は個人の資質ではなく、借用規則をチームで共有できるかという運用の問題として見積もる。

Rust で書かれたツール

言語そのものより先に、Rust で書き直された開発ツールに触れる人も多い。

ツール用途置き換え対象
SWCTypeScript トランスパイラBabel (公式は単一スレッドで 20 倍と表示)
RuffPython の linter・フォーマッターFlake8 / Black
TurbopackNext.js のバンドラーwebpack
BiomeLinter + FormatterESLint + Prettier
ripgrep (rg)テキスト検索grep
fdファイル検索find

開発ツールで Rust が選ばれる理由は速さの数字よりも構造にある。GC の停止時間がないので入力ごとに走るツールの応答が安定し、所有権の検査があるためマルチコアでの並列処理を後から入れやすく、依存関係を含めた 1 つの実行ファイルとして配布できる。ただし高速なツールが Rust 製に限るわけではなく、同じ領域には Go で書かれた esbuild のような実装もある。置き換え対象と書いた道具がすぐ消えるわけでもない。既存のプラグイン資産を引き継げるかが実際の乗り換え判断を決める。

Lambda での Rust

Rust はネイティブコードにコンパイルされるため、Lambda 側に Rust 専用のマネージドランタイムは用意されていない。公式ドキュメントの案内どおり、Rust 用のランタイムクライアントを組み込んだ実行ファイルをビルドし、OS だけを提供するランタイム (provided.al2023 など) にデプロイする。OS のパッチ適用は Lambda 側が続ける。

起動時に仮想マシンや処理系の初期化を待たないので、コールドスタートは GC を持つ言語より短くなりやすい。ただし実際の時間はパッケージの大きさ・初期化処理・VPC 接続の有無で変わるため、要件があるなら自分の関数で計測してから設計に織り込む。

use lambda_runtime::{service_fn, LambdaEvent, Error};
use serde_json::Value;

async fn handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
    Ok(serde_json::json!({ "message": "Hello from Rust" }))
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    lambda_runtime::run(service_fn(handler)).await
}

WebAssembly (Wasm)

Rust は wasm32-unknown-unknown を標準のコンパイルターゲットとして持ち、別の処理系を用意せずに WebAssembly を出力できる。JavaScript との値の受け渡しは wasm-bindgen 系のツールが引き受ける。用途は、ブラウザで重い計算 (画像処理、暗号化、独自形式のパーサー) を動かす面と、実行環境を選ばない配布形式が欲しい面だ。

一方、DOM 操作が中心の画面をまるごと Wasm に移しても速くはならない。JavaScript との境界を越える回数そのものがコストになるため、計算が固まっている部分だけを切り出すのが定石になる。

学習の順序には向きがある。借用規則の理屈から入るより、VecString を使う小さなプログラムを書き切り、コンパイラのエラーメッセージを読む練習をしてから所有権に戻ると早い。Rust のエラーは問題の位置と修正候補まで示すので、コンパイラと対話しながら規則を覚える言語だと考えてよい。

この記事は役に立ちましたか?

関連用語

関連する記事