1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustプログラムの終了コードを徹底解説

Last updated at Posted at 2024-11-13

概要

Rustプログラムの終了コード(exit code)を把握することは、プログラムの挙動を理解する上で非常に重要です。本記事では、Rustプログラムがどのように終了コードを決定するかを詳しく解説します。終了コードについて理解しておくと、例えばシェルスクリプトでRustプログラムを実行する際のエラーハンドリングを行う際などに役立ちます。

Rustプログラムの終了コードは以下のようになります。

条件 終了コード 結果
main関数が何も返さない 0 正常終了
main関数がResult::Ok(())を返す 0 正常終了
main関数がResult::Errを返す 1 異常終了
main関数がExitCode::SUCCESSを返す 0 正常終了
main関数がExitCode::FAILUREを返す 1 異常終了
main関数が任意の値のExitCodeを返す ExitCodeの値 -
パニックが発生する 101 異常終了
std::process::exit()を実行する 引数の値 -

プログラムの実装としては、正常に終了した場合はmain関数から何も返さないかResult::Ok(())を返すようにするのが良いでしょう。異常終了の場合はResult::Errを返すことで終了コードが1になるようにすることができます。

プログラムの実行側からはRustプログラムの終了コードが0なら正常終了、それ以外なら異常終了と判定するのが良いでしょう。(パニックが発生した場合は終了コードが101になるため注意が必要です)

以下ではこのような終了コードになる理由やプログラムを正しく終了させるための方法について解説していきます。

Rustプログラムの終了コードの確認

ケース1: main関数が何も返さない

まず以下のようにmain関数が何も返さないプログラムの終了コードを確認してみます。

fn main() {
}

0になりました。

$ cargo run
$ echo $?
0

ケース2: main関数がResult::Okを返す

以下のようにmain関数がResult::Okを返すプログラムの終了コードを確認してみます。

fn main() -> Result<(), Box<dyn std::error::Error>> {
    Ok(())
}

これも0になりました。

$ cargo run
$ echo $?
0

ケース3: main関数がResult::Errを返す

次に以下のようにmain関数がResult::Errを返すプログラムの終了コードを確認してみます。

fn main() -> Result<(), Box<dyn std::error::Error>> {
    Err("error".into())
}

こちらは1になりました。

$ cargo run
$ echo $?
1

ケース4: main関数がExitCodeを返す

main関数からstd::process::ExitCode型の値を返すことができます。ExitCodeにはSUCCESSFAILUREの2つの値が定義されており、それぞれ終了コード01に対応しています。

use std::process::ExitCode;

fn main() -> ExitCode {
    ExitCode::SUCCESS
}
$ cargo run
$ echo $?
0
use std::process::ExitCode;

fn main() -> ExitCode {
    ExitCode::FAILURE
}
$ cargo run
$ echo $?
1

また、ExitCode::from()を使って任意の8ビット数値の終了コードを返すこともできます。

use std::process::ExitCode;

fn main() -> ExitCode {
    ExitCode::from(99)
}
$ cargo run
$ echo $?
99

ケース5: パニックが発生する

最後に以下のようにmain関数でパニックが発生するプログラムの終了コードを確認してみます。

fn main() -> Result<(), Box<dyn std::error::Error>> {
    panic!()
}

こちらは101になりました。

$ cargo run
$ echo $?
101

終了コードの内部メカニズムとTerminationトレイト

では上記で確認したようになる理由を詳しく見ていきましょう。

まずRustのmain関数の戻り値はTerminationトレイトを実装している必要があります。例えばユニット型(())やResult型にはこのTerminamtionトレイトが実装されています。

pub trait Termination {
    // Required method
    fn report(self) -> ExitCode;
}

reportメソッドの実装によって終了コードが決まります。

String型などTerminamtionトレイトを実装していない型をmain関数から返そうとするとコンパイルエラーになります。

// do not compile
fn main() -> String {
    "Hello, world!".to_string()
}

ケース1のmain関数が何も返さない場合はユニット型()を返しているのと同義です。ユニット型へのTerminationトレイトの実装は以下のようになっているため、終了コードが0になります。

impl Termination for () {
    #[inline]
    fn report(self) -> ExitCode {
        ExitCode::SUCCESS
    }
}

Result型には以下のようにTerminationトレイトが実装されています。main関数がResult::Okを返す場合は終了コードがラップされた値のreportメソッドの結果になります。上記の場合ではResult::Ok(())を返しているので終了コードはユニット型と同じく0になります。Result::Errを返す場合は終了コードは常に1になります。

impl<T: Termination, E: fmt::Debug> Termination for Result<T, E> {
    fn report(self) -> ExitCode {
        match self {
            Ok(val) => val.report(),
            Err(err) => {
                io::attempt_print_to_stderr(format_args_nl!("Error: {err:?}"));
                ExitCode::FAILURE
            }
        }
    }
}

main関数から返されるResult型の中の値もTerminationトレイトを実装している必要があるため、以下のように(Terminationトレイトを実装していない)String型の値を含んだResult::Okを返そうとするとコンパイルエラーになります。

// do not compile
fn main() -> Result<String, Box<dyn std::error::Error>> {
    Ok("Hello, world!".to_string())
}

そのため、基本的な用途ではmain関数から返すResult::Okの値はTerminationトレイトを実装している()になるでしょう。

ExitCode型については以下のようにTerminationトレイトが実装されています。ExitCodeの値がそのまま終了コードになります。1

impl Termination for ExitCode {
    #[inline]
    fn report(self) -> ExitCode {
        self
    }
}

Result型で正常終了と異常終了の場合を表現することができるため、一般にExitCode型を使う場面はあまりないでしょう。ただし、01以外の終了コードを返したい特殊な場合にはこちらを利用することができます。

パニック時の挙動

パニックした場合については以下のように終了コードが101になるような実装になっているとドキュメントにあります。

If the main thread panics it will terminate all your threads and end your program with code 101.

std::process::exitの利用

main関数から値を返したり、パニックする以外にプログラムを終了する方法にstd::process::exit()を実行するというものがあります。この関数の引数に数値を渡すとそれが終了コードになります(筆者の環境では最大値が231でした)。

use std::process;

fn main() {
    process::exit(99);
}
$cargo run
$ echo $?  
99

しかしながら、この方法でプログラムを終了させてしまうとデストラクタが走らずリソースが解放されません。そのため、main関数からResult型やExitCode型の値を返してしてプログラムを終了させることが推奨されています。

Note that because this function never returns, and that it terminates the process, no destructors on the current stack or any other thread’s stack will be run. If a clean shutdown is needed it is recommended to only call this function at a known point where there are no more destructors left to run; or, preferably, simply return a type implementing Termination (such as ExitCode or Result) from the main function and avoid this function altogether:

まとめ

Rustプログラムの終了コードを理解することで、正常終了と異常終了を区別して適切な処理を行うことができます。

正常に処理が完了した場合はmain関数から何も返さないか、Result::Okを返すようにするのが良いでしょう。また処理が正常に完了しなかった時はResult::Errを返すことで終了コードが1になるようにすることができます。パニックが発生した場合は終了コードが101になるため、エラーハンドリングを行う際には注意が必要です。終了コードが0なら正常終了、それ以外なら異常終了と判定するのが良いでしょう。

std::process::exitについてはリソースの解放が行われないため利用を避けましょう。

参考

We Are Hiring!

VALUESでは「組織の提案力と生産性を最大化する提案ナレッジシェアクラウド」Pitchcraftなどの開発メンバーとしてエンジニアを積極採用中です。

PitchcraftではRustを積極的に採用して開発しています。Rustでの開発に興味がある方はぜひご連絡ください!

  1. ExitCodeがTerminationトレイトを実装しているのでResult型の中の値としても利用することもできます。例えばResult::Ok(ExitCode::FAILURE)を返すことで終了コードが1になるような実装も可能です。実装上可能というだけで、直感に反するのでのであまりやらない方がいいでしょう。冒頭で終了コードが0になる場合をResult::Ok(())に限定したのはこのためです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?