Rustのプログラミングを学ぶ上で、最初に触れることになるのがmain関数です。

この関数は、プログラムが実行された際にシステムから呼び出される「エントリーポイント」としての役割を担っています。

Rustにおけるmain関数は、単にコードの開始地点を示すだけでなく、エラーハンドリングや非同期処理の設定といった重要な責務を持っています。

本記事では、基本的なmain関数の書き方から、実務で必須となるResult型を用いたエラー制御、さらには非同期ランタイムを利用したasync mainの実装方法まで詳しく解説します。

2026年現在の最新のプラクティスを反映した内容となっていますので、安定したアプリケーション開発の参考にしてください。

Rustにおけるmain関数の基本構成

Rustプログラムの最もシンプルなmain関数は、引数を取らず、何も返さない形式で記述されます。

標準的なプロジェクト構成では、src/main.rsというファイルにこの関数を定義することで、実行バイナリの起点として認識されます。

Rust
// 最も基本的なmain関数の例
fn main() {
    println!("Hello, Rust world!");
}
実行結果
Hello, Rust world!

この形式では、関数内でパニックが発生しない限り、プログラムは終了ステータス0(正常終了)をOSに返します。

小規模なスクリプトや学習用のコードであれば、この記述方法で十分な場合が多いでしょう。

しかし、実際のプロダクト開発においては、ファイルの読み書きやネットワーク通信など、失敗する可能性のある処理を扱う必要が出てきます。

そのような場面では、単にprintln!を記述するだけではなく、より高度な構造が求められるようになります。

main関数のシグネチャと制約

main関数は、原則として引数を持つことができません。

コマンドライン引数を受け取る場合は、関数の引数リストではなく、std::envモジュールなどの標準ライブラリを使用して取得します。

また、戻り値については、従来は空のタプルである()のみが許容されていました。

しかし、現在のRustではstd::process::Terminationトレイトを実装した型であれば、戻り値として指定することが可能になっています。

Result型を返すmain関数の実装

Rustの大きな特徴の一つは、例外処理の代わりにResult型を使用した厳密なエラーハンドリングを行う点にあります。

main関数からResult型を返せるようになったことで、プログラム全体のエラー管理が飛躍的に簡潔になりました。

Rust
use std::fs::File;
use std::io::Read;

// main関数からResult型を返す
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut file = File::open("config.txt")?; // エラー時はここで即座に復帰する
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

    println!("設定ファイルの内容: {}", contents);
    
    // 正常終了時はOk(())を返す
    Ok(())
}

このコードでは、?(クエスチョンマーク)演算子を使用してエラーを伝播させています。

もしFile::openが失敗した場合、main関数は即座に終了し、エラー情報が標準エラー出力に表示されます。

戻り値の型として指定されているBox<dyn std::error::Error>は、あらゆる種類のエラーを許容するための「トレイトオブジェクト」です。

なぜResultを返す必要があるのか

関数内で発生したエラーをmatch式やif letで一々処理すると、コードのネストが深くなり、可読性が低下してしまいます。

main関数がResultを返すことで、個別のエラー処理を省略し、ハッピーパス(正常系)の記述に集中できるようになります。

また、エラーが発生した際に自動的に適切な終了コード(一般的には1)をOSへ返してくれるため、シェルスクリプトなどとの連携もスムーズに行えます。

外部クレートによるエラーハンドリングの強化

実際の開発では、標準のBox<dyn std::error::Error>よりも便利な外部クレートが多用されます。

特にanyhowというクレートは、アプリケーションレベルのエラーハンドリングにおいてデファクトスタンダードとなっています。

Rust
use anyhow::{Context, Result};

fn main() -> Result<()> {
    let content = std::fs::read_to_string("data.json")
        .with_context(|| "ファイルの読み込みに失敗しました")?;
    
    println!("データ: {}", content);
    Ok(())
}

anyhowを使用すると、エラーが発生した際のコンテキスト情報を付与することができ、デバッグ効率が大幅に向上します。

Result<()>という簡潔な記述で済むため、コード全体の美しさを保つことが可能です。

非同期main関数(async main)の仕組み

現代的なRust開発において、非同期処理(Async/Await)は避けて通れない要素です。

しかし、Rustの標準ライブラリ自体は非同期ランタイムを持っていないため、async fn main()をそのまま記述することはできません。

そこで、tokioなどの非同期ランタイムを提供するクレートと、マクロを使用するのが一般的です。

Tokioを使用したasync mainの定義

最も普及している非同期ランタイムであるtokioを使用した例を見てみましょう。

Rust
#[tokio::main] // 非同期ランタイムを起動するマクロ
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::get("https://www.rust-lang.org")
        .await?
        .text()
        .await?;

    println!("レスポンスサイズ: {}", response.len());
    Ok(())
}

この#[tokio::main]という属性が付与された関数は、コンパイル時に「通常のmain関数が非同期実行環境を構築し、その中で非同期関数を実行する」という形に展開されます。

開発者は複雑なランタイムの初期化を意識することなく、トップレベルから.awaitを使用できるようになります。

非同期mainの内部挙動

マクロが展開された後のコードイメージは、以下のようになります。

Rust
fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        // ここに元々のasync mainの内容が入る
    });
}

このように、OSから呼び出される本物のエントリーポイントは依然として同期的なfn main()です。

その内部でイベントループが動き出し、非同期タスクを管理する仕組みになっています。

コマンドライン引数の取得と解析

main関数はプログラムの入り口であるため、実行時に渡される引数を処理することも重要な役割です。

Rust標準ライブラリのstd::env::argsを使用することで、引数をイテレータとして取得できます。

Rust
fn main() {
    // 引数をベクタとして収集
    let args: Vec<String> = std::env::args().collect();

    if args.len() < 2 {
        eprintln!("使用法: {} [名前]", args[0]);
        std::process::exit(1);
    }

    println!("こんにちは、{}さん!", args[1]);
}

args[0]には実行ファイル自身のパスが含まれており、ユーザーが入力した引数はargs[1]以降に格納されます。

なお、引数に不正なUnicodeが含まれている可能性がある場合は、std::env::args_osを使用する方が安全です。

引数解析ライブラリ「clap」の活用

本格的なCLIツールを作成する場合、手動で引数をパースするのは非常に困難です。

そのため、clapクレートを利用して、宣言的に引数構造を定義するのが一般的です。

Rust
use clap::Parser;

#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Args {
    /// 挨拶する相手の名前
    #[arg(short, long)]
    name: String,

    /// 挨拶を繰り返す回数
    #[arg(short, long, default_value_t = 1)]
    count: u8,
}

fn main() {
    let args = Args::parse();

    for _ in 0..args.count {
        println!("Hello {}!", args.name);
    }
}

clapを使用すれば、ヘルプ画面の自動生成や、型チェック、デフォルト値の設定などが極めて簡単に行えます。

main関数の冒頭でArgs::parse()を呼び出すだけで、構造化されたデータとして引数を受け取れるようになります。

終了ステータスコードの制御

プログラムが正常に終了したのか、それともエラーで終了したのかを呼び出し元に伝えることは、システム連携において不可欠です。

Rustでは、いくつかの方法で終了コードを制御できます。

std::process::exitの使用

最も直接的な方法は、std::process::exit関数を呼び出すことです。

Rust
fn main() {
    if some_critical_error() {
        eprintln!("致命的なエラーが発生しました");
        std::process::exit(1); // 任意のコードで即座に終了
    }
    println!("正常終了");
}

ただし、この関数を呼び出すと、その時点より後のデストラクタ(Drop)が実行されません

リソースの解放が適切に行われない可能性があるため、可能な限り後述のResult型やExitCodeを使用することが推奨されます。

std::process::ExitCodeの活用

Rust 1.61から導入されたstd::process::ExitCodeを使用すると、よりクリーンに終了コードを指定できます。

Rust
use std::process::ExitCode;

fn main() -> ExitCode {
    if check_condition() {
        println!("成功です");
        ExitCode::SUCCESS // 0を返す
    } else {
        eprintln!("失敗です");
        ExitCode::FAILURE // 1を返す
    }
}

この方法であれば、main関数がスコープを抜ける際に正しくリソースが解放されます。

また、特定のプラットフォーム固有の終了コードを返したい場合は、ExitCode::from(u8)を使用して数値を指定することも可能です。

Terminationトレイトによるカスタマイズ

main関数の戻り値として使用できる型は、実は固定されているわけではありません。

標準ライブラリのstd::process::Terminationトレイトを実装していれば、独自の型を返すことができます。

用途終了コード
()最もシンプルな形式常に 0
Result<(), E>標準的なエラーハンドリングOkなら0、Errなら1
ExitCode明示的なステータス指定SUCCESSなら0、FAILUREなら1
! (never型)無限ループなど終了しない処理N/A

この柔軟な設計により、Rustのmain関数は型安全でありながら、OSとの親和性も高く保たれています。

main関数内での初期化処理の順序

アプリケーションの規模が大きくなると、main関数の冒頭で行うべき初期化処理が増えてきます。

適切な順序で初期化を行わないと、ログが出力されなかったり、設定値が反映されなかったりするトラブルの原因になります。

一般的には、以下の順序で記述することが推奨されます。

  1. ロガーの初期化(例:env_loggertracing
  2. コマンドライン引数の解析
  3. 環境変数や設定ファイルの読み込み
  4. 非同期ランタイムやスレッドプールの構築
  5. 主要なコンポーネント(データベース接続など)の生成
  6. メインロジックの実行

特にロガーの初期化は、エラーが発生した際にその詳細を記録するために、最も早い段階で行うべきです。

Rust
fn main() -> anyhow::Result<()> {
    // 1. ロガーを最初に初期化
    tracing_subscriber::fmt::init();

    // 2. 設定を読み込み
    let config = Config::load()?;

    tracing::info!("アプリケーションを起動中...");

    // 3. メインロジックへ
    run_app(config)?;

    Ok(())
}

このような構造にすることで、万が一プログラムの途中でエラーが発生しても、原因の特定が容易になります。

まとめ

Rustのmain関数は、単なる開始地点を超えて、プログラム全体の安定性を支える重要な基盤です。

基本的なfn main()から始まり、エラーハンドリングを重視したResult型の導入、そして現代の計算機環境に不可欠なasync mainまで、用途に応じて多様な記述方法が提供されています。

特に?演算子を活用したエラー伝播や、clapなどの強力な外部ライブラリとの組み合わせにより、堅牢かつ書きやすいコードを実現できるのがRustの強みです。

まずはResult<(), Box<dyn Error>>を返す形式に慣れることから始め、徐々に非同期処理や詳細な終了ステータス管理へとステップアップしていきましょう。

本記事で紹介したパターンを活用することで、あなたのRustプロジェクトのエントリーポイントはより洗練されたものになるはずです。