m13o.net

2020-12-10 Thu 23:00
Rustでwavを解析する その3   AdventCalendar2020 Rust programming

前回の終わりに, エラー関連についてもう少しちゃんと考えないといけないなという気付きを得たので, 今回はwavをパースする上でのエラー対応について考えてみようと思います.

このあたりでも軽く触れましたが, RustにおけるErrorは歴史的経緯による紆余曲折があった結果, std::error::Error traitをベースにして実装していくのが良さそうです.

今回は内部的にbinary_reader::BinaryReader structを利用して解析していくので, BufferOverRunErrorが原因でエラーとなる場合と, 読み取りは成功したが読み取った値が誤っている事が原因でエラーとなる場合と, 原因の異なるエラーが発生する可能性があります.

例えばparse()というメソッドの中でBinaryReaderを利用しつつ解析処理をするとした時, 解析結果をユーザーに任せるために, 戻り値はResult<T, E>を指定すると思います. Tは解析結果を格納する型であるとして, Eをどうするか.

std::error::Error traitとして戻り値とする場合. こうすると, BufferOverRunErrorだろうがそうではないエラー型だろうが, std::error::Errorを実装している限りは何であっても返す事ができ, ユーザーは個別のエラーに対して対応する事になります.

std::error::Error traitを実装している特定のエラー型を返す場合, そのメソッドの内部で発生したエラーの原因によっては, ユーザーがその原因を辿れるようにするために, source()メソッドの実装が必要となります.

どちらも一長一短あるのでケースバイケースだとは思いますが, 今回は結構プリミティブな物を作ろうとしているというのもあるので, std::error::Error traitを返す形で実装してみようと思います.

というわけで, まずは, BinaryReaderの時と同様, error.rsというファイルを作り, lib.rsの先頭に以下を追記します.

pub mod error;

次にerror.rsに以下のようにエラーを実装します.

use std::error::Error;
use std::fmt::{Display, Formatter};

#[derive(Debug, Eq, PartialEq)]
pub enum RiffParseError {
    InvalidChunkFourCC(u32),
}

impl Display for RiffError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            RiffParseError::InvalidChunkFourCC(_) => {
                write!(f, "無効なFourCCを持ったチャンクです.")
            }
        }
    }
}

impl Error for RiffParseError {
}


mod tests {
    use super::*;

    #[test]
    fn display_invalid_chunk_four_cc() {
        let error = RiffParseError::InvalidChunkFourCC(0xDEADBEAF);
        assert_eq!(format!("{}", error), "無効なFourCCを持ったチャンクです.");
    }
}

今回はRiffParseErrorというenumにエラーの要素を追加していく形にしてみました. Debug traitの実装はderiveに任せて, Displayの実装のみを行っています. std::error::Error traitの実装もデフォルトの実装に任せる形です.

そして前回実装した parse_chunk_header()を以下のように変更します.

fn parse_chunk_header(&mut self) -> Result<RiffCommonHeader, Box<(dyn Error + 'static)>> {
    let (four_cc, _rest) = self.reader.read_le_u32()?;
    let (chunk_size, _rest) = self.reader.read_le_u32()?;
    Ok(RiffCommonHeader {
        four_cc,
        chunk_size
    })
}

まず戻り値ですが, Result<RiffCommonHeader, BufferOverRunError> だったものが, Result<RiffCommonHeader, Box<(dyn Error + 'static)>> 型になっています.

std::error::Errorはtraitなので, 型のサイズが不明です. そのため, Box<T>型のTにstd::error::Errorを指定して, あくまで返すのはBox<T>型であるという事を示す事でコンパイラにSizedである事をわかってもらいます.

'static 指定が入っていますが, これは, 最大でも'staticのライフタイムまで生存するという事を示しています. 場合によってはアプリケーションを終了させなければならない原因のエラーになるかもしれないので, 最大でもそこまでは生存しているという事を示します. 最大でも'staticまで生存するなので, それより前にDropしても問題ありません.

また, 実装の中身も少し変えています. ? を利用してすっきりさせました. ネストが深いと色々大変なのでこういう簡素に書けるようになったRustは素晴らしいですね.

本当はSend, Syncをそれぞれtrait境界として入れた方がいいのでしょうが, 気が向いたらその内やろうかなと思います(YAGNIの濫用).