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 にはデータを読み書きする機能がありません。そのため、実際の操作は TypedArray や DataView を通して行い、整数や浮動小数点数などの型やエンディアンを指定してアクセスします。
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)
テキストをバイナリに、またその逆に変換するには TextEncoder や TextDecoder を使います。
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.encodeはUint8Arrayを返すため、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を返すため、効率や参照の共有を重視する場合は、同じバッファを参照するTypedArrayのsubarrayを使用できます。
TypedArray.subarray と共有の違い
TypedArrayのsubarrayは同じ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のバイナリ型であるBufferとArrayBufferを相互変換できます。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 で幅広く利用されています。
- バイナリプロトコルの解析や構築(
DataViewでヘッダ情報を処理する) - 画像・音声データなどのメディア処理(
fetch(...).then(res => res.arrayBuffer())を使用) - WebAssembly のメモリ共有(Wasm メモリは
ArrayBufferを基にして動作) - 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 からバイナリデータを取得し、先頭数バイトを表示します。
fetchAPI で取得したデータをArrayBufferとして読み込み、最初の 16 バイトをUint8Arrayで確認しています。
まとめ
ArrayBuffer は生のメモリ表現で、TypedArray と DataView を利用して多様なバイナリ操作を効率よく行えます。コピーを避ける設計とエンディアンの明示を心がけることで、安全かつ高速なバイナリ処理が可能になります。
YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。