FFI におけるエラー処理
説明
C のような外部言語では、エラーは戻りコードで表現されます。しかし、 Rust の型システムでは、はるかに豊かなエラー情報を完全な型としてキャプチャし、 伝播できます。
このベストプラクティスでは、さまざまな種類のエラーコードと、それらを 利用しやすい形で公開する方法を示します。
- フラットな列挙型は整数に変換し、コードとして返すべきです。
- 構造化された列挙型は、詳細を表す文字列エラーメッセージを伴う整数コードに 変換すべきです。
- カスタムエラー型は、C 表現を持つ「透過的」なものにすべきです。
コード例
フラットな列挙型
enum DatabaseError {
IsReadOnly = 1, // ユーザーが書き込み操作を試みた
IOError = 2, // ユーザーは原因を知るために C の errno() を読むべき
FileCorrupted = 3, // ユーザーは修復ツールを実行して復旧すべき
}
impl From<DatabaseError> for libc::c_int {
fn from(e: DatabaseError) -> libc::c_int {
(e as i8).into()
}
}
構造化された列挙型
pub mod errors {
enum DatabaseError {
IsReadOnly,
IOError(std::io::Error),
FileCorrupted(String), // 問題を説明するメッセージ
}
impl From<DatabaseError> for libc::c_int {
fn from(e: DatabaseError) -> libc::c_int {
match e {
DatabaseError::IsReadOnly => 1,
DatabaseError::IOError(_) => 2,
DatabaseError::FileCorrupted(_) => 3,
}
}
}
}
pub mod c_api {
use super::errors::DatabaseError;
use core::ptr;
#[no_mangle]
pub extern "C" fn db_error_description(
e: Option<ptr::NonNull<DatabaseError>>,
) -> Option<ptr::NonNull<libc::c_char>> {
// SAFETY: `e` のライフタイムが現在のスタックフレームよりも
// 長いと仮定します。
let error = unsafe { e?.as_ref() };
let error_str: String = match error {
DatabaseError::IsReadOnly => {
format!("cannot write to read-only database")
}
DatabaseError::IOError(e) => {
format!("I/O Error: {e}")
}
DatabaseError::FileCorrupted(s) => {
format!("File corrupted, run repair: {}", &s)
}
};
let error_bytes = error_str.as_bytes();
let c_error = unsafe {
// SAFETY: error_bytes を末尾に '\0' バイトを持つ
// 割り当て済みバッファにコピーします。
let buffer = ptr::NonNull::<u8>::new(libc::malloc(error_bytes.len() + 1).cast())?;
buffer
.as_ptr()
.copy_from_nonoverlapping(error_bytes.as_ptr(), error_bytes.len());
buffer.as_ptr().add(error_bytes.len()).write(0_u8);
buffer
};
Some(c_error.cast())
}
}
カスタムエラー型
struct ParseError {
expected: char,
line: u32,
ch: u16,
}
impl ParseError {
/* ... */
}
/* C 構造体として公開される第 2 のバージョンを作成する */
#[repr(C)]
pub struct parse_error {
pub expected: libc::c_char,
pub line: u32,
pub ch: u16,
}
impl From<ParseError> for parse_error {
fn from(e: ParseError) -> parse_error {
let ParseError { expected, line, ch } = e;
parse_error { expected, line, ch }
}
}
利点
これにより、外部言語はエラー情報に明確にアクセスできるようになり、 同時に Rust コードの API をまったく損ないません。
欠点
多くの記述が必要であり、型によっては C に簡単に変換できない場合があります。