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;
#[no_mangle]
pub extern "C" fn db_error_description(e: *const DatabaseError) -> *mut libc::c_char {
let error: &DatabaseError = unsafe {
// SAFETY: ポインタの寿命が現在のスタックフレームより大きい
&*e
};
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 c_error = unsafe {
// SAFETY: error_strを、アローケートしたバッファに
// NUL文字終端を付けてコピーする
let mut malloc: *mut u8 = libc::malloc(error_str.len() + 1) as *mut _;
if malloc.is_null() {
return std::ptr::null_mut();
}
let src = error_str.as_bytes().as_ptr();
std::ptr::copy_nonoverlapping(src, malloc, error_str.len());
std::ptr::write(malloc.add(error_str.len()), 0);
malloc as *mut libc::c_char
};
c_error
}
}
カスタムエラー型
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言語に簡単に変換できない型もあります。