メモリに保持している数値データをバイナリ形式で出力するとか, 逆にバイナリデータを読み込んできて数値にキャストするとかしたくなることがあります. 例えば大規模な数値シミュレーションの中間データを出力しておきたいとかですね. このような操作は 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 );
}
データをバイナリに変換
浮動小数点数はバイナリ列を扱うメソッドは標準ライブラリは提供していない (f64
と u64
を変換する 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 です. 例えば上のコードの f64
を f32
に書き直したとして, それなのに [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 クレートを使うのが楽です. 変換の際にエンディアンを指定する機能もついてきます.