FFI のエラー処理

説明

C言語のような言語では、エラーはリターンコードで表されます。しかしRustの型システムは、よりリッチなエラー情報を完全な型を通して捕捉、伝播することが可能です。

このベストプラクティスでは、さまざまな種類のエラーコードを示し、どのようにそれらを扱いやすい方法で公開するかを示します:

  1. フラットな列挙型は整数に変換してコードとして返します。
  2. 構造化された列挙型は、詳細についての文字列のエラーメッセージとともに、整数コードに変換されるべきです。
  3. カスタムエラー型は、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言語に簡単に変換できない型もあります。

Last change: 2024-07-09, commit: 317c88e