FFIにおけるエラーハンドリング
説明
C言語のような外部言語では、エラーはリターンコードで表現されます。しかし、Rustの型システムでは、より豊富なエラー情報を完全な型として捕捉し、伝播させることができます。
このベストプラクティスでは、さまざまな種類のエラーコードと、それらを使いやすい形で公開する方法を示します:
- フラットな列挙型は整数に変換し、コードとして返すべきです。
- 構造化された列挙型は、詳細情報として文字列エラーメッセージを持つ整数コードに変換すべきです。
- カスタムエラー型は、C表現を持つ「透過的」なものにすべきです。
コード例
フラットな列挙型
enum DatabaseError {
IsReadOnly = 1, // user attempted a write operation
IOError = 2, // user should read the C errno() for what it was
FileCorrupted = 3, // user should run a repair tool to recover it
}
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), // message describing the issue
}
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: we assume that the lifetime of `e` is greater than
// the current stack frame.
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: copying error_bytes to an allocated buffer with a '\0'
// byte at the end.
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 {
/* ... */
}
/* Create a second version which is exposed as a C structure */
#[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言語に簡単には変換できない場合があります。