Rust の現時点の最新の安定版 1.5 では、panic が起こった時に、それを捕捉することができない。panic を起こしたスレッドはメモリーを解放した後、クラッシュする。1.7 nightly では panic を捕捉する仕組みとして std::panic:recover()
が非安定 API として追加された。
私は Erlang VM から FFI 経由で Rust の関数を使いたいので、panic が捕捉できないとかなり辛い。この機能を試してみることにした。
2016年4月10日追記:この機能は Rust 1.9 で安定化された
変更点
-
std::panic::recover()
から、std::panic::catch_unwind()
に変更 -
RecoverSafe
とAssertRecoverSafe()
から、UnwindSafe
とAssertUnwindSafe()
に変更 - 変数だけでなく、クロージャーを
AssertUnwindSafe
でラップすることも可能になった(マイナス面あり)
最後の変更により、例えばこう書いていたのが、
let b_sess = AssertRecoverSafe(&sess);
let b_cstore = AssertRecoverSafe(&cstore);
let b_cfg = AssertRecoverSafe(cfg.clone());
let b_control = AssertRecoverSafe(&control);
panic::recover(|| {
driver::compile_input(&b_sess, &b_cstore, (*b_cfg).clone(),
&input, &out,
&None, None, &b_control)
})
こう書けるようになった。これにより、このクロージャーがキャプチャしてる変数が、一括して UnwindSafe
にマークされる。
let res = panic::catch_unwind(AssertUnwindSafe(|| {
driver::compile_input(&sess, &cstore, cfg.clone(),
&input, &out,
&None, None, &control)
}));
ただし、API ドキュメントに書かれている ように、この使い方にはマイナス面もある。それは、後日、クロージャーにキャプチャする変数を追加した時に、仮にそれを UnwindSafe
にするべきでなかった時でも自動的に UnwindSafe
とみなされ、コンパイルが通ってしまうことだ。クロージャーをラップするかどうかは、よく考えてから決めよう。
(追記終わり)
環境
- rustc バージョン:1.7 nighly(2016年1月19日のソースコードからビルドしたもの)
- Host:FreeBSD 10.2-RELEASE
% rustc --version --verbose
rustc 1.7.0-dev (f6dd66e56 2016-01-19)
binary: rustc
commit-hash: f6dd66e568f705de6a35c7c7a765ad6d79c69b1e
commit-date: 2016-01-19
host: x86_64-unknown-freebsd
release: 1.7.0-dev
Rust は例外処理を提供しない
Rust には、一般的な言語に見られる例外処理の仕組みが用意されていない1。唯一、回復不可能なエラーを起こす panic!()
があり、これを起こしたスレッドは、メモリーを開放した後にクラッシュする。もしメインスレッドが panic すると、プログラム全体が abort される。
例:明示的に panic を起こす
fn main() {
panic!("おっと");
}
% ./main
thread '<main>' panicked at 'おっと', main.rs:4
% echo $?
101 # ← 異常終了(abort)
例:None
から値を取り出そうとすると、panic する
fn main() {
let _: Option<i64> = None.unwrap();
}
% ./main
thread '<main>' panicked at 'called `Option::unwrap()` on
a `None` value', src/libcore/option.rs:366
% echo $?
101
例:0による除算で panic する
fn main() {
println!("1 ÷ 0 = {}", 1 / 0);
}
% ./main
thread '<main>' panicked at 'attempted to divide by zero', main.rs:4
% echo $?
101
例:子スレッドが panic しても、メインスレッドには影響ない
子スレッドがクラッシュした時は、join()
で Err<_>
が返る。
use std::thread;
fn main() {
let h = thread::spawn(|| {
panic!("おっと");
});
match h.join() {
Ok(_) => println!("あいつは生き延びたようだ"),
Err(_) => println!("あいつはやられたようだ"),
}
}
% ./main
thread '<unnamed>' panicked at 'おっと', main.rs:7
あいつはやられたようだ
% echo $?
0 # ← 正常終了
エラー処理をとりまくプログラミングスタイル
Rust では、エラー処理に関連するプログラミングスタイルとして、以下が推奨されている。
- エラーが起こったことは、例外(exception)ではなく、戻り値で示す
- 例えば、
std::io
にある大半の関数は、戻り値としてstd::io::Result<T>
を返す。Result
はエラーが起こることを暗示する型 - 例外を使わないほうがプログラムの流れを追いやすく、また、パターンマッチにより簡潔に書ける
- 例えば、
- 致命的な問題が起こった時は、スレッドを回復させるよりも、クラッシュさせて、きれいな状態から再スタートさせる
- 仮に回復させられたとしても、中途半端に壊れたデータ(DBとの接続なども含む)などが原因で、予想外の振る舞いをするかもしれない。また
unsafe
ブロックがある場合、その部分のコードによっては、メモリーリークする可能性もある - 問題が起こった時にクラッシュさせる手法「Let it crash」は Erlang/OTP のベストプラクティスを参考にした
- 仮に回復させられたとしても、中途半端に壊れたデータ(DBとの接続なども含む)などが原因で、予想外の振る舞いをするかもしれない。また
panic のような例外処理は、静的な型検査で検出できないプログラムの実行時に起こる問題に対応するためだけに使う。
Rust RFC 1236 Stabilize catch panic
とはいえ、現実のプログラムでは、きれいごとばかりも言ってられない。堅牢性や性能のために、panic を捕捉したいユースケースも確実にある。これに応えるために提案されたのが、Rust の RFC 1236 「Stabilize catch panic」(catch panic の安定化)だ。
そこには、以下のようなユースケースが挙げられている。
-
Rust の panic では、他言語関数インターフェイス(FFI)の境界を超えた時の振る舞いを定義していない。もし C プログラムから Rust の関数を呼び出した時に Rust 側で panic したら、何が起こるのかは相手側の処理系に依存する。Rust 側で panic を捕捉できるようにすることで、Rust 関数を呼び出した時に、プロセス全体が abort されるリスクを回避できる。
-
汎用的なスレッドプールの実装では、タスクが panic した時に、スレッドをクラッシュ(そして、新規スタート)するのではなく、panic を捕捉することを選ぶかもしれない。
普段 Erlang/OTP で開発している私としては、2ではスレッドをクラッシュさせたほうが堅牢なシステムが作れると考える。しかし、OS のネイティブスレッドを使う Rust の場合、スレッドの作成は比較的重い処理になるので、性能の要求によっては、2のような選択もアリだろう。
1は個人的には切実な問題だ。というのは、私は Erlang VM 内で Rust 関数を呼び出して使う予定だからだ。現状は、Rust 側で panic すると、VM 全体がコアダンプを吐いてクラッシュしてしまう。
panic はプログラムのバグに起因するものがほとんどなので、十分にレビューされた、お行儀の良いプログラムなら、ほとんどの場合、防げるだろう。しかし、その一方で、スタックオーバーフローなどの実行環境に起因するものも少なからずあるので、完全に防ぐことはできない。
なお、現在の安定版では RCF 1236 の機能は使用できない。オンラインのマニュアル「The Rust Programming Language」の「Effective Rust -- Foreign Function Interface」の章では、以下のような 苦しい対応法を勧めている。
FFI と panic
FFI では
panic!
に気を配ることが重要です。なぜなら FFI 境界を超えたpanic!
の振る舞いは未定義だからです。もし panic する可能性のあるコードを書いているなら、その部分は別のスレッドで走らせるべきでしょう。そうすれば、panic が C に伝わることを防げます。use std::thread; #[no_mangle] pub extern fn oh_no() -> i32 { let h = thread::spawn(|| { panic!("Oops!"); }); match h.join() { Ok(_) => 1, Err(_) => 0, } }
処理の粒度にもよるが、単に panic を捕捉するためだけに別スレッドを立てて実行するのは、多くの場合、複雑すぎるだろう。また、Rust では Thread 間でデータを渡す際の安全性の検査が厳しく、渡せないデータも多い。例えば、後で実例を見るが、他言語から渡された C の生ポインターは、スレッド境界を超えられない。
ちなみに thread::spawn()
は、スレッドが作れなかった時に panic を起こすので安全ではない。io::Result<T>
を返す thread::Builder::spawn()
を使うのが懸命だろう。
仕様のポイント
RFC 1236 は、長くて激しい議論 の末、以下のような仕様に落ち着いた。現在は unstable API として 1.7 に取り込まれている。
- panic を起こしたスレッドが、自身でそれを捕捉するために
std::panic::recover()
を導入する。対象とするコードブロックは、以下のようにクロージャーとして与える。
use std::panic;
#[no_mangle]
pub extern fn called_from_c(ptr: *const c_char, num: i32) -> i32 {
let result = panic::recover(|| {
let s = unsafe { CStr::from_ptr(ptr) };
println!("{}: {}", s, num);
});
match result {
Ok(_) => 0,
Err(_) => 1,
}
}
- panic で処理が打ち切られた時のデータの安全性を保証するため、呼び出し元とクロージャーの間で受け渡すデータは、
std::panic::RecoverSafe
マーカー・トレイトを実装した型のみに制限する- 変更不可能な値や、その参照は壊れないので
RecoverSafe
である - 変更可能な参照
&mut T
は、データが中途半端に更新された状態で panic が起こると安全ではなくなる(例:配列の一部は更新済みで、残りは未更新か未定義値)。従ってRecoverSafe
ではなく、クロージャーの境界を超えらない - C の生ポインター(
*const
と*mut
)は、クロージャーの境界を超えられる。生ポインターへのアクセスは、そもそもコンパイラーでは安全性の検査ができずunsafe
ブロックで囲む必要がある。そのため、panic 時の安全性の確認も開発者に任されることになる。コンパイラーはこれらの型をRecoverSafe
とみなすことで、クロージャーとの受け渡しを許容する - 変更可能な型を
RecoverSafe
とみなすためのラッパーstd::panic::AssertRecoverSafe
を提供する。これでラップされたデータの安全性と panic 捕捉後の処置については、コンパイラーではなく、開発者が責任を持つ
- 変更不可能な値や、その参照は壊れないので
panic::recover() を一般的な Rust 関数で試してみる
panic を起こす関数を panic::recover()
で捕捉してみよう。
なお、この API は現時点(2016年1月 Rust 1.7 nightly)では非安定だ。今後、予告なしに変更、または、削除される可能性もあるので注意してほしい。また、コンパイルには、非安定版の rustc が必須となる。
ここでは、例として、以下のような簡単なプログラムを使用する。
fn remainders(number: i64, divisors: &Vec<i64>) -> Vec<i64> {
let mut rems = Vec::new();
for div in divisors.iter() {
rems.push(number % div);
}
rems
}
関数 remainders()
は、number: i64
と divisors: &Vec<i64>
の2つの引数を取る。divisors
の各要素で number
の剰余を取り、Vec<i64>
型で結果を返す。
呼び出す側は以下の通り。
fn main() {
let number = 13;
let divisors = vec![7, 3, 11, 5, 2];
let rems = remainders(number, &divisors);
println!("{} % {:?} = {:?}", number, divisors, rems);
}
実行結果
% cargo run
13 % [7, 3, 11, 5, 2] = [6, 1, 2, 3, 1]
もし、divisors
が 0
を含んでいると、ゼロ除算による panic が起こり、プロセスが abort される。
// let divisors = vec![7, 3, 11, 5, 2];
let divisors = vec![7, 3, 0, 5, 2];
thread '<main>' panicked at 'attempted remainder with a divisor of zero', src/libcore/ops.rs:430
Process didn't exit successfully: `target/debug/example` (exit code: 101)
今回のようにゼロ除算で問題が起きると事前にわかっている場合、Rust のプラクティスに従うなら、以下のように Result<T, E>
型を使うべきだろう。
fn remainders(number: i64, divisors: &Vec<i64>) -> Result<Vec<i64>, String> {
let mut rems = Vec::new();
for div in divisors.iter() {
if *div == 0 {
return Err("attempted remainder with a divisor of zero".to_owned());
}
rems.push(number % div);
}
Ok(rems)
}
fn main() {
let number = 13;
let divisors = vec![7, 3, 0, 5, 2];
match remainders(number, &divisors) {
Ok(rems) =>
println!("{} % {:?} = {:?}", number, divisors, rems),
Err(reason) =>
println!("{} % {:?} failed because {}.", number, divisors, reason),
}
}
13 % [7, 3, 0, 5, 2] failed because attempted remainder with a divisor of zero.
ただ、今回は説明のために、あえて panic を捕捉するアプローチを取る。
安定版 Rust での方法:別スレッドを立てる
まずは、現在の安定版で行える対策として、panic を起こしそうな関数を、別スレッドで実行する。
join()
は、スレッドの正常終了時は Ok
を返し、クラッシュ時は Err
を返す。
use std::thread;
fn remainders(number: i64, divisors: &Vec<i64>) -> Result<Vec<i64>, String> {
let (number_c, divisors_c) = (number.clone(), divisors.clone());
let h = try!(thread::Builder::new().name("rem".to_owned()).spawn(move || {
let mut rems = Vec::new();
for div in divisors_c.iter() {
rems.push(number_c % div);
}
rems
}).map_err(|e| e.to_string()));
match h.join() {
Ok(rems) => Ok(rems),
Err(e) =>
match e.downcast_ref::<String>() {
Some(as_string) => Err(as_string.to_owned()),
None => Err("".to_owned()),
}
}
}
fn main() {
let number = 13;
let divisors = vec![7, 3, 0, 5, 2];
match remainders(number, &divisors) {
Ok(rems) =>
println!("{} % {:?} = {:?}", number, divisors, rems),
Err(reason) =>
println!("{} % {:?} failed because {}.", number, divisors, reason),
}
}
thread 'rem' panicked at 'attempted remainder with a divisor of zero', src/libcore/ops.rs:430
13 % [7, 3, 0, 5, 2] failed because attempted remainder with a divisor of zero.
確かに動くが、無駄にスレッドを走らせている感は否めない。
後半の match
式のあたりはごちゃごちゃしているが、その気になれば、共通部品として別関数にくくり出せるので問題ないだろう。今回はこのまま match
式を書き続ける。
非安定版 panic::recover() を使う
本題の panic::recover()
を使ってみよう。この関数は、正常終了時は元の戻り値を Ok
でラップし、panic 時は Err
を返す。
#![feature(recover, std_panic)]
use std::panic;
fn remainders(number: i64, divisors: &Vec<i64>) -> Result<Vec<i64>, String> {
let result = panic::recover(move || {
let mut rems = Vec::new();
for div in divisors.iter() {
rems.push(number % div);
}
rems
});
match result {
Ok(rems) => Ok(rems),
Err(e) =>
match e.downcast_ref::<String>() {
Some(as_string) => Err(as_string.to_owned()),
None => Err("".to_owned()),
}
}
}
fn main() {
// 省略(スレッド版と同じ)
}
thread '<main>' panicked at 'attempted remainder with a divisor of zero', src/libcore/ops.rs:430
13 % [7, 3, 0, 5, 2] failed because attempted remainder with a divisor of zero.
% echo $?
0
メインスレッドで panicked と表示されたが、その後、回復して結果を表示した。いい感じだ。
AssertRecoverSafe が必要なケース
remainder()
を修正して、毎回新しい vector を作って返すのではなく、呼び出し元が用意した vector を利用する方法に変えてみよう。今回は1回限りの実行なので、なんのメリットもないが、vector を繰り返し使うバッファー的な用途では、メモリ割り当て、解放のオーバーヘッドを回避できる。
#![feature(recover, std_panic)]
use std::panic;
fn remainders(number: i64, divisors: &Vec<i64>, mut rems: &mut Vec<i64>) -> Result<(), String> {
let result = panic::recover(move || {
for (i, div) in divisors.iter().enumerate() {
rems[i] = number % div;
}
});
match result {
Ok(_) => Ok(()),
Err(e) =>
match e.downcast_ref::<String>() {
Some(as_string) => Err(as_string.to_owned()),
None => Err("".to_owned()),
}
}
}
fn main() {
let number = 13;
let divisors = vec![7, 3, 0, 5, 2];
let mut rems = vec![0; 5];
match remainders(number, &divisors, &mut rems) {
Ok(_) =>
println!("{} % {:?} = {:?}", number, divisors, rems),
Err(reason) =>
println!("{} % {:?} failed because {}.", number, divisors, reason),
}
}
% cargo run
Compiling example v0.1.0 (file:///...)
src/main.rs:8:15: 8:29 error: the trait `std::panic::RecoverSafe` is not
implemented for the type `&mut collections::vec::Vec<i64>` [E0277]
src/main.rs:8 let res = panic::recover(move || {
^~~~~~~~~~~~~~
src/main.rs:8:15: 8:29 help: run `rustc --explain E0277` to see a detailed explanation
src/main.rs:8:15: 8:29 note: the type &mut collections::vec::Vec<i64> may not
be safely transferred across a recover boundary
src/main.rs:8:15: 8:29 note: required because of the requirements on
the impl of `std::panic::RecoverSafe`
for `[closure@src/main.rs:8:30: 12:6
divisors:&collections::vec::Vec<i64>, number:i64, rems:&mut collections::vec::Vec<i64>]`
src/main.rs:8:15: 8:29 note: required by `std::panic::recover`
error: aborting due to previous error
Could not compile `example`.
To learn more, run the command again with --verbose.
コンパイルエラーになった。
このように、変更可能な参照 &mut T
は RecoverSafe
ではないためクロージャーの境界を超えることはできない。なぜなら、変更可能な参照は、データが中途半端に更新された状態で panic が起こったときに、その内容が安全かどうか保証できなくなるからだ。例えば、今回のように配列の要素を順に更新している最中に panic が起きると、一部の要素は更新済みで、残りの要素は未更新で残ってしまう。
でも、このプログラムでは、panic を捕捉した後は、その中途半端に更新された値を使わずに捨てるのだから、なんら問題がない。そのことをコンパイラーに教えてあげよう。AssertRecoverSafe
でラップする。
#![feature(recover, std_panic)]
use std::panic;
use std::panic::AssertRecoverSafe;
fn remainders(number: i64, divisors: &Vec<i64>, mut rems: &mut Vec<i64>) -> Result<(), String> {
let mut rems = AssertRecoverSafe::new(&mut rems); // この行を追加
let result = panic::recover(move || {
for (i, div) in divisors.iter().enumerate() {
rems[i] = number % div;
}
});
// (以下、省略)
thread '<main>' panicked at 'attempted remainder with a divisor of zero', src/libcore/ops.rs:430
13 % [7, 3, 0, 5, 2] failed because "attempted remainder with a divisor of zero".
これでよし。
panic::recover() を FFI で試してみる
今度は少し実践的なユースケースとして、FFI を扱う。C のインターフェイスを通じて他言語から呼び出される Rust 関数を想定しよう。機能は先ほどと同じだが、引数の受け渡し方が異なる。divisors
と remainders
配列は、他言語側が用意したものに、生ポインター(*const T
や *mut T
)を通してアクセスする。
use std::slice;
extern crate libc;
use libc::c_int;
use libc::c_long;
#[no_mangle]
pub extern fn remainders(number: c_long, divisors_ptr: *const c_long, len: c_int,
rems_ptr: *mut c_long) -> c_int {
let divs_slice = unsafe { slice::from_raw_parts(divisors_ptr, len as usize) };
let rems_slice = unsafe { slice::from_raw_parts_mut(rems_ptr, len as usize) };
for (div_ptr, rem_ptr) in divs_slice.iter().zip(rems_slice.iter_mut()) {
(*rem_ptr) = number % (*div_ptr);
}
0
}
呼び出し元だが、わざわざ C で書くのも面倒なので、普通の Rust の main 関数で代用しよう。引数の形式だけ C の流儀に従った。
fn main() {
let number: c_long = 13;
let divisors: [c_long; 5] = [7, 3, 0, 5, 2];
let mut rems: [c_long; 5] = unsafe { std::mem::uninitialized() };
let divisors_ptr = divisors.as_ptr();
let rems_ptr = rems.as_mut_ptr();
let status = remainders(number, divisors_ptr, divisors.len() as c_int, rems_ptr);
if status == 0 {
println!("{} % {:?} = {:?}", number, divisors, rems);
} else {
println!("{} % {:?} failed.", number, divisors);
}
}
thread '<main>' panicked at 'attempted remainder with a divisor of zero', src/main.rs:31
Process didn't exit successfully: `target/debug/example` (exit code: 101)
ゼロ除算でクラッシュした。
安定版 Rust での方法:別スレッドを立てる、のは結構厳しい
use std::slice;
use std::thread;
extern crate libc;
use libc::c_int;
use libc::c_long;
#[no_mangle]
pub extern fn remainders(number: c_long, divisors_ptr: *const c_long, len: c_int,
rems_ptr: *mut c_long) -> c_int {
let h = thread::Builder::new().name("rem".to_owned()).spawn(move || {
let divs_slice = unsafe { slice::from_raw_parts(divisors_ptr, len as usize) };
let rems_slice = unsafe { slice::from_raw_parts_mut(rems_ptr, len as usize) };
for (div_ptr, rem_ptr) in divs_slice.iter().zip(rems_slice.iter_mut()) {
(*rem_ptr) = number % (*div_ptr);
}
0
});
match h {
Ok(h) => h.join().unwrap_or(1),
Err(_) => 1,
}
}
fn main() {
// 変更なし。省略
% cargo run
Compiling example v0.1.0 (file:///...)
src/main.rs:11:59: 19:7 error: the trait `core::marker::Send` is not implemented
for the type `*const i64` [E0277]
src/main.rs:11 let h = thread::Builder::new().name("rem".to_owned()).spawn(move || {
...
src/main.rs:11:59: 19:7 help: run `rustc --explain E0277` to see a detailed explanation
src/main.rs:11:59: 19:7 note: `*const i64` cannot be sent between threads safely
src/main.rs:11:59: 19:7 note: required because it appears within the type
`[closure@src/main.rs:11:65: 19:6
divisors_ptr:*const i64, len:i32, rems_ptr:*mut i64, number:i64]`
error: aborting due to previous error
Could not compile `example`.
To learn more, run the command again with --verbose.
コンパイルエラーになった。
生ポインターは、スレッド境界を超えることはできない。このようにコンパイルエラーになる。スレッド境界を超えるためには、型が Send + 'static
であることが要求される。Send
はスレッド間で安全に受け渡しができることを表すマーカー・トレイト。'static
はデータの寿命が特定のスコープに依存しないことを表す。
直すのが面倒なので、このまま次に進もう。
非安定版 panic::recover() を使う
仕様のところで説明したとおり、生ポインターは RecoverSafe
を実装している。そのため、panic::recover()
のクロージャー境界を超えることができる。
#![feature(recover, std_panic)]
use std::slice;
use std::panic;
extern crate libc;
use libc::c_int;
use libc::c_long;
#[no_mangle]
pub extern fn remainders(number: c_long, divisors_ptr: *const c_long, len: c_int,
rems_ptr: *mut c_long) -> c_int {
panic::recover(move || {
let divs_slice = unsafe { slice::from_raw_parts(divisors_ptr, len as usize) };
let rems_slice = unsafe { slice::from_raw_parts_mut(rems_ptr, len as usize) };
for (div_ptr, rem_ptr) in divs_slice.iter().zip(rems_slice.iter_mut()) {
(*rem_ptr) = number % (*div_ptr);
}
0
}).unwrap_or(1)
}
fn main() {
// 変更なし。省略
thread '<main>' panicked at 'attempted remainder with a divisor of zero', src/main.rs:18
13 % [7, 3, 0, 5, 2] failed.
ただ、生ポインターの場合は、コンパイラーがその安全性を保証することはできない。panic 時のデータの破壊や、メモリーリークについては、開発者自身が対策を行う必要がある。例えば、今回のように panic 後のデータを使わずに破棄する、といった対応が必要だ。
panic::recover() で捕捉できないケース
当然のことだが panic::recover()
は Rust 内で起こした panic しか捕捉できない。不正な値を持った生ポインターへのアクセスなどは、OS がプロセスを強制終了するので、捕捉は不可能だ。
fn main() {
...
// let rems_ptr = rems.as_mut_ptr();
let rems_ptr = unsafe { std::mem::uninitialized() };
% cargo run
Running `target/debug/example`
An unknown error occurred
To learn more, run the command again with --verbose.
% cargo run --verbose
Running `target/debug/example`
Process didn't exit successfully: `target/debug/example` (signal: 11)
このようなエラーはバグに起因するものなので、バグを直すのが先決だ。なお、FFI を使わない普通の Rust プログラムでは、生ポインターも unsafe
ブロックも登場しないので、全ての panic を捕捉できるだろう。
まとめ
- Rust RFC 1236 では、panic を捕捉する関数
std::panic::recover()
と、panic からの回復に対して安全なことを表すstd::panic::RecoverSafe
マーカー・トレイトを提案している - この機能は rustc 1.7 nightly で試すことができる
- 主に FFI や汎用的なスレッドプールでの利用を想定している
- Rust のベストプラクティスでは、panic に頼らないエラー処理を推奨している
個人的には、RFC 1236 は、データの安全性を保証しながらも panic を捕捉できる、実践的な機能に仕上がっていると感じた。
-
Rustは低レベルのプログラミングをサポートしているので、独自の例外処理をRust自身で実装することは可能だ。 ↩