研究開発グループの大久保です。 当社の製品の中にはC/C++で書かれたものが存在し、その中には独自のバイナリフォーマットを取り扱うものが存在します。既存のコードとやり取りするようなRustのプロジェクトを起こすためには、その独自のバイナリフォーマットをRustで取り扱えるようにしなければなりません。しかしながら、Rustの標準ライブラリの機能だけでは、バイナリの読み書きは意外と面倒になります。そのため、今回はRustでバイナリを扱うのならぜひ知っておきたいクレートを3つご紹介します。 byteorder byteorder はその名の通り、バイトオーダ、つまりエンディアンを扱うためのクレートです。使い方はシンプルで、 ByteOrder トレイトと、 BigEndian, LittleEndian, NativeEndian のうち自分が扱いたいエンディアンをインポートすれば、バッファと数値型の間で読み書きを行うことができます。 例えば、長さ4バイトのバッファから32bit整数型を読み出す場合、次のようになります。 use byteorder :: {BigEndian, LittleEndian, NativeEndian, ByteOrder}; fn main () { let buf = [ 0 , 0 , 0 , 42 ]; let a = LittleEndian :: read_u32 ( & buf); assert_eq! (a, 704643072 ); let a = BigEndian :: read_u32 ( & buf); assert_eq! (a, 42 ); let a = NativeEndian :: read_u32 ( & buf); assert_eq! (a, 704643072 ); } 32bit整数型の書き込みは次のようになります。 use byteorder :: {BigEndian, LittleEndian, NativeEndian, ByteOrder}; fn main () { let mut buf = [ 0 ; 4 ]; LittleEndian :: write_u32 ( &mut buf, 42 ); assert_eq! (buf, [ 42 , 0 , 0 , 0 ]); BigEndian :: write_u32 ( &mut buf, 42 ); assert_eq! (buf, [ 0 , 0 , 0 , 42 ]); NativeEndian :: write_u32 ( &mut buf, 42 ); assert_eq! (buf, [ 42 , 0 , 0 , 0 ]); } NativeEndian はこれを実行しているプラットフォームのエンディアンを示します。 bytes bytes は、Rust用の非同期ライブラリ tokio で使われているバイナリ操作用のクレートです。 Buf と BufMut というトレイトを導入することで、バイナリ読み書きのためのメソッドを利用することができます。 例として、用意したデータの先頭から順に整数を読み出していきます。 use bytes :: Buf; fn main () { let data = [ b'a' , 0 , 33 , 42 , 0 ]; let mut p = & data[..]; assert_eq! (p. get_u8 (), b'a' ); // 0バイト目を8bit整数として読み出し assert_eq! (p. get_u16 (), 33 ); // 1〜2バイト目をビッグエンディアン16bit整数として読み出し assert_eq! (p. get_u16_le (), 42 ); // 3〜4バイト目をリトルエンディアン16bit整数として読み出し } Vec に順番に整数を書き込んでいくこともできます。 use bytes :: BufMut; fn main () { let mut buf = Vec :: new (); buf. put_u8 ( b'r' ); // 8bit整数を書き込み buf. put_u8 ( b'u' ); buf. put_u8 ( b's' ); buf. put_u8 ( b't' ); buf. put_u16 ( 0xFFEE ); // ビッグエンディアンとして16bit整数を書き込み buf. put_u16_le ( 0x1122 ); // リトルエンディアンとして16bit整数を書き込み assert_eq! (buf, [ b'r' , b'u' , b's' , b't' , 0xFF , 0xEE , 0x22 , 0x11 ]); } 読み書きどちらも関数名の後ろに _le を付けるとリトルエンディアン扱いになります。バッファの先頭から読み書きしていくようなデータ構造の場合、bytesはなかなか便利なクレートと言えるでしょう。 nom nom はRust用のパーサコンビネータライブラリです。nomが提供するパース用の関数を組み合わせて、対象となるフォーマット用のパーサを作り上げるようにして使います。nomのexampleに示されているのは、テキスト( &str )のパースですが、バイナリ( &[u8] )のパースにも使えます。 例えば、先頭から順にバイナリを読んでいき、結果を MyData 構造体に格納していく場合は次のようになります。 use nom :: IResult; use nom :: number :: complete :: {be_u8, be_u32}; /// 読み込みたいデータ #[derive( PartialEq , Debug )] struct MyData { a: u8 , b: u8 , c: u32 , d: u32 , } /// バイナリをパースしてMyDataを読み込む関数 fn parse_mydata (input: & [ u8 ]) -> IResult < & [ u8 ], MyData > { let (input, a) = be_u8 (input)?; let (input, b) = be_u8 (input)?; let (input, c) = be_u32 (input)?; let (input, d) = be_u32 (input)?; Ok ((input, MyData { a, b, c, d })) } fn main () { let data = [ 10 , 20 , 0 , 0 , 0 , 30 , 0 , 0 , 0 , 40 ]; let mydata = parse_mydata ( & data). unwrap (). 1 ; assert_eq! ( mydata, MyData { a: 10 , b: 20 , c: 30 , d: 40 , } ); } 基本的にnomにおけるパーサ関数は、パースしたい領域のスライスを受け取り、読み残しのスライスと読み取った結果のタプルを返します。そのため、返り値のスライスを読み取れば、先頭から順に値を読み込んでいくことができます。 先頭から順に読んでいくだけならbytesでも可能ですが、nomの関数を使えば複雑な構造のバイナリを読み取ることも可能です。例えば、先頭に mydata というマジックナンバーがついているバイナリをパースしたい場合は、 tag を使うことができます。 use nom :: bytes :: complete :: tag; use nom :: number :: complete :: {be_u32, be_u8}; use nom :: IResult; /// 読み込みたいデータ #[derive( PartialEq , Debug )] struct MyData { a: u8 , b: u8 , c: u32 , d: u32 , } /// バイナリをパースしてMyDataを読み込む関数 fn parse_mydata (input: & [ u8 ]) -> IResult < & [ u8 ], MyData > { let (input, _) = tag ( b"mydata" )(input)?; // inputの先頭6バイトが"mydata"かどうか確かめる let (input, a) = be_u8 (input)?; let (input, b) = be_u8 (input)?; let (input, c) = be_u32 (input)?; let (input, d) = be_u32 (input)?; Ok ((input, MyData { a, b, c, d })) } fn main () { let data = [ b'm' , b'y' , b'd' , b'a' , b't' , b'a' , 10 , 20 , 0 , 0 , 0 , 30 , 0 , 0 , 0 , 40 , ]; let mydata = parse_mydata ( & data). unwrap (). 1 ; assert_eq! ( mydata, MyData { a: 10 , b: 20 , c: 30 , d: 40 , } ); } また、このデータの末尾に、ヌル終端のASCII文字列が格納されていた場合を考えてみます。この場合、 take_until を使うことで、0が現れるまでのスライスを取得することができます。 use nom :: bytes :: complete :: {tag, take_until}; use nom :: number :: complete :: {be_u32, be_u8}; use nom :: IResult; /// 読み込みたいデータ #[derive( PartialEq , Debug )] struct MyData { a: u8 , b: u8 , c: u32 , d: u32 , id: Vec < u8 > , } /// バイナリをパースしてMyDataを読み込む関数 fn parse_mydata (input: & [ u8 ]) -> IResult < & [ u8 ], MyData > { let (input, _) = tag ( b"mydata" )(input)?; let (input, a) = be_u8 (input)?; let (input, b) = be_u8 (input)?; let (input, c) = be_u32 (input)?; let (input, d) = be_u32 (input)?; let (input, id) = take_until ( & b" \0 " [..])(input)?; // ヌル文字が現れるまでのデータを取得 Ok (( input, MyData { a, b, c, d, id: id. to_vec (), }, )) } fn main () { let data = [ b'm' , b'y' , b'd' , b'a' , b't' , b'a' , 10 , 20 , 0 , 0 , 0 , 30 , 0 , 0 , 0 , 40 , b'a' , b'b' , b'c' , b'd' , b'e' , b'f' , 0 , ]; let mydata = parse_mydata ( & data). unwrap (). 1 ; assert_eq! ( mydata, MyData { a: 10 , b: 20 , c: 30 , d: 40 , id: b"abcdef" . to_vec (), } ); } 他にもnomにはいろいろな機能が用意されていますので、うまく使えばもっと多様なフォーマットにも対応できます。 最後に Rustでちょっと凝ったことをすると、標準ライブラリ以外のクレートが必要になることが多く、適したクレートを探すのは少し大変です。そのため、今回はバイナリを扱う場合に必須になりそうなクレートをご紹介しました。Rustはその適用範囲上、バイナリを扱うことも多いかと思いますので、この記事がご参考になれば幸いです。