TypeScriptにおけるArrayBuffer

TypeScriptにおけるArrayBuffer

この記事ではTypeScriptにおけるArrayBufferについて説明します。

TypeScriptにおけるArrayBufferについて、基本から実践的なテクニックまで、ステップバイステップで説明します。

YouTube Video

TypeScriptにおけるArrayBuffer

ArrayBufferはバイナリデータの「生のメモリ領域」を表す組み込みオブジェクトです。長さが固定の生のバッファを表し、その上に TypedArray や DataView を重ねて読み書きします。

ArrayBuffer の基本概念と性質

ArrayBuffer は「長さが固定のバイト列」です。バイト数でのサイズを作成時に指定し、後で長さを変えることはできません。

1// Create a new ArrayBuffer of 16 bytes.
2const buf = new ArrayBuffer(16);
3console.log(buf.byteLength); // 16
  • このコードは 16 バイトの空のバッファを作成します。byteLength でサイズを確認できます。

TypedArray と DataView — バッファを操作するためのビュー

ArrayBuffer にはデータを読み書きする機能がありません。そのため、実際の操作は TypedArrayDataView を通して行い、整数や浮動小数点数などの型やエンディアンを指定してアクセスします。

1// Create a buffer and a Uint8Array view over it. Then write bytes and read them.
2const buffer = new ArrayBuffer(8);
3const u8 = new Uint8Array(buffer);
4
5u8[0] = 0x41; // 'A'
6u8[1] = 0x42; // 'B'
7console.log(u8); // Uint8Array(8) [ 65, 66, 0, 0, 0, 0, 0, 0 ]
  • Uint8Array はバイト単位の配列ビューで、配列のようにアクセスできます。複数のビューを同じ ArrayBuffer に置くと、同じメモリを共有して読み書きできます。

DataView:任意の境界・エンディアンでの読み書き

DataView はバイト単位で細かい読み書きを行う際に便利で、リトル/ビッグエンディアンを指定できます。

 1// Using DataView to write/read multi-byte values with endianness control.
 2const buf2 = new ArrayBuffer(8);
 3const view = new DataView(buf2);
 4
 5// Write a 32-bit integer (little-endian)
 6view.setInt32(0, 0x12345678, true);
 7
 8// Read it back as little-endian and big-endian
 9const little = view.getInt32(0, true);
10const big = view.getInt32(0, false);
11console.log(little.toString(16)); // "12345678"
12console.log(big.toString(16));    // "78563412"
  • DataView はメモリ上のオフセットを指定して値を読み書きするため、ネットワークバイトオーダを扱うプロトコル実装に適しています。

文字列と ArrayBuffer の変換(TextEncoder / TextDecoder)

テキストをバイナリに、またその逆に変換するには TextEncoderTextDecoder を使います。

 1// Convert string -> ArrayBuffer and back using TextEncoder/TextDecoder.
 2const encoder = new TextEncoder();
 3const decoder = new TextDecoder();
 4
 5// Unicode escape sequences
 6const str = "\u3053\u3093\u306B\u3061\u306F";
 7const encoded = encoder.encode(str); // Uint8Array
 8console.log(encoded); // Uint8Array([...])
 9
10// If you need an ArrayBuffer specifically:
11const ab = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
12console.log(ab.byteLength); // bytes length of encoded text
13
14// Decode back
15const decoded = decoder.decode(encoded);
16console.log(decoded);
  • TextEncoder.encodeUint8Array を返すため、ArrayBuffer が必要な場合は、.bufferを参照する必要があります。sliceメソッドで、オフセットと長さを指定して、必要なデータを取り出せます。

ArrayBuffer のスライスとコピー

slice メソッドは、新しい ArrayBuffer を返します。ArrayBuffer はサイズを変更できないため、リサイズが必要な場合は新しいバッファを作成し、そこへコピーします。

 1// Slice an ArrayBuffer and copy to a new sized buffer.
 2const original = new Uint8Array([1,2,3,4,5]).buffer;
 3const part = original.slice(1, 4); // bytes 1..3
 4console.log(new Uint8Array(part)); // Uint8Array [ 2, 3, 4 ]
 5
 6// Resize: create a new buffer and copy existing content
 7const larger = new ArrayBuffer(10);
 8const target = new Uint8Array(larger);
 9target.set(new Uint8Array(original), 0);
10console.log(target); // first bytes filled with original data
  • slice メソッドは、新しい ArrayBuffer を返すため、効率や参照の共有を重視する場合は、同じバッファを参照する TypedArraysubarray を使用できます。

TypedArray.subarray と共有の違い

TypedArraysubarrayは同じArrayBufferを参照する新しいビューを返します。

1// subarray shares the same underlying buffer; modifying one affects the other.
2const arr = new Uint8Array([10,20,30,40]);
3const viewSub = arr.subarray(1,3); // shares memory
4viewSub[0] = 99;
5console.log(arr); // Uint8Array [10, 99, 30, 40]
  • 共有ビューはコピーのコストを避けられますが、同じバッファを参照するため副作用に注意が必要です。

バッファの連結(複数の ArrayBuffer を結合する)

ArrayBuffer は固定長なので、複数のバッファを結合するには新しい ArrayBuffer を作成してコピーします。

 1// Concatenate multiple ArrayBuffers
 2function concatBuffers(buffers: ArrayBuffer[]): ArrayBuffer {
 3  const total = buffers.reduce((sum, b) => sum + b.byteLength, 0);
 4  const result = new Uint8Array(total);
 5  let offset = 0;
 6  for (const b of buffers) {
 7    const u8 = new Uint8Array(b);
 8    result.set(u8, offset);
 9    offset += u8.length;
10  }
11  return result.buffer;
12}
13
14const a = new Uint8Array([1,2]).buffer;
15const b = new Uint8Array([3,4,5]).buffer;
16const c = concatBuffers([a, b]);
17console.log(new Uint8Array(c)); // [1,2,3,4,5]
  • 頻繁に大きな連結を行う場合は、このコードのように事前に合計サイズを算出して一度だけ新規バッファを割り当てる方法が効率的です。

ArrayBuffer を Worker に渡す(Transferable)

ブラウザの postMessage では、ArrayBuffer を「転送可能(transferable)」として渡すことができます。コピーせずに所有権を移動できるため、コピーコストを避けられます。

1// Example: posting an ArrayBuffer to a Worker as a transferable object (browser)
2const worker = new Worker('worker.js');
3const bufferToSend = new Uint8Array([1,2,3,4]).buffer;
4
5// Transfer ownership to the worker (main thread no longer owns it)
6worker.postMessage(bufferToSend, [bufferToSend]);
7
8// After transfer, bufferToSend.byteLength === 0 in many browsers (detached)
9console.log(bufferToSend.byteLength); // may be 0
  • postMessage の第2引数に転送対象のオブジェクトを配列で指定することで、ArrayBuffer などの transferable オブジェクトをコピーせずに所有権ごと渡せます。
  • 転送後は、元の側ではバッファが「切り離された(detached)」状態となり、アクセスできなくなります。SharedArrayBufferを使えば複数スレッドから同時にアクセスできますが、利用にはセキュリティ要件や環境の制約があります。

Node.js での扱い(Buffer との相互変換)

Node.jsでは、Node.jsのバイナリ型であるBufferArrayBufferを相互変換できます。TypeScript でブラウザとNode.jsの両方を対象にする場合に役立ちます。

 1// Convert ArrayBuffer <-> Node.js Buffer
 2// In Node.js environment:
 3const ab = new Uint8Array([10,20,30]).buffer;
 4const nodeBuffer = Buffer.from(ab); // ArrayBuffer -> Buffer
 5console.log(nodeBuffer); // <Buffer 0a 14 1e>
 6
 7const backToAb = nodeBuffer.buffer.slice(
 8    nodeBuffer.byteOffset,
 9    nodeBuffer.byteOffset + nodeBuffer.byteLength
10);
11console.log(new Uint8Array(backToAb)); // Uint8Array [10,20,30]
  • Node.js の Buffer.from(arrayBuffer) は通常コピーされますが、参照共有を使えるケースもあるためオフセットに注意が必要です。

パフォーマンスの注意点とベストプラクティス

パフォーマンスを最適化し、効率的に ArrayBuffer を扱うためには、いくつかの重要なポイントを意識する必要があります。以下では、実践的なベストプラクティスを挙げて説明します。

  • コピー回数を減らす 大きなバイナリを扱うときは subarray(ビュー共有)や transferables を使い、コピーを減らすようにします。

  • 大きなバッファをまとめて確保する 小さなバッファを何度も割り当てるとオーバーヘッドが増えます。可能であれば大きめのバッファを一度に確保し、その一部を必要に応じて利用します。

  • エンディアンを明示する マルチバイト値を扱う際は DataView を使い、エンディアンを明示します。ネットワークプロトコルではビッグエンディアンが標準の場合が多いです。

よくあるユースケースの実例集

ArrayBuffer はブラウザや Node.js で幅広く利用されています。

  1. バイナリプロトコルの解析や構築(DataView でヘッダ情報を処理する)
  2. 画像・音声データなどのメディア処理(fetch(...).then(res => res.arrayBuffer()) を使用)
  3. WebAssembly のメモリ共有(Wasm メモリは ArrayBuffer を基にして動作)
  4. Worker による重い処理のオフロード(ArrayBuffer を transferable として渡す)

次のコードは、バイナリデータの取得と解析の例です。

1// Example: fetch binary data in browser and inspect first bytes
2async function fetchAndInspect(url: string) {
3  const resp = await fetch(url);
4  const ab = await resp.arrayBuffer();
5  const u8 = new Uint8Array(ab, 0, Math.min(16, ab.byteLength));
6  console.log('first bytes:', u8);
7}
  • このコードは、ブラウザで任意の URL からバイナリデータを取得し、先頭数バイトを表示します。fetch API で取得したデータを ArrayBuffer として読み込み、最初の 16 バイトを Uint8Array で確認しています。

まとめ

ArrayBuffer は生のメモリ表現で、TypedArray と DataView を利用して多様なバイナリ操作を効率よく行えます。コピーを避ける設計とエンディアンの明示を心がけることで、安全かつ高速なバイナリ処理が可能になります。

YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。

YouTube Video