Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust 実践開発ガイド アウトライン

Rust は、ガベージコレクション(GC)を持たずにコンパイル時にメモリ安全性を保証する、新世代のシステムプログラミング言語です。高い実行性能と並行処理の安全性を両立しており、Web サーバー、CLI ツール、組み込みシステム、そしてデスクトップアプリケーションなど多様な分野で活用されています。

本書では、Rust の基本的な文法や設計思想である「所有権・借用」の仕組みを基礎から丁寧に学びます。さらに実施編として、引数解析クレート clap を用いた実用的な CLI アプリケーションの作成と、フロントエンド技術(Vite + TypeScript)と Rust を組み合わせたデスクトップ Web アプリケーション基盤「Tauri」による開発手法をハンズオン形式で解説します。

章構成

  • 1. Rust の特徴と開発環境の構築

    Rust の誕生背景、メモリ安全性とゼロコスト抽象化という設計原則、公式ツールチェーン管理ツール rustup によるインストール手順、およびビルドツール・パッケージマネージャーである cargo の基本操作を学びます。

  • 2. 基本文法と型システム

    変数宣言と可変性(mut)、静的型付けに基づく基本データ型(スカラー型・複合型)、関数定義、制御構造(if、loop、while、for)、そして構造体(struct)と列挙型(enum)およびパターンマッチング(match)の基礎を整理します。

  • 3. 所有権・借用とライフタイム

    Rust 最大の特徴である所有権(Ownership)の 3 つの原則、値の移動(ムーブセマンティクス)と複製(Clone/Copy)、参照と借用(不変参照 & と可変参照 &mut)、データ競合を防ぐ借用規則、スライス型、および参照の有効期間を表すライフタイムの基礎概念を理解します。

  • 4. トレイトとエラーハンドリング

    共通の振る舞いを定義するトレイト(Trait)の基本とジェネリクス、自動実装を可能にする derive 属性、Rust 特有のエラーハンドリング思想(回復不能エラーと回復可能エラー)、Option<T> 型と Result<T, E> 型の活用、そして ? 演算子による安全な早期リターンを学びます。

  • 5. 実践:CLI ツールの開発

    コマンドライン引数解析ライブラリ clap(Derive 構文)を用いた実用的なファイル検索 CLI ツールを作成します。エラーハンドリングライブラリ anyhow の活用、ファイル入出力処理、カラー表示、ユニットテストの作成、および最適化されたリリースバイナリのビルドまでの一連の流れを実践します。

  • 6. 実践:Tauri + Vite によるデスクトップアプリ開発

    Web 技術と Rust を組み合わせた軽量デスクトップアプリフレームワーク Tauri と Vite(React / TypeScript)によるプロジェクト構築を行います。Rust バックエンドとフロントエンドの連携(Tauri コマンドの定義と JavaScript からの呼び出し)、イベント通知、安全なファイルアクセス、そしてデスクトップアプリのビルドと配布準備を実践します。

  • 7. まとめとエコシステム

    本書で学んだ内容の要点を振り返り、パッケージリポジトリ crates.io の活用法、静的コード解析ツール clippy やフォーマッター rustfmt、依存関係の脆弱性検査ツール、および今後の学習に役立つ公式ドキュメントとリソースを紹介します。

第1章 Rust の特徴と開発環境の構築

Rust(ラスト)は、信頼性と処理速度を極めて高い次元で両立させた現代的なシステムプログラミング言語です。

長年、C 言語や C++ が担ってきた OS、ブラウザエンジン、ゲームエンジン、ネットワーク基盤などの高速な領域から、CLI ツールや Web サーバー、デスクトップアプリまで、幅広い分野で採用が進んでいます。本章では、Rust が注目される理由と、手元の環境ですぐに開発を始めるためのセットアップ手順を学びます。

1.1 なぜ Rust なのか?3つの大きな特徴

Rust が多くの開発者から支持される背景には、主に 3 つの設計思想があります。

1. ガベージコレクション(GC)なしのメモリ安全性

Java や Go、Python などの言語では、プログラムの実行中に不要になったメモリを自動で掃除する「ガベージコレクション(GC)」が動作します。GC は便利ですが、掃除のためにプログラムの実行が一瞬止まる(GC ポーズ)ことがあります。

一方、従来の C/C++ では開発者が手動でメモリの確保と解放を管理していたため、解放忘れによるメモリリークや、すでに解放されたメモリへの不正アクセス(ダングリングポインタ)といった重大なバグやセキュリティ脆弱性が頻発していました。

Rust は「所有権(Ownership)」という独自の仕組みにより、プログラムの実行時ではなく「コンパイル時(ビルド時)」にメモリの安全性を検証します。そのため、GC なしで C/C++ と同等に高速に動きながら、メモリに起因するクラッシュやセキュリティ欠陥をゼロにします。

2. ゼロコスト抽象化

Rust は、高水準な抽象化(オブジェクト指向的な構造体、パターンマッチ、イテレータなど)を提供しながらも、それらが実行時の速度低下を引き起こさないように設計されています。

これを「ゼロコスト抽象化(Zero-cost abstractions)」と呼びます。開発者が読みやすくきれいに書いた抽象的なコードは、コンパイラによって最も効率的な機械語へと最適化されます。

3. データ競合のない並行処理

現代の CPU は複数のコアを持っています。複数の処理を同時に動かす並行プログラミングでは、同じデータに対して複数の処理が同時に書き込みを行うことで起きる「データ競合(Data Race)」が最も解決しにくいバグの一つでした。

Rust では、型システムと所有権のルールによって、データ競合が発生する可能性のあるコードをコンパイルの段階で確実に検出してエラーにします。これにより、安全かつ高速なマルチスレッド処理を安心して記述できます。

1.2 開発環境のセットアップ

Rust のツールチェーンは、公式のインストーラーである rustup を使用して導入・管理します。

インストール手順

OS に応じて以下のコマンドを実行します。

Linux や macOS では、ターミナルで以下のシェルスクリプトを実行します。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Windows では、公式サイト(https://www.rust-lang.org/tools/install)からインストーラー(rustup-init.exe)をダウンロードして実行します。また、Windows でのビルドには「C++ Build Tools」(Visual Studio Installer から入手可能)が必要です。

インストールが完了したら、新しいターミナルを開き、正しく導入されたか確認します。

rustc --version
cargo --version

rustc(Rust コンパイラ)と cargo(ビルドツール兼パッケージマネージャー)のバージョン番号が表示されれば、環境構築は完了です。

1.3 初めてのプロジェクト作成と Cargo

Rust では、コードのコンパイル、外部ライブラリ(クレート)の管理、テストの実行などをすべて cargo コマンドで一元管理します。

プロジェクトの新規作成

以下のコマンドで、新しいバイナリ(実行可能プログラム)プロジェクトを作成します。

cargo new hello_world

生成されたディレクトリ構造は以下のとおりです。

hello_world/
├── Cargo.toml
└── src/
    └── main.rs

プロジェクトの設定ファイル(Cargo.toml)

Cargo.toml はプロジェクトのメタ情報と依存関係を記述する設定ファイルです。

[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"

[dependencies]

ソースコード(src/main.rs)

自動生成された src/main.rs を確認します。

fn main() {
    println!("Hello, world!");
}

Rust プログラムの実行は常に main 関数から始まります。末尾に ! が付いている println! は関数ではなく、コンパイル時にコードを展開する「マクロ(Macro)」です。

頻出する Cargo コマンド

開発中に日常的に使用する基本コマンドは以下の 4 つです。

# プロジェクトをビルドして即座に実行する
cargo run

# コードに文法エラーや型エラーがないかを高速に検証する(バイナリを生成しないため超高速)
cargo check

# 開発用(デバッグ情報付き)のバイナリをビルドする(target/debug/ に出力)
cargo build

# 最適化を施した本番用(リリース用)の高速バイナリをビルドする(target/release/ に出力)
cargo build --release

日常の開発では、コードを書くたびに cargo check を走らせてコンパイラによるチェックを受け、動作確認の際に cargo run を実行するのが標準的な開発サイクルです。

第2章 基本文法と型システム

Rust は静的型付け言語であり、すべての変数や値の型がコンパイル時に確定します。

型推論が非常に強力であるため、多くの場合で型の明示を省略しながらも、厳格な型安全性を享受できます。本章では、Rust の基本文法、変数と可変性、基本データ型、制御フロー、そしてデータ設計の中心となる構造体と列挙型を学びます。

2.1 変数宣言と可変性

Rust の変数は、デフォルトで 不変(Immutable) です。一度値を束縛すると、後から変更できません。

不変変数と可変変数(mut)

値を変更したい場合は、明示的に mut キーワードを付与します。

fn main() {
    let x = 5;
    // x = 6; // コンパイルエラー:不変変数には再代入できない

    let mut y = 10;
    println!("変更前: {y}");
    y = 20; // OK:可変変数なので変更可能
    println!("変更後: {y}");
}

意図しない値の書き換えを防ぐため、可能な限り不変(let)を使い、値の変更がどうしても必要な場合のみ mut を付与するのが Rust の基本設計です。

定数(const)とシャドーイング

定数は const キーワードで宣言し、常に型注釈が必要です。また、プログラムの実行期間中ずっと有効で、計算結果などを束縛することはできません。

#![allow(unused)]
fn main() {
const MAX_USERS: u32 = 100_000;
}

同じスコープ内で同じ名前の変数を再宣言することを「シャドーイング(Shadowing)」と呼びます。元の値を変換して新しい型にしたいときなどに便利です。

#![allow(unused)]
fn main() {
let spaces = "   "; // 文字列型(&str)
let spaces = spaces.len(); // 数値型(usize)へ変換して再束縛
}

2.2 基本データ型

Rust の基本的なデータ型は、「スカラー型」と「複合型」の 2 つに分類されます。

スカラー型(単一の値を表す型)

  • 整数型: 符号付き(i8, i16, i32, i64, i128, isize)と符号なし(u8, u16, u32, u64, u128, usize)。デフォルトは i32 です。

  • 浮動小数点型: f32 と f64。デフォルトは f64 です。

  • 真偽値型: bool(true または false)。

  • 文字型: char(Unicode スカラー値を表し、4 バイトのサイズを持ちます。日本語や絵文字も 1 文字として扱えます)。

#![allow(unused)]
fn main() {
let age: u32 = 25;
let rate: f64 = 3.14;
let is_active: bool = true;
let icon: char = '🦀';
}

複合型(複数の値をまとめる型)

  • タプル: 異なる型の複数の値を固定長でまとめる型です。

  • 配列: 同じ型の複数の値を固定長でまとめる型です(可変長のリストには後述する Vec を使います)。

#![allow(unused)]
fn main() {
// タプルの宣言と要素へのアクセス
let person: (&str, u32) = ("Alice", 20);
println!("名前: {}, 年齢: {}", person.0, person.1);

// 配列の宣言(要素数 3、型は i32)
let scores: [i32; 3] = [80, 95, 70];
println!("先頭の点数: {}", scores[0]);
}

2.3 関数と制御フロー

関数と「文」と「式」

関数は fn キーワードで定義します。引数と戻り値には必ず型を明記します。

#![allow(unused)]
fn main() {
fn add(a: i32, b: i32) -> i32 {
    a + b // セミコロンを付けないことで「式」となり、関数の戻り値になる
}
}

Rust は 式指向(Expression-based) の言語です。文(Statement)は値を返さず末尾にセミコロン ; を付けます。一方、式(Expression)は値を評価して返します。ブロック {} の最後の式が、そのままそのブロックの評価値になります。

条件分岐(if 式)

if も式であるため、変数に直接結果を代入できます。

#![allow(unused)]
fn main() {
let number = 7;
let message = if number % 2 == 0 {
    "偶数です"
} else {
    "奇数です"
};
println!("{message}");
}

繰り返し(loop, while, for)

#![allow(unused)]
fn main() {
// 無限ループ(break で値を返すことも可能)
let mut count = 0;
let result = loop {
    count += 1;
    if count == 10 {
        break count * 2;
    }
};

// while ループ
let mut n = 3;
while n > 0 {
    n -= 1;
}

// for ループ(イテレータや範囲指定を走査する最も安全で高速な繰り返し)
for i in 1..=5 {
    println!("カウント: {i}");
}
}

2.4 構造体(struct)とメソッド

複数の関連するデータを 1 つの名前付きグループにまとめるには構造体を使います。

// 構造体の定義
struct User {
    username: String,
    email: String,
    active: bool,
}

// メソッドと関連関数の定義
impl User {
    // 関連関数(コンストラクタ的な生成処理。self を取らない)
    fn new(username: &str, email: &str) -> Self {
        Self {
            username: username.to_string(),
            email: email.to_string(),
            active: true,
        }
    }

    // メソッド(インスタンスを参照する &self を取る)
    fn deactivate(&mut self) {
        self.active = false;
    }
}

fn main() {
    let mut user = User::new("taro", "taro@example.com");
    println!("ユーザー名: {}", user.username);
    user.deactivate();
}

2.5 列挙型(enum)とパターンマッチング

Rust の enum は、各バリアントに異なるデータを持たせることができる非常に表現力の高い型です。

#![allow(unused)]
fn main() {
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
}
}

match 式による完全な網羅チェック

match 式を使うと、すべてのパターンを漏れなく処理できます。未処理のケースがあるとコンパイルエラーになります。

#![allow(unused)]
fn main() {
fn handle_message(msg: Message) {
    match msg {
        Message::Quit => {
            println!("終了イベントです");
        }
        Message::Move { x, y } => {
            println!("移動: x={x}, y={y}");
        }
        Message::Write(text) => {
            println!("テキスト出力: {text}");
        }
    }
}
}

if let 式

特定の 1 つのパターンのみに関心がある場合は、if let 構文で簡潔に記述できます。

#![allow(unused)]
fn main() {
let msg = Message::Write("こんにちは".to_string());
if let Message::Write(text) = msg {
    println!("メッセージを受信: {text}");
}
}

第3章 所有権・借用とライフタイム

所有権(Ownership)は、Rust の最も独創的で重要な概念です。

GC(ガベージコレクタ)に頼らず、かつ手動解放のミスを起こさずにメモリ安全性を達成するための核心となる仕組みです。本章では、所有権のルール、値の移動(ムーブ)、参照と借用、スライス型、そしてライフタイムの基礎を順を追って理解します。

3.1 所有権の 3 つのルールとムーブ

プログラムが実行時に利用するメモリには、「スタック」と「ヒープ」があります。サイズが固定のデータ(整数や真偽値など)は高速なスタックに置かれますが、サイズが可変のデータ(動的な文字列 String や配列 Vec など)はヒープに領域を確保します。

Rust では、以下の 3 つの所有権ルール によってヒープメモリを自動的かつ安全に管理します。

  1. Rust の各値には、「所有者(Owner)」と呼ばれる変数が存在する。
  2. いかなる時点においても、所有者は常に 1 人(1 つの変数)だけである。
  3. 所有者がスコープ({})から外れたとき、その値は自動的にメモリから破棄(ドロップ)される。

所有権の移動(ムーブセマンティクス)

ヒープを保持する String 型の変数を別の変数へ代入すると、データの「所有権」が移動(ムーブ)します。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // 所有権が s1 から s2 へ移動する

    // println!("{s1}"); // コンパイルエラー:s1 はすでに無効
    println!("{s2}"); // OK
}

代入後、元の変数 s1 は無効化されるため、同じメモリを二重に解放してしまう事故(ダブルフリー)がコンパイル時に根本から防がれます。

データの複製(Clone)とスタック上のコピー(Copy)

ヒープ領域のデータそのものを深く複製(ディープコピー)したい場合は、明示的に .clone() を呼び出します。

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // ヒープデータを複製
println!("s1: {s1}, s2: {s2}"); // 両方有効
}

一方、整数や真偽値などのサイズが固定されたスカラー型は、高速にビット単位で複製できるため、Copy トレイトが実装されています。これらの型は代入時にもムーブせず、自動的に値がコピーされます。

3.2 参照と借用

所有権を渡さずに、値の読み取りや変更の権限だけを一時的に貸し出す仕組みを「借用(Borrowing)」と呼び、借用するために作るポインタを「参照(Reference)」と呼びます。

不変参照(&T)

アンパサンド & を付けると、所有権を奪わずに値を参照できます。参照先は読み取り専用です。

fn calculate_length(s: &String) -> usize {
    s.len()
} // s は参照なので、スコープを抜けても元の値は破棄されない

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // &s1 で参照を渡す
    println!("「{}」の長さは {} です", s1, len); // s1 は依然として有効
}

可変参照(&mut T)

参照先の内容を変更したい場合は、&mut を使って可変参照を作ります。

fn append_world(s: &mut String) {
    s.push_str(", world!");
}

fn main() {
    let mut greeting = String::from("hello");
    append_world(&mut greeting);
    println!("{greeting}"); // "hello, world!"
}

借用規則(コンパイラが保証する安全性の鉄則)

Rust では、データ競合や参照の無効化を防止するため、以下の規則がコンパイル時に厳密にチェックされます。

  • あるデータに対して、不変参照(&)は同時にいくつでも存在できる。
  • あるデータに対して、可変参照(&mut)は同時に 1 つしか存在できない。
  • 不変参照と可変参照を同時に存在させることはできない。

誰かがデータを読み取っている間は誰も変更できず、誰かが変更している間は他の誰も読み書きできないという排他制御が、言語レベルで自動保証されます。

3.3 スライス型(Slice)

スライスは、コレクション全体ではなく、その中の一連の連続した要素を参照する型です。

文字列スライス(&str)

String は可変長のヒープ確保文字列ですが、&str はその一部または全体を指し示す不変の文字列スライスです。

#![allow(unused)]
fn main() {
let s = String::from("hello world");

let hello: &str = &s[0..5];
let world: &str = &s[6..11];

println!("{hello} {world}");
}

関数の引数に文字列を受け取る際は、&String ではなく &str を指定するのが Rust の標準です。これにより、String の参照と文字列リテラル("...")の両方を柔軟に受け取ることができます。

3.4 ライフタイムの基礎

ライフタイムとは、参照が有効である期間(スコープ) のことです。

Rust のコンパイラ(ボローチェッカー)は、すでに破棄されたメモリを指す不正なポインタ(ダングリング参照)が生まれないか、ライフタイムを検証します。

#![allow(unused)]
fn main() {
// ライフタイム注釈の例
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
}

'a という記号がライフタイム注釈です。この記述は、「戻り値の参照は、引数 x と y の両方が生存している期間(より短い方の期間)と同じだけ有効である」ことをコンパイラに伝えます。

多くの日常的なコードでは、コンパイラの「ライフタイム省略規則」が働くため手動での記述は不要ですが、構造体に参照を保持させる場合や複数の参照を返す関数では必要になります。

第4章 トレイトとエラーハンドリング

Rust には他の言語に見られるクラス継承の仕組みがありません。

代わりに、インターフェースに相当する「トレイト(Trait)」と、強力なジェネリクスを組み合わせて柔軟なポリモーフィズムを実現します。また、例外機構(try-catch)を持たず、エラーを値として扱う安全なエラーハンドリングモデルを採用しています。

本章では、トレイトの基本、主要な標準トレイト、そして Option や Result を使った堅牢なエラー処理を学びます。

4.1 トレイトの基本とジェネリクス

トレイトは、複数の型に共通して実装させたいメソッドの仕様(インターフェース)を定義する仕組みです。

トレイトの定義と実装

#![allow(unused)]
fn main() {
// トレイトの定義
pub trait Summary {
    fn summarize(&self) -> String;
}

// 構造体の定義
pub struct NewsArticle {
    pub headline: String,
    pub author: String,
}

// 構造体へのトレイトの実装
impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{} by {}", self.headline, self.author)
    }
}
}

トレイト境界(Trait Bounds)

ジェネリクス関数において、特定のトレイトを実装している型のみを受け取るように制約を設けることができます。

#![allow(unused)]
fn main() {
pub fn notify<T: Summary>(item: &T) {
    println!("速報! {}", item.summarize());
}
}

上記の notify 関数は、Summary トレイトを実装している型であれば、どのような構造体でも安全に引数として受け取ることができます。

4.2 派生トレイト(Derive)と標準トレイト

Rust では、頻繁に利用する標準的なトレイトを #[derive(...)] 属性を付与するだけで自動生成(自動実装)できます。

#[derive(Debug, Clone, PartialEq)]
pub struct Point {
    pub x: i32,
    pub y: i32,
}

fn main() {
    let p1 = Point { x: 10, y: 20 };
    let p2 = p1.clone(); // Clone トレイトにより複製可能

    // Debug トレイトにより {:?} で中身を出力可能
    println!("デバッグ出力: {:?}", p1);

    // PartialEq トレイトにより == で比較可能
    if p1 == p2 {
        println!("p1 と p2 は同一の座標です");
    }
}

ユーザー向けに整形した文字列を表示したい場合は、標準ライブラリの std::fmt::Display トレイトを手動で実装します。

4.3 エラーハンドリングの思想

Rust ではエラーを「回復不能エラー」と「回復可能エラー」の 2 種類に明確に分類します。

回復不能エラー(panic!)

配列の範囲外アクセスや深刻な内部不整合など、プログラムを直ちに停止させるべき重大な欠陥が発生した場合は panic! マクロが呼び出されます。

#![allow(unused)]
fn main() {
fn crash_now() {
    panic!("致命的な問題が発生しました。プログラムを終了します。");
}
}

回復可能エラー

ファイルが見つからない、ネットワーク接続がタイムアウトしたなど、想定されるエラーはプログラムを落とさず、適切な代替処理やユーザーへの通知を行うべきです。Rust では、これらを「値」として関数の戻り値で表現します。

4.4 Option 型と Result 型の実践

値の有無を表す Option 型

Rust には他の言語のような null や nil が存在しません。値が存在するか空であるかは Option<T> 型で厳格に表します。

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T), // 値が存在する
    None,    // 値が存在しない
}
}

値を取り出すには、パターンマッチを行って必ず None の場合を考慮しなければならないため、NullPointerException のような実行時クラッシュが原理的に防がれます。

成否を表す Result 型

処理が成功したか失敗したかを表すには Result<T, E> 型を使用します。

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),  // 成功し、型 T の結果を含む
    Err(E), // 失敗し、型 E のエラー情報を含む
}
}

? 演算子によるスマートなエラー伝播

エラーハンドリングを簡潔に行うために、Rust では ? 演算子が用意されています。

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?; // 失敗した場合は直ちに Err を return する
    let mut username = String::new();
    file.read_to_string(&mut username)?; // 失敗した場合は直ちに Err を return する
    Ok(username) // 成功した場合は Ok で包んで返す
}
}

? 演算子を変数の直後に付けると、Ok の場合は中身の値をそのまま取り出し、Err の場合は現在の関数からその Err を即座にリターンします。これにより、エラーハンドリングコードでネストが深くなるのを防ぎ、処理の本質を読みやすく保つことができます。

第5章 実践:CLI ツールの開発

Rust は、起動速度の速さ、シングルバイナリへのコンパイル、低メモリ消費といった特徴から、実用的な CLI(コマンドラインインターフェース)ツールの開発に最適な言語です。

本章では、指定したファイルの中からキーワードを高速に検索し、該当する行を行番号やカラーハイライト付きで表示するミニ grep ツール「rfind」を一から作成します。

5.1 CLI ツールの仕様と設計

作成するツール「rfind」の要件は以下のとおりです。

  • 検索対象のキーワードとファイルパスをコマンドライン引数として受け取る。
  • 大文字と小文字を区別しない検索オプション(-i / --ignore-case)をサポートする。
  • 行番号の表示・非表示を切り替えるオプション(-n / --line-number)をサポートする。
  • 一致したキーワード部分をターミナル上で見やすく色付け(ハイライト)して表示する。
  • 引数が不正な場合やファイルが存在しない場合に、分かりやすいエラーメッセージを表示する。

5.2 プロジェクトの作成と依存クレートの設定

最初に、新しいバイナリプロジェクトを作成します。

cargo new rfind
cd rfind

Cargo.toml の編集

Rust のエコシステムには、高品質な CLI 開発用クレートが豊富に存在します。Cargo.toml の [dependencies] に以下を追加します。

[package]
name = "rfind"
version = "0.1.0"
edition = "2021"

[dependencies]
# コマンドライン引数を構造体にパースする標準ライブラリ
clap = { version = "4.5", features = ["derive"] }

# ターミナル出力をカラフルにするライブラリ
colored = "2.1"

# 簡潔で実用的なエラーハンドリングを提供するライブラリ
anyhow = "1.0"

各クレートの役割は以下のとおりです。

  • clap: Rust で最も広く使われている引数パーサーです。構造体にアトリビュートを付与するだけで、引数の解析や --help 表示を自動生成します。

  • colored: 文字列に .red() や .green() などのメソッドを追加し、ターミナルで直感的にカラー装飾を行えます。

  • anyhow: アプリケーション開発における汎用的なエラー処理を、少ない記述量で安全に行えるようにします。

5.3 コマンドライン引数の定義

src/main.rs を開き、clap の Derive 機能を使ってコマンドライン引数を定義します。

#![allow(unused)]
fn main() {
use clap::Parser;
use std::path::PathBuf;

#[derive(Parser, Debug)]
#[command(name = "rfind")]
#[command(author = "Your Name <you@example.com>")]
#[command(version = "1.0")]
#[command(about = "指定したファイルからキーワードを高速に検索する CLI ツール")]
struct Cli {
    /// 検索対象の文字列
    #[arg(short, long)]
    query: String,

    /// 検索するファイルのパス
    #[arg(short, long)]
    path: PathBuf,

    /// 大文字と小文字を区別せずに検索する
    #[arg(short = 'i', long = "ignore-case")]
    ignore_case: bool,

    /// 行番号を表示する
    #[arg(short = 'n', long = "line-number", default_value_t = true)]
    line_number: bool,
}
}

このように構造体を定義するだけで、Cli::parse() を呼べばコマンドライン引数が自動的にバリデーションされ、構造体のフィールドに値が格納されます。

5.4 検索ロジックの実装とテスト

検索処理はテストがしやすいように、独立した関数として切り出します。

#![allow(unused)]
fn main() {
/// 検索結果を保持する構造体
#[derive(Debug, PartialEq)]
pub struct SearchResult<'a> {
    pub line_number: usize,
    pub line_content: &'a str,
}

/// 検索ロジック本体
pub fn search<'a>(query: &str, content: &'a str, ignore_case: bool) -> Vec<SearchResult<'a>> {
    let mut results = Vec::new();
    let query_lower = query.to_lowercase();

    for (index, line) in content.lines().enumerate() {
        let is_matched = if ignore_case {
            line.to_lowercase().contains(&query_lower)
        } else {
            line.contains(query)
        };

        if is_matched {
            results.push(SearchResult {
                line_number: index + 1,
                line_content: line,
            });
        }
    }

    results
}
}

ユニットテストの作成

検索ロジックが意図通りに動作するかを検証するテストコードを記述します。

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    const SAMPLE_TEXT: &str = "\
Rust is fast and safe.
Memory safety without GC.
Trust the compiler.
";

    #[test]
    fn test_case_sensitive() {
        let results = search("Rust", SAMPLE_TEXT, false);
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].line_number, 1);
    }

    #[test]
    fn test_case_insensitive() {
        let results = search("rust", SAMPLE_TEXT, true);
        // "Rust" と "Trust" の 2 行にマッチする
        assert_eq!(results.len(), 2);
    }
}
}

テストは cargo test コマンドでいつでも即座に実行できます。

5.5 完全なコードの実装

ファイルの読み込み、検索結果の装飾表示、およびエラーハンドリングを組み合わせた src/main.rs の全貌は以下のとおりです。

use anyhow::{Context, Result};
use clap::Parser;
use colored::*;
use std::fs;
use std::path::PathBuf;

#[derive(Parser, Debug)]
#[command(name = "rfind")]
#[command(version = "1.0")]
#[command(about = "Rust 製の高速ファイル検索 CLI ツール")]
struct Cli {
    /// 検索対象の文字列
    query: String,

    /// 検索するファイルのパス
    path: PathBuf,

    /// 大文字と小文字を区別しない
    #[arg(short = 'i', long = "ignore-case")]
    ignore_case: bool,

    /// 行番号を表示する
    #[arg(short = 'n', long = "line-number", default_value_t = true)]
    line_number: bool,
}

#[derive(Debug, PartialEq)]
pub struct SearchResult<'a> {
    pub line_number: usize,
    pub line_content: &'a str,
}

pub fn search<'a>(query: &str, content: &'a str, ignore_case: bool) -> Vec<SearchResult<'a>> {
    let mut results = Vec::new();
    let query_lower = query.to_lowercase();

    for (index, line) in content.lines().enumerate() {
        let is_matched = if ignore_case {
            line.to_lowercase().contains(&query_lower)
        } else {
            line.contains(query)
        };

        if is_matched {
            results.push(SearchResult {
                line_number: index + 1,
                line_content: line,
            });
        }
    }

    results
}

fn main() -> Result<()> {
    let args = Cli::parse();

    // ファイル内容の読み込み(失敗時は親切なエラーメッセージを付与)
    let content = fs::read_to_string(&args.path)
        .with_context(|| format!("ファイルを開けませんでした: {:?}", args.path))?;

    let results = search(&args.query, &content, args.ignore_case);

    if results.is_empty() {
        println!("{}", "一致する行は見つかりませんでした。".yellow());
        return Ok(());
    }

    println!(
        "{} 件の一致が見つかりました(対象: {:?}):\n",
        results.len().to_string().green().bold(),
        args.path
    );

    for item in results {
        let line_prefix = if args.line_number {
            format!("{}: ", item.line_number.to_string().cyan())
        } else {
            String::new()
        };

        // 一致したキーワードを赤色太字でハイライト
        let highlighted_line = if args.ignore_case {
            // 大文字小文字無視の簡易置換(表示用)
            item.line_content.replace(&args.query, &args.query.red().bold().to_string())
        } else {
            item.line_content.replace(&args.query, &args.query.red().bold().to_string())
        };

        println!("{line_prefix}{highlighted_line}");
    }

    Ok(())
}

5.6 実行確認とリリースビルド

動作確認

まずは自動生成されたヘルプ画面を表示してみましょう。

cargo run -- --help

引数を渡して実際に検索を実行します。

# 大文字小文字を区別して検索
cargo run -- Rust Cargo.toml

# 大文字小文字を無視して検索
cargo run -- -i rust Cargo.toml

本番用シングルバイナリのビルド

開発が完了したら、コンパイラの最高レベルの最適化を効かせたリリースバイナリを出力します。

cargo build --release

ビルドが完了すると、target/release/rfind(Windows では target/release/rfind.exe)という単一の実行可能ファイルが生成されます。依存関係がすべて 1 つのバイナリに静的にリンクされているため、このファイル単体を他の PC やサーバーに配置するだけで即座に高速動作します。

第6章 実践:Tauri + Vite によるデスクトップアプリ開発

Web フロントエンド技術(HTML / CSS / JavaScript)と Rust を組み合わせることで、洗練された UI と圧倒的なパフォーマンス・安全性を両立したデスクトップアプリケーションを開発できます。

本章では、軽量デスクトップアプリフレームワーク「Tauri」と、超高速フロントエンドビルドツール「Vite」を用いて、システム情報を取得・表示するデスクトップ Web アプリケーションを一から作成します。

6.1 Tauri の特徴とアーキテクチャ

デスクトップアプリを Web 技術で構築する代表格として Electron がありますが、Tauri には Electron と比較して際立った優位性があります。

Electron と Tauri の違い

  • メモリとバイナリサイズ: Electron は Chromium ブラウザと Node.js ランタイムをまるごとアプリ内に同梱するため、配布サイズが 100MB を超え、メモリ消費量も大きくなります。対して Tauri は、OS 標準の Webview(Windows なら WebView2、macOS なら WebKit、Linux なら WebKitGTK)を利用し、バックエンドを Rust でネイティブコンパイルするため、配布サイズが数 MB 〜 十数 MB 程度と極めて軽量で省メモリです。

  • セキュリティと堅牢性: フロントエンドの JavaScript がローカルマシンに直接アクセスするのを厳格に制限し、Rust 側の安全なコマンド経由(IPC 通信)でのみシステム操作を許可する強固なセキュリティ設計となっています。

graph LR
    subgraph Frontend["フロントエンド (Webview)"]
        UI["UI (Vite + TypeScript)<br/>HTML / CSS / JS"]
        INVOKE["invoke()"]
    end

    subgraph Backend["バックエンド (Rust)"]
        CMD["#[tauri::command]<br/>Rust 関数"]
        OS["OS 機能 / ハードウェア / ファイル"]
    end

    UI --> INVOKE
    INVOKE -->|IPC 通信 (JSON)| CMD
    CMD --> OS

6.2 開発環境のセットアップとプロジェクト作成

開発には、Rust ツールチェーン(rustc, cargo)に加えて、Node.js(npm / pnpm / yarn)が必要です。

公式のセットアップツール create-tauri-app を利用してプロジェクトの雛形を生成します。

npm create tauri-app@latest

対話形式のプロンプトでは以下を選択します。

  • Project name: sys-monitor
  • Choose which language to use: TypeScript / JavaScript
  • Choose your package manager: npm
  • Choose your UI template: Vanilla または React(ここでは構造が分かりやすい Vanilla を選択)
  • Choose your UI flavor: TypeScript

ディレクトリ構成

プロジェクトが生成されると、以下のような構成になります。

sys-monitor/
├── index.html           # フロントエンドの起点
├── package.json         # フロントエンドの依存管理
├── src/                 # フロントエンドの TypeScript/CSS コード
│   ├── main.ts
│   └── styles.css
├── src-tauri/           # Rust バックエンドのコードと設定
│   ├── Cargo.toml       # Rust の依存管理
│   ├── tauri.conf.json  # ウィンドウサイズや権限の設定
│   └── src/
│       ├── lib.rs       # コマンド定義とロジック
│       └── main.rs      # エントリーポイント
└── vite.config.ts       # Vite のビルド設定

6.3 Rust バックエンドの実装(Tauri コマンドの定義)

まずは Rust 側で、PC のシステム情報(OS 名、CPU アーキテクチャ、メモリ情報など)を取得する関数と、テキストファイルを安全に保存するコマンドを定義します。

Cargo.toml の編集

src-tauri/Cargo.toml を開き、システム情報取得用クレート sysinfo を追加します。

[dependencies]
tauri = { version = "2.0", features = [] }
tauri-plugin-opener = "2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sysinfo = "0.31"

コマンドの実装(src-tauri/src/lib.rs)

#[tauri::command] 属性を付与した関数は、フロントエンドから IPC(プロセス間通信)経由で非同期に呼び出せるようになります。

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
use sysinfo::System;

#[derive(Serialize, Deserialize)]
pub struct SystemStats {
    os_name: String,
    total_memory_mb: u64,
    used_memory_mb: u64,
    cpu_count: usize,
}

// システム情報を取得して返す Tauri コマンド
#[tauri::command]
fn get_system_stats() -> SystemStats {
    let mut sys = System::new_all();
    sys.refresh_all();

    let os_name = System::name().unwrap_or_else(|| "Unknown OS".to_string());
    let total_memory_mb = sys.total_memory() / 1024 / 1024;
    let used_memory_mb = sys.used_memory() / 1024 / 1024;
    let cpu_count = sys.cpus().len();

    SystemStats {
        os_name,
        total_memory_mb,
        used_memory_mb,
        cpu_count,
    }
}

// ユーザー入力のテキストをローカルファイルに書き出すコマンド
#[tauri::command]
fn save_log_file(content: String) -> Result<String, String> {
    use std::fs;
    let file_path = "app_log.txt";
    fs::write(file_path, content).map_err(|e| e.to_string())?;
    Ok(format!("{} に正常に保存しました", file_path))
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![
            get_system_stats,
            save_log_file
        ])
        .run(tauri::generate_context!())
        .expect("Tauri アプリケーションの実行中にエラーが発生しました");
}
}

tauri::generate_handler![get_system_stats, save_log_file] に関数名を登録することで、フロントエンドからの呼び出しが可能になります。

6.4 フロントエンドの実装(Vite + TypeScript)

次に、Web フロントエンド側を実装します。

HTML の作成(index.html)

index.html にシンプルなダッシュボード画面を定義します。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>System Monitor</title>
    <link rel="stylesheet" href="/src/styles.css" />
  </head>
  <body>
    <div class="container">
      <h1>🦀 System Monitor (Tauri + Vite)</h1>

      <div class="card">
        <h2>システム稼働状況</h2>
        <div id="stats-display">読み込み中...</div>
        <button id="refresh-btn">状態を更新</button>
      </div>

      <div class="card">
        <h2>ログメモ保存</h2>
        <textarea id="log-input" placeholder="保存したいメモを入力..."></textarea>
        <button id="save-btn">ファイルへ保存</button>
        <p id="save-status"></p>
      </div>
    </div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

スタイリング(src/styles.css)

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background-color: #f4f6f8;
  color: #333;
  margin: 0;
  padding: 20px;
}

.container {
  max-width: 600px;
  margin: 0 auto;
}

h1 {
  font-size: 1.5rem;
  color: #1f2937;
  text-align: center;
}

.card {
  background: white;
  border-radius: 8px;
  padding: 16px 20px;
  margin-bottom: 16px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}

button {
  background-color: #2563eb;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 500;
}

button:hover {
  background-color: #1d4ed8;
}

textarea {
  width: 100%;
  height: 80px;
  margin-top: 8px;
  margin-bottom: 8px;
  box-sizing: border-box;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

フロントエンドロジック(src/main.ts)

Tauri 公式の @tauri-apps/api/core パッケージに含まれる invoke 関数を使用して、Rust コマンドを呼び出します。

import { invoke } from "@tauri-apps/api/core";

interface SystemStats {
  os_name: String;
  total_memory_mb: number;
  used_memory_mb: number;
  cpu_count: number;
}

const statsDisplay = document.getElementById("stats-display") as HTMLDivElement;
const refreshBtn = document.getElementById("refresh-btn") as HTMLButtonElement;
const logInput = document.getElementById("log-input") as HTMLTextAreaElement;
const saveBtn = document.getElementById("save-btn") as HTMLButtonElement;
const saveStatus = document.getElementById("save-status") as HTMLParagraphElement;

// Rust コマンドを呼び出してシステム情報を取得
async function fetchStats() {
  try {
    statsDisplay.innerText = "情報取得中...";
    const stats = await invoke<SystemStats>("get_system_stats");
    statsDisplay.innerHTML = `
      <ul>
        <li><strong>OS:</strong> ${stats.os_name}</li>
        <li><strong>CPU コア数:</strong> ${stats.cpu_count} コア</li>
        <li><strong>メモリ使用量:</strong> ${stats.used_memory_mb} MB / ${stats.total_memory_mb} MB</li>
      </ul>
    `;
  } catch (error) {
    statsDisplay.innerText = `エラーが発生しました: ${error}`;
  }
}

// ログ保存コマンドを呼び出し
async function saveLog() {
  const content = logInput.value;
  if (!content) return;

  try {
    const result = await invoke<string>("save_log_file", { content });
    saveStatus.innerText = result;
    saveStatus.style.color = "green";
  } catch (error) {
    saveStatus.innerText = `保存失敗: ${error}`;
    saveStatus.style.color = "red";
  }
}

refreshBtn.addEventListener("click", fetchStats);
saveBtn.addEventListener("click", saveLog);

// 初回読み込み実行
fetchStats();

6.5 アプリの動作確認と配布パッケージのビルド

開発用サーバーでの実行

プロジェクトのルートディレクトリで以下のコマンドを実行します。

npm run tauri dev

このコマンドを実行すると、Vite の高速なホットモジュールリプレース(HMR)が起動した上で、ローカルデスクトップウィンドウが立ち上がります。フロントエンドコードを変更すると、再ビルドを待つことなく即座に画面が自動更新されます。

本番用インストーラーのビルド

配布用アプリケーションをパッケージングする際は、以下のコマンドを実行します。

npm run tauri build

コンパイルが完了すると、src-tauri/target/release/bundle/ 配下に実行ファイルおよび OS に合わせたインストーラー(Windows では .msi や .exe、macOS では .dmg や .app、Linux では .deb や AppImage)が自動生成されます。

わずか数十 MB の軽量な単一インストーラーとして配布でき、ユーザーは追加の Node.js などをインストールすることなく即座に利用できます。

第7章 まとめとエコシステム

本書では、Rust の基本的な文法と核心となる所有権モデルから、実用的な CLI ツールの実装、そして Tauri と Vite を組み合わせたモダンなデスクトップ Web アプリケーションの開発までを一通り体験しました。

最終章となる本章では、学んだ内容を総括し、今後の Rust 開発をより快適で安全にするための公式ツール群や学習リソースを紹介します。

7.1 本書の振り返り

本書を通じて習得した主要なポイントは以下のとおりです。

  • メモリ安全性とゼロコスト抽象化: GC なしでコンパイル時にメモリ破壊やデータ競合を完全に防ぎ、高水準なコードをネイティブな実行速度で動かす設計思想を学びました。

  • 所有権・借用・ライフタイム: 1 つの値には 1 人の所有者しか存在しないルールと、不変参照・可変参照の排他制御によって、安全なリソース管理が実現される仕組みを理解しました。

  • 型システムと堅牢なエラー処理: Option 型と Result 型、および ? 演算子を用いることで、null によるクラッシュを防ぎ、エラーハンドリングを簡潔かつ厳格に記述できることを確認しました。

  • 実践 CLI 開発: clap を使って本格的なコマンドライン引数解析を実装し、テスト駆動でのロジック検証と高速なシングルバイナリ生成を実践しました。

  • 実践 Tauri + Vite 開発: OS ネイティブな Webview と Rust を IPC で連携させ、超軽量かつセキュアなデスクトップ Web アプリケーションを構築する手法を習得しました。

7.2 開発を加速する公式ツール群

Rust には、コードの品質と一貫性を自動で維持するための強力な公式ツールチェーンが標準で付属しています。

1. cargo clippy(静的解析・リンター)

clippy は、コード内の非効率な記述、イディオマティックでない(Rust らしくない)書き方、潜在的なバグを指摘してくれる公式リンターです。

cargo clippy

コンパイラの警告よりも一歩踏み込んだ改善案を丁寧に提示してくれるため、コードレビューの自動化や Rust のベストプラクティス習得に役立ちます。

2. cargo fmt(公式フォーマッター)

rustfmt は、コミュニティの標準ガイドラインに従ってコードのインデントや改行を自動整形します。

cargo fmt

チーム開発でもコードスタイルの議論に時間を費やす必要がなくなります。

3. cargo audit(依存関係の脆弱性スキャン)

プロジェクトで使用しているクレートに、既知のセキュリティ脆弱性(CVE)が存在しないかを自動で検査できます。

cargo install cargo-audit
cargo audit

7.3 エコシステムと今後の学習リソース

パッケージリポジトリとドキュメント

  • crates.io: Rust 公式のパッケージレジストリ(https://crates.io)です。世界中の開発者が公開している数万以上のクレートを検索できます。

  • docs.rs: crates.io に公開されたすべてのクレートの API ドキュメントが自動生成されるポータル(https://docs.rs)です。統一されたレイアウトで各関数の型や使い方を瞬時に調べられます。

おすすめの公式学習リソース

  • The Rust Programming Language(The Book): 公式の決定版テキストブックです。Rust の全仕様と背景思想が詳しく網羅されています。

  • Rust by Example: 豊富な実例コードを動かしながら学べる公式のチュートリアル集です。

  • Tauri 公式ドキュメント: Tauri の各種プラグイン(ファイルダイアログ、通知、クリップボード、SQL 連携など)の利用手順やセキュリティ設定が網羅されています。

Rust はコンパイル時のチェックが厳格であるため、最初はコンパイラのエラーメッセージに戸惑うこともあるかもしれません。しかし、Rust コンパイラのエラー表示は世界で最も親切と言われており、エラーの原因と修正候補を具体的に教えてくれます。コンパイラとの対話を楽しみながら、安全で高速なソフトウェア開発を続けていきましょう。