JavaScript
js
メモリ
バイナリ

[基礎編]JavaScriptでバイナリデータを扱ってみる

BRIGHT VIE Advent Calendar 2017 - Qiita 3日担当の社会人6年目 @megadreams14 です。

昨日は弊社社長の初!投稿記事 「IonicPage採用時のHTMLタグ拡張(Custom Components)実装について (1/2)」 でしたね。

よく似た画面構成やパーツなどをCustom Componentsによって共通化することで、より開発効率が上がりそうですね!
次回、実際にCustom Componentsを追加する手順の記事が楽しみです。

JavaScriptでバイナリデータ!?

弊社では、AngularとCordovaがベースとなったIonicを利用してアプリの開発を行っており、
その際にBluetoothを利用してデバイスと連携する機能の開発を行うことがあり、
バイナリデータをJavaScriptで扱う必要があったので、そのときの開発メモとなります。

なお、Bluetoothから取得したデータの解析みたいな部分は明日以降の記事で執筆しようと思っており、
また、IonicでのBluetoothを扱う方法は、「Ionic Advent Calendar 2017 - Qiita」で後日執筆する予定ですので、そちらの記事もお楽しみに!

前提知識

メモリ単位

メモリ上では、8ビット=1バイトの単位で管理されており、
2進数表記で8桁で内部状態が表現されたりしております。

例えば、「10」「50」「100」の状態を図に表すとそれぞれ下記のようになります。

メモリの管理.png

2,10,16進数については下記の記事が分かりやすいかもです。
「図解」 2進数=10進数=16進数 変換する為の計算方法!

また、数値を扱う際には、

  • 何ビットで管理するのか? (8ビット?, 16ビット?, 32ビット?)
  • 符号はあるの?ないの? (正の数(+)だけ表すの? 負の数(-)も表すの?)
  • 整数値だけ?小数点も扱うの? (整数値で管理?、浮動小数点で管理?)

などなど、扱いたいデータの範囲に合わせて保存する形式を選ぶことが可能です。

例) 整数型の一覧

サイズ 範囲
符号付き 8 ビット整数 -128 ~ 127
符号付き 16 ビット整数 -32,768 ~ 32,767
符号付き 32 ビット整数 -2,147,483,648 ~ 2,147,483,647
符号付き 64 ビット整数 -9,223,372,036,854,775,808 〜 9,223,372,036,854,775,807
符号なし 8 ビット整数 0 ~ 255
符号なし 16 ビット整数 0 ~ 65,535
符号なし 32 ビット整数 0 ~ 4,294,967,295
符号なし 64 ビット整数 0 ~ 18,446,744,073,709,551,615

JavaScriptでバイナリ形式の数値を扱う場合

JavaScriptでは、上記のデータ形式を扱うために後ほど説明する
DataViewクラスののgetter/setterメソッドとして下記のようなものがあります。

意味 getterメソッド setterメソッド
Int8 符号付き 8ビット整数を扱う getInt8 getInt8
Int16 符号付き16ビット整数を扱う getInt16 getInt16
Int32 符号付き32ビット整数を扱う getInt32 getInt32
Uint8 符号なし 8ビット整数を扱う getUint8 getUint8
Uint16 符号なし16ビット整数を扱う getUint16 getUint16
Uint32 符号なし32ビット整数を扱う getUint32 getUint32
Float32 32ビット浮動小数点数を扱う getFloat32 setFloat32
Float64 64ビット浮動小数点数を扱う getFloat64 setFloat64

などが準備されています。

バイトオーダー

他にも2バイト以上の数値をメモリ上に格納する時の並び順を表すバイトオーダーと呼ばれるものがあり、
メモリ上に上位バイトから順に格納する「ビッグエンディアン方式」と
下位バイトから順に格納する「リトルエンディアン方式」
の2種類の方式があります。

それぞれのバイトオーダーには、メリット・デメリットがありますがここでは詳しく説明はせず、
またはプロセッサによって変化するため扱うバイナリデータの仕様書などを確認して見て下さい。

実際にJavaScriptで扱ってみる

JavaScriptでバイナリを扱うためには、「ArrayBuffer」「DataView」「TypedArray」の3種類の仕様があります。

簡単に説明すると下記のようになります。

  • ArrayBuffer
    • 物理メモリ上に領域を確保するためのクラス
    • このクラスでは、確保するのみで、バッファに対してデータをセットしたり読み出す操作は出来ない
    • バッファに対してデータを操作するには、「TypedArray」か「DataView」を利用する
  • TypedArray
    • 型を指定してバッファから配列を生成する
    • 通常の配列のようにバッファにアクセス可能
    • 「高速」に読み書きが出来る
  • DataView
    • TypedArrayよりも高機能なクラス
    • バイト数を適切に設定出来る便利な機能が備わっている
      • 前述した Uint32 や Float64 を簡単に扱うことが出来る

詳細は下記記事がわかりやすいです。
[JavaScript] ArrayBufferについて調べてみた)

今回は、ArrayBufferとDataViewを利用してバイナリデータを操作してみます。

メモリを確保、データをセット

今日は、12月3日なので、数字の12と3をそれぞれメモリ上に保存してみます。

図にすると下記のような形です。

12と3をメモリ上に保存.png

今回は符号なしでデータを扱います。そのため、上記をプログラムで記載すると

// 1バイト(8ビット) + 1バイト(ビット) = 2バイト(16ビット)のバッファ領域を確保
let buffer = new ArrayBuffer(2);

// バッファに対してデータの書き込みが出来るようにDataViewクラスを初期化
let dv = new DataView(buffer);

// DataViewにデータをセットするには、setXXX メソッドを利用する。
// XXXには、扱いたいメモリ上への保存形式によって変更する
// 今回は「符号なし 8ビット整数」を扱っているため setUint8 を利用する

// 1バイト目に12をセット
dv.setUint8(0, 12);

// 2バイト目に3をセット
dv.setUint8(1, 3);

DataViewのセッターは、何バイト目から記録するのかのバイトオフセットと設定したい値を引数に渡すことで保存することが出来ます。
また実際に取り出すときにもセットした保存形式に合わせてデータを取得することでバイナリデータを取り出すことが出来ます。

// 1バイト目に設定した値を取得する
let month = dv.getUint8(0);
console.log(month);              // 10進数で、  12が表示される
console.log(month.toString(2));  //  2進数で、1100が表示される

// 2バイト目に設定した値を取得する
let day = dv.getUint8(1);
console.log(day);              // 10進数で、   3が表示される
console.log(day.toString(2));  //  2進数で、  11が表示される

日付をバイナリデータで保存してみる

上記に続いて今日の日付をバイナリデータで扱ってみましょう。

Bluetoothのデータを扱うときを参考にしつつ、
今回は日付の保存形式の仕様を下記とします。

開始バイト位置 項目 バイト数 形式 バイトオーダー
0 2バイト 符号なし整数 リトルエンディアン
2 1バイト 符号なし整数 -
3 1バイト 符号なし整数 -

図に表すとこのような形で保存されることになります。

バイナリ_2017年12月3日の場合.png

これだけ見ると凄く難しそうですが、JavaScriptで上記を実現する方法は以外と簡単です。

// 4バイト分のバッファ領域を確保
let buffer = new ArrayBuffer(4);

// バッファに対してデータの書き込みが出来るようにDataViewクラスを初期化
let dv = new DataView(buffer);


// 1. 年(2017)を保存してみる
//  - 扱うデータは「符号なし16ビット整数」なので、Uint16を使います
//  - またバイトオーダーは、「リトルエンディアン」なので、setUint16の第3引数にtrueを渡す
dv.setUint16(0, 2017, true);

// 2, 月(12)を保存する
//  - 扱うデータは「符号なし8ビット整数」なので、Uint8を使います
//  - 年で2バイト利用しているので、月の保存は3バイト目からです。
dv.setUint8(2, 12);

// 3, 日(3)を保存する
//  - 扱うデータは「符号なし8ビット整数」なので、Uint8を使います
//  - 4バイト目に保存するので、配列の添字は3を指定します。
dv.setUint8(3, 3);

なんとたったの5行で出来ちゃいました。
最初何も知らない状態のときは、ビット演算とかやらなあかんのかぁーと思っていましたが、
JavaScriptさんバイナリデータの扱いに優しい気がします。(他の言語も同じかもしれないですが...)

本当に正しくデータが保存されているのかバイナリデータの中身を覗いてみましょう。
(比較対象は説明図のような2進数の数値の並びになっていればOKです)

// 1バイトずつどのような数値の並びになっているか2進数で見てみる
console.log(dv.getUint8(0).toString(2)); // 出力: 11100001
console.log(dv.getUint8(1).toString(2)); // 出力: 00000111
console.log(dv.getUint8(2).toString(2)); // 出力: 00001100
console.log(dv.getUint8(3).toString(2)); // 出力: 00000011

はい、図のようになっていますね!

なおこのバイナリデータから正しい日付でデータを10進数で取り出すには、下記のようにすれば可能です。

// getterの場合、バイトオーダーがリトルエンディアンの場合は第2引数にtrueを渡す
let year = dv.getUint16(0, true);
console.log(year);              // 10進数で、2017 が表示される

let month = dv.getUint8(2);
console.log(month);             // 10進数で、  12が表示される

let day = dv.getUint8(3);
console.log(day);              // 10進数で、   3が表示される

まとめ

高校生のときに初級シスアド(今はITパスポートかな)を取得した際に勉強したことが今になってかなり役立ったという感じでした!
そのためか、思っていた以上にJavaScriptでバイナリデータを扱うことが簡単であり、他の言語ってどうなのだろうというのが気になりました。

実際は、プログラム言語よりも、バイトオーダーや符号ありなしなどの概念を理解できていれば、比較的スムーズに理解できるのかもしれませんね。

さて、明日は、浮動小数点や実際にIEEEで定められているBluetoothの仕様を元にバイナリデータをJSで扱ってみようと思います。

参考