TypeScriptにおける`DataView`

TypeScriptにおける`DataView`

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

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

YouTube Video

TypeScriptにおけるDataView

TypeScriptでDataViewを使うと、ArrayBuffer上でバイト単位の細かい読み書きができます。プロトコル実装、バイナリファイル解析、WebSocketでのバイナリ送受信などの低レベルなバイナリ処理で非常に有用です。

基本概念:ArrayBuffer・TypedArray・DataViewの違い

ArrayBufferは、固定長のバイト列を保持するための基本的なデータ構造です。Uint8ArrayなどのTypedArrayはそのバッファを特定の型配列として扱うビューで、要素ごとに型が決まっています。

一方、DataView任意のオフセット位置に対して、好きなデータ型で値を読み書きできる柔軟なビュー です。固定的な型を前提とせず、バイト単位で細かく制御できるため、バイナリ形式のプロトコル解析や低レベル処理に適しています。

以下はArrayBufferを作り、Uint8ArrayDataViewを作る例です。

 1// Create an ArrayBuffer (8 bytes)
 2const buffer = new ArrayBuffer(8);
 3
 4// Create a Uint8Array view (easy to manipulate individual bytes)
 5const u8 = new Uint8Array(buffer);
 6u8[0] = 0x12;
 7u8[1] = 0x34;
 8
 9// Create a DataView (allows reading/writing any type at any offset)
10const dv = new DataView(buffer);
11
12// Read a 16-bit unsigned integer (big-endian assumed if second argument is omitted)
13console.log(dv.getUint16(0)); // 0x1234
  • このコードでは、1つのバッファに対して、Uint8ArrayDataViewという異なる型のビューを同時に扱っています。DataView を使えば、任意のオフセットとエンディアンを指定して柔軟に値を読み書きできます。

DataViewの主要メソッド

DataViewArrayBuffer をバイト単位で操作するためのインターフェイスで、整数や浮動小数点などさまざまな型を読み書きするためのメソッドが用意されています。

主なメソッドは次の通りです。

  • 読み取り: getUint8, getInt8, getUint16, getInt16, getUint32, getInt32, getFloat32, getFloat64
  • 書き込み: setUint8, setInt8, setUint16, setInt16, setUint32, setInt32, setFloat32, setFloat64

どのメソッドも、第一引数で「バイトオフセット」を指定し、get はその位置から読み出し、set はその位置に値を書き込みます。また、16、32、64ビット幅のデータを扱うときは、第2引数でエンディアンを指定できます。実務ではデータ仕様に従って 必ずエンディアンを明示する のが安全です。

以下の例では、リトルエンディアンで 32 ビット整数と 32 ビット浮動小数点をバッファに書き込み、同じ形式で読み出す方法を示しています。

 1const buf = new ArrayBuffer(12); // Enough space for integer + float
 2const view = new DataView(buf);
 3
 4// Write a 32-bit unsigned integer at offset 0 (little-endian)
 5view.setUint32(0, 305419896, true); // 305419896 = 0x12345678
 6
 7// Read the same 32-bit unsigned integer back from offset 0 (little-endian)
 8const intVal = view.getUint32(0, true);
 9console.log(intVal); // 305419896
10
11// Write a 32-bit floating number at offset 4 (little-endian)
12view.setFloat32(4, 3.1415926, true);
13
14// Read the 32-bit floating number back from offset 4 (little-endian)
15const floatVal = view.getFloat32(4, true);
16console.log(floatVal); // 3.1415926 (approx)
  • エンディアンを明示しておくことで、異なるプラットフォームやバイナリ仕様との互換性を確保できます。

エンディアン(バイト順)について

ネットワーク系などビッグエンディアンが使われるプロトコルもあれば、多くのCPUやファイル形式のようにリトルエンディアンが主流の設計も存在します。DataView では、第2引数が true なら リトルエンディアンfalse または省略すれば ビッグエンディアン として扱われます。

次の例では、同じ数値をビッグエンディアンとリトルエンディアンで書き込んだ場合に、メモリ上のバイト配列がどのように変わるか を確認できます。

 1const b = new ArrayBuffer(4);
 2const a = new Uint8Array(b);
 3const dv = new DataView(b);
 4
 5// Write in big-endian order
 6dv.setUint32(0, 0x0A0B0C0D, false); // big-endian
 7console.log([...a]); // [10, 11, 12, 13]
 8
 9// Write the same value in little-endian order
10dv.setUint32(0, 0x0A0B0C0D, true); // little-endian
11console.log([...a]); // [13, 12, 11, 10]
  • ビッグエンディアンとリトルエンディアンでバイト並びがどう変わるかを理解すると、通信データやバイナリ形式の解析がずっと楽になります。

文字列の入出力(TextEncoderDecoderの併用)

DataViewは数値の読み書きに優れていますが、文字列は直接扱いません。TextEncoderTextDecoderを使ってUTF-8などにエンコードしてからUint8Arrayでバッファにコピーするのが一般的です。以下はUTF-8文字列を書き込み、後で読み出す例です。

 1const encoder = new TextEncoder();
 2const decoder = new TextDecoder();
 3
 4const str = "\u3053\u3093\u306b\u3061\u306f";
 5const encoded = encoder.encode(str); // Uint8Array (UTF-8 encoded bytes)
 6
 7// Create a buffer and write the encoded string bytes into it
 8const buf2 = new ArrayBuffer(encoded.length);
 9const u8v = new Uint8Array(buf2);
10u8v.set(encoded);
11
12// Read the bytes and decode them back into a string
13const decodedStr = decoder.decode(new Uint8Array(buf2));
14console.log(decodedStr);
  • 文字列をバイナリに変換し、必要なら先頭に長さを付けて保存すると、可変長の文字列を格納できます。

実践:カスタムバイナリフォーマットのエンコード/デコード

以下では、バージョン番号とID、名前を含むシンプルなメッセージ形式を定義しています。メッセージをバイナリデータへ変換するエンコード処理と、逆にバイナリから元のオブジェクトへ復元するデコード処理を実装します。このメッセージ形式では、先頭 1 バイトにバージョン、続く 4 バイトにリトルエンディアンで表した ID、その後の 1 バイトに名前の長さ、最後に UTF-8 でエンコードされた名前を格納します。

 1// Encode an object into a binary message
 2function encodeMessage(msg: { version: number; id: number; name: string }): ArrayBuffer {
 3  const encoder = new TextEncoder();
 4  const nameBytes = encoder.encode(msg.name);
 5  const total = 1 + 4 + 1 + nameBytes.length;
 6  const buf = new ArrayBuffer(total);
 7  const dv = new DataView(buf);
 8  let offset = 0;
 9
10  dv.setUint8(offset, msg.version); offset += 1;      // write version
11  dv.setUint32(offset, msg.id, true); offset += 4;    // write ID (little-endian)
12  dv.setUint8(offset, nameBytes.length); offset += 1; // write name length
13
14  // write name bytes
15  new Uint8Array(buf, offset).set(nameBytes);
16  return buf;
17}
18
19// Decode a binary message back into an object
20function decodeMessage(buf: ArrayBuffer) {
21  const dv = new DataView(buf);
22  let offset = 0;
23
24  const version = dv.getUint8(offset); offset += 1;   // read version
25  const id = dv.getUint32(offset, true); offset += 4; // read ID (little-endian)
26  const nameLen = dv.getUint8(offset); offset += 1;   // read name length
27
28  // extract name bytes
29  const nameBytes = new Uint8Array(buf, offset, nameLen);
30  const decoder = new TextDecoder();
31  // decode UTF-8 string
32  const name = decoder.decode(nameBytes);
33
34  return { version, id, name };
35}
36
37// Example usage
38const packed = encodeMessage({ version: 1, id: 42, name: "Alice" });
39const unpacked = decodeMessage(packed);
40console.log(unpacked); // { version: 1, id: 42, name: "Alice" }
  • この例は、可変長文字列を含む基本的なメッセージフォーマットを扱う典型的な実装であり、ネットワーク通信や独自バイナリファイル設計など、多くの実用的な場面で応用できます。

注意点とベストプラクティス

DataView を使ってバイナリデータを扱う際には、単に値を読み書きするだけでなく、安全性やエンディアンの統一、型の扱いなど、実践的な観点で注意すべきポイントがあります。特に、外部から受け取るバイナリやサイズの大きい整数を扱う場合は、誤った読み取りやバッファオーバーランを防ぐための設計が重要になります。以下では、実務で覚えておくと役に立つポイントをいくつかまとめています。

  • 境界チェック DataViewはオフセットやサイズがバッファを超えると例外を投げます。信頼できないバイナリを扱うときは長さをチェックしましょう。

  • エンディアンを必ず明示 リトルエンディアン、ビッグエンディアンの指定をコード内で必ず統一して明示してください。

  • 型整合性 JavaScriptの数値はIEEE-754の64ビット浮動小数点です。getUint32などは内部で適切に扱われますが、getUint64は無いので64ビット整数は工夫が必要です。

  • 64ビット整数の扱い 64ビット整数を扱う場合は BigInt を使うか手作業で高低32ビットに分ける必要があります。以下は簡易的な64ビット符号なし整数の読み取り例です。

1function getUint64(dv: DataView, offset: number, littleEndian = true): bigint {
2  const low = BigInt(dv.getUint32(offset, littleEndian));
3  const high = BigInt(dv.getUint32(offset + 4, littleEndian));
4  return littleEndian ? (high << 32n) | low : (low << 32n) | high;
5}
  • BigIntを使えば安全に64ビット以上の整数も扱えます。

Node.jsでの利用(Bufferとの連携)

Node.jsではBufferがよく使われますが、ArrayBufferDataViewとの相互変換も簡単に行えます。BufferbufferプロパティやUint8Arrayコンストラクタを使います。

1// Node.js: Buffer -> ArrayBuffer
2const nodeBuf = Buffer.from([1,2,3,4]);
3const arrBuf = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.byteLength);
4const dvNode = new DataView(arrBuf);
5console.log(dvNode.getUint16(0));
  • Nodeとブラウザ間でバイナリをやり取りする際のツールとして使えます。

まとめ

DataView は、バイナリを自由に読み書きできる強力な仕組みで、エンディアンの指定や任意位置へのアクセスなど、柔軟な制御が必要な場面で特に役立ちます。ArrayBufferTypedArrayDataView を組み合わせると、TypeScriptでバイナリデータを柔軟かつ正確に扱えるようになり、プロトコル実装からファイル解析まで幅広い用途に対応できます。必要に応じて文字列エンコードや64ビット整数の扱いなども組み合わせると、より実践的なバイナリ操作が実現できます。

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

YouTube Video