30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

ArrayBuffer と TypedArray でバイナリデータを扱う

ArrayBufferTypedArrayはバイナリデータを扱うためのオブジェクトで、ES2015で標準化された。
この記事では、これらのオブジェクトの概要について述べたあと、Node.js でバイナリファイルを読み書きする方法についても説明する。

動作確認は Node.js のv10.9.0で行っている。

データとインターフェース

バイナリデータはArrayBufferオブジェクトで表現する。
new ArrayBuffer(バイト単位のサイズ)ArrayBufferのインスタンスを作ることが出来る。

const buf = new ArrayBuffer(8);
console.log(buf); // ArrayBuffer { byteLength: 8 }

しかし、ArrayBufferインスタンスの中身を見たり操作したりすることは出来ない。
インスタンス自身は何のプロパティも持たないし、プロトタイプであるArrayBufferprototypeにも、中身を操作するプロパティはない。byteLengthで長さを取得することくらいしか出来ない。
値は全て0なのだが、それを確認することも出来ない。

const buf = new ArrayBuffer(8);
console.log(buf); // ArrayBuffer { byteLength: 8 }

console.log(buf[0]); // undefined
console.log(Object.getOwnPropertyNames(buf)); // []

console.log(Object.getPrototypeOf(buf)); // ArrayBuffer {}
console.log(Object.getOwnPropertyNames(ArrayBuffer.prototype)); // [ 'constructor', 'byteLength', 'slice' ]
console.log(Object.getPrototypeOf(ArrayBuffer.prototype)); // {}

console.log(buf.byteLength); // 8

バイナリデータの中身を読み書きする際には、TypedArrayオブジェクトを使う。
例えば、TypedArrayを継承しているUint8ArrayArrayBufferインスタンスを渡してUint8Arrayインスタンスを作成すると、バイナリデータの中身を操作できるようになる。

const buf = new ArrayBuffer(8);
const ta = new Uint8Array(buf);
console.log(ta); // Uint8Array [ 0, 0, 0, 0, 0, 0, 0, 0 ]
console.log(ta[0]) // 0
ta[0] = 1;
console.log(ta[0]); // 1
console.log(ta); // Uint8Array [ 1, 0, 0, 0, 0, 0, 0, 0 ]

TypedArrayを直接使うことは出来ず、TypedArrayを継承しているオブジェクトを使う。

TypedArray // ReferenceError: TypedArray is not defined

TypedArrayを継承しているオブジェクトはUint8Array以外にも複数あり、バイナリデータを配列としてどのように表現するのかが異なる。
例えばUint8Arrayは8ビット(1バイト)ごとに符号なし整数で表現し、Uint16Arrayは16ビット(2バイト)ごとに符号なし整数で表現する。

const buf = new ArrayBuffer(8);
const u8 = new Uint8Array(buf);
const u16 = new Uint16Array(buf);

console.log(u8); // Uint8Array [ 0, 0, 0, 0, 0, 0, 0, 0 ]
console.log(u16); // Uint16Array [ 0, 0, 0, 0 ]

TypedArrayを継承しているオブジェクトの一覧はこのページで見ることが出来る。

TypedArrayのインスタンスはbufferプロパティを使うことができ、コンストラクタを初期化する際に渡したArrayBufferのインスタンスを参照している。

const typedArray = Object.getPrototypeOf(Uint8Array);
console.log(typedArray); // [Function: TypedArray]
console.log(typedArray.prototype.hasOwnProperty('buffer')); // true
console.log('buffer' in Uint8Array.prototype); // true

const buf = new ArrayBuffer(8);
const ta = new Uint8Array(buf);
console.log(ta.buffer === buf); // true

1つのArrayBufferに対して複数のTypedArrayを作ることが出来るが、どのbufferも同じものを参照している。
そして、いずれかのTypedArrayで行った操作の結果は、他のTypedArrayにも反映される。

const buf = new ArrayBuffer(8);
const arr1 = new Uint8Array(buf);
const arr2 = new Uint8Array(buf);
const arr3 = new Uint16Array(buf);

// それぞれ別の TypedArray インスタンスだが……

console.log(arr1 === arr2); // false
console.log(arr1 === arr3); // false
console.log(arr2 === arr3); // false

// 参照している ArrayBuffer は同じ

console.log(buf === arr1.buffer); // true
console.log(arr1.buffer === arr2.buffer); // true
console.log(arr1.buffer === arr3.buffer); // true
console.log(arr2.buffer === arr3.buffer); // true

console.log(arr1[0], arr2[0], arr3[0]); // 0 0 0
arr1[0] = 9;
console.log(arr1[0], arr2[0], arr3[0]); // 9 9 9

つまり、TypedArrayはバイナリデータのインターフェースを作成するものであり、インターフェースが複数あったとしても操作するバイナリデータは同じであると言える。

TypedArrayのインスタンスを作る際に数値を渡すと、その数の要素を持ったインスタンスが作られる。

console.log(new Uint8Array(1)); // Uint8Array [ 0 ]
console.log(new Uint8Array(2)); // Uint8Array [ 0, 0 ]

この書き方をするとArrayBufferのインスタンスの作成を暗黙的に行う。
つまり、new Uint8Array(1)new Uint8Array(new ArrayBuffer(1))は同じことを行っている。

const arr1 = new Uint8Array(1);
console.log(arr1); // Uint8Array [ 0 ]

const arr2 = new Uint8Array(arr1.buffer);
console.log(arr2); // Uint8Array [ 0 ]

console.log(arr1.buffer === arr2.buffer); // true

fs モジュールを使ってバイナリファイルを読み書きする

ここからは Node.js 固有の話。
以前、フロントエンドでのバイナリファイルの取り扱いについて書いたが、それの Node.js 版である。

numb86-tech.hatenablog.com

バイナリファイルの出力は単にfs.writeFileTypedArrayを書き出せばいい。

const fs = require('fs');

const arr = new Uint8Array(4);
arr[0] = 4;
arr[1] = 3;
arr[2] = 2;
arr[3] = 1;

fs.writeFile('./foo', arr, err => {
  if (err) throw err;
  console.log('done!');
});

hexdumpコマンドで確認すると、正しく出力されているのを確認できる。

$ hexdump foo
0000000 04 03 02 01                                    
0000004

ファイルの読み込みに使うのはfs.readFile
テキストファイルを読み込むときはfs.readFile(ファイルパス, エンコーディング, コールバック関数)だが、バイナリファイルのときはエンコーディングは指定せずfs.readFile(ファイルパス, コールバック関数)とする。

const fs = require('fs');

fs.readFile('./foo', (err, result) => {
  if (err) throw err;
  console.log(result); // <Buffer 04 03 02 01>
});

ファイルの内容をもとにBufferインスタンスが作られる。
BufferArrayBufferとは別のもので、EcmaScript で定義されたものではない。そのためブラウザ環境には存在しない。

const fs = require('fs');

fs.readFile('./foo', (err, result) => {
  if (err) throw err;
  console.log(result instanceof Buffer); // true
  console.log(result instanceof ArrayBuffer); // false
});

実はBufferUint8Arrayを継承しており、そのため、値を操作することが出来る。

const fs = require('fs');

fs.readFile('./foo', (err, result) => {
  if (err) throw err;
  console.log(result instanceof Uint8Array); // true
  console.log(result[0]); // 4
  result[0] = 9;
  console.log(result); // <Buffer 09 03 02 01>
});

参考資料