概要
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
にはSUCCESS
とFAILURE
の2つの値が定義されており、それぞれ終了コード0
と1
に対応しています。
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型を使う場面はあまりないでしょう。ただし、0
と1
以外の終了コードを返したい特殊な場合にはこちらを利用することができます。
パニック時の挙動
パニックした場合については以下のように終了コードが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での開発に興味がある方はぜひご連絡ください!
-
ExitCodeがTerminationトレイトを実装しているのでResult型の中の値としても利用することもできます。例えば
Result::Ok(ExitCode::FAILURE)
を返すことで終了コードが1
になるような実装も可能です。実装上可能というだけで、直感に反するのでのであまりやらない方がいいでしょう。冒頭で終了コードが0
になる場合をResult::Ok(())
に限定したのはこのためです。 ↩