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

## 概要

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関数が何も返さないプログラムの終了コードを確認してみます。

```rust
fn main() {
}
```

`0`になりました。

```bash
$ cargo run
$ echo $?
0
```

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

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

```rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    Ok(())
}
```
これも`0`になりました。

```bash
$ cargo run
$ echo $?
0
```

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

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

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

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

```bash
$ cargo run
$ echo $?
1
```

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

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

```rust
use std::process::ExitCode;

fn main() -> ExitCode {
    ExitCode::SUCCESS
}
```

```bash
$ cargo run
$ echo $?
0
```

```rust
use std::process::ExitCode;

fn main() -> ExitCode {
    ExitCode::FAILURE
}
```

```bash
$ cargo run
$ echo $?
1
```

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

```rust
use std::process::ExitCode;

fn main() -> ExitCode {
    ExitCode::from(99)
}
```

```bash
$ cargo run
$ echo $?
99
```

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

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

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

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

```bash
$ cargo run
$ echo $?
101
```

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

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

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

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

https://doc.rust-lang.org/std/process/trait.Termination.html

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

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

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

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

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

https://doc.rust-lang.org/src/std/process.rs.html#2444-2449

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

```rust
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
            }
        }
    }
}
```

https://doc.rust-lang.org/src/std/process.rs.html#2474-2484

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

```rust
// 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]

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

https://doc.rust-lang.org/src/std/process.rs.html#2466-2471

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

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

## パニック時の挙動

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

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

https://doc.rust-lang.org/std/macro.panic.html#current-implementation

## `std::process::exit`の利用

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

```rust
use std::process;

fn main() {
    process::exit(99);
}
```

```bash
$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:

https://doc.rust-lang.org/std/process/fn.exit.html

## まとめ

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

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

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

## 参考

https://doc.rust-lang.org/std/process/trait.Termination.html

https://doc.rust-lang.org/std/macro.panic.html

https://doc.rust-lang.org/std/process/fn.exit.html

## We Are Hiring!

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

https://www.valuesccg.com/pitchcraft/

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