研究開発グループの大久保です。
当社の製品の中には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はその適用範囲上、バイナリを扱うことも多いかと思いますので、この記事がご参考になれば幸いです。