LoginSignup
2
1

More than 3 years have passed since last update.

[Rust] 数値データをバイナリデータとして扱う

Posted at

メモリに保持している数値データをバイナリ形式で出力するとか, 逆にバイナリデータを読み込んできて数値にキャストするとかしたくなることがあります. 例えば大規模な数値シミュレーションの中間データを出力しておきたいとかですね. このような操作は unsafe Rust ならば可能です.

注意: unsafe なコードを書くときは細心の注意を払ってください.

整数型は std がサポートしている

i32, usize 等の整数型は標準ライブラリに from_be_bytes(), from_le_bytes(), from_ne_bytes(), to_be_bytes(), to_le_bytes(), to_ne_bytes() というメソッドが容易されているのでこれを使えばよいです. (Rust 1.32 以上)

use std::i32;

fn main() {
    let x: i32 = 1;
    let b: [u8;4] = x.to_le_bytes();
    println!("{:?}", b);
    let y: i32 = i32::from_le_bytes(b);
    assert_eq!( x, y );
}

データをバイナリに変換

浮動小数点数はバイナリ列を扱うメソッドは標準ライブラリは提供していない (f64u64 を変換する from_bits(), to_bits() はある) ので, 自力で何とかしましょう. 何らかのデータをスタックに保持しているとします. 例えば f64 なら

let x: f64 = 1.;

という感じですね. このデータは 8 byte ですから, バイナリデータの観点からは [u8;8] と等価です. そこで, このデータを指す生ポインタ (raw pointer) を取得し, それを [u8;8] を指すポインタと解釈し直してみます.

let p = &x as *const f64 as *const [u8;8];

一応説明しておくと, &x は上で確保した f64 を指す参照で, それをポインタ型 *const f64 へキャストした上で, 異なる型のデータを指すポインタ型 *const [u8;8] と読み替えています. 生ポインタはメモリ上の住所に過ぎず, その値は最初に変数 x を束縛した時点で決まっていますから, 生ポインタを取得するだけなら危険なことは起こりません. 従ってこの段階までは unsafe なしで大丈夫です. 例えば, 続けて

println!("{:?}", p);

とすると生ポインタのアドレスが出力されます.

問題は生ポインタが指すデータを読み出す (deref する) ときに発生します. 読み出す先のメモリが正しく初期化されているか, また意味のあるデータが存在しているかは保証されないので, deref は unsafe ブロックのなかでしかできません. 従って, f64 型のデータ x のバイナリ表現は

let b: [u8;8] = unsafe { *p };

という形で取得することになります. いったん取得してしまえば通常の safe Rust です.

まとめると, 浮動小数点数のバイナリ表現を表示するコードは次のようになります.

let x: f64 = 1.;
let p = &x as *const f64 as *const [u8;8];
let b: [u8;8] = unsafe { *p };
println!("{:?}", b );

何度でも強調しておきますが, 生ポインタの deref は unsafe です. 例えば上のコードの f64f32 に書き直したとして, それなのに [u8;8][u8;4] に直すのを忘れたとしたら何が起こるでしょう? unsafe ブロック内にそのようなコードが含まれていてもコンパイルは通ってしまいます.

バイナリデータを数値データに変換

次に, 何らかのバイナリデータが与えられたときに, それを数値データとして正しく解釈し直す方法を考えます. とはいえ, これは上の逆のことを行うだけなので, コード例を載せれば十分でしょう.

let b: [u8;8] = [0, 0, 0, 0, 0, 0, 240, 63]; // Little Endian
let p = &b as *const [u8] as *const f64;
let x = unsafe { *p };
println!("{}", x );

言うまでもないですが, バイナリデータを扱う際には エンディアン に注意してください.

Vec<T> をバイナリデータとして読む

Vec<T> のポインタは &Vec<T> ではなく .as_ptr() メソッドにより取得するという点のみ注意すれば大丈夫です. 例えば:

let vec: Vec<f64> = vec![ 0., 2., 4., 8. ];
let p = vec.as_ptr() as *const u8;

for i in 0..8*vec.len() as isize {
    print!("{:?} ",
           unsafe { *p.offset(i) }
    );
}
let vec: Vec<u8> = vec![ 0, 0, 0, 0, 0, 0, 240, 63 ]; // Little Endian
let p = vec.as_ptr() as *const f64;

println!("{}",
         unsafe { *p } );

例によってアクセスするメモリの範囲には注意してください. これは unsafe Rust です. (逆に言うと C 言語でいかに危険なコードを平気で生産していたか... Fortran もすぐセグフォが出るし)

byteorder

長々と書きましたが, バイナリデータを扱うならば生ポインタと unsafe を駆使するのではなく byteorder クレートを使うのが楽です. 変換の際にエンディアンを指定する機能もついてきます.

2
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1