Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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;
    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 に簡単に変換できない場合があります。