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>
});

参考資料

既存プロジェクトを TypeScript に移行する際の ESLint の対応

JavaScript で書いていたプロジェクトを TypeScript に移行する場合、アノテーションの追加やビルドの設定の他、ESLint の対応も必要になる。
この記事では、TypeScript に移行した後も引き続き ESLint を使えるようにするための手順を書いていく。
方針として、ESLint のルールは既存のものをそのまま引き継ぎ、TypeScript 用のルールは採り入れない。まずは移行を完了させてから必要に応じて適宜ルールを追加していく、という考え方。

前提

eslint-config-airbnbを使用しており、.eslintrcが以下の状態になっていると仮定する。

{
  "extends": "airbnb"
}

パーサーの設定

デフォルトのパーサーでは TypeScript の記法をパースできないので、@typescript-eslint/parserをパーサーとして設定する。

インストールして、.eslintrcに設定を追加する。

$ yarn add -D @typescript-eslint/parser
{
  "extends": "airbnb",
  "parser": "@typescript-eslint/parser"
}

これで、パースできるようになる。

TypeScript ファイルを ESLint の対象にする

そもそもデフォルトでは.ts.tsxは ESLint の対象ではないので、明示的に指定する必要がある。
--extを使って指定する。src/以下のファイルを対象にする場合は次のコマンドを実行すればよい。

yarn の場合。

$ yarn eslint --ext .js,.jsx,.ts,.tsx src/

npm の場合。

$ npx eslint --ext .js,.jsx,.ts,.tsx src/

import/no-unresolved を修正する

これで TypeScript ファイルに対しても ESLint を実行できるようになったが、import/no-unresolvedというエラーが出ているはず。
TypeScript ファイルのインポートを ESLint が解決できないことで、このエラーが発生してしまっている。
import/resolverを設定することで解決できる。

解決したい拡張子を指定すればよいので、次のように記述する。

{
  "extends": "airbnb",
  "parser": "@typescript-eslint/parser",
  "settings": {
    "import/resolver": {
      "node": {
        "extensions": [
          ".js",
          ".jsx",
          ".json",
          ".ts",
          ".tsx"
        ]
      }
    }
  }
}

これでimport/no-unresolvedは解消したはず。

VSCode の設定

エディタの自動修正を利用することで、開発効率が大幅に上がる。
TypeScript ファイルも自動修正の対象にして、コードを保存するたびに自動修正が実行されるようにしたい。
VSCode の場合は、設定ファイルに以下の内容を追加すればよい。

    "editor.formatOnSave": false,
    "javascript.format.enable": false,
    "javascript.validate.enable": false,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        { "language": "typescript", "autoFix": true },
        { "language": "typescriptreact", "autoFix": true }
    ],

参考資料