[JavaScript講座] バイナリデータと TypedArray

当ページのリンクには広告が含まれています。
目次

なぜ「バイナリデータ」とTypedArray が必要?

JavaScript はもともと「ブラウザ上のスクリプト言語」として設計されていて、

  • 文字列 (string)
  • 数値 (number)
  • 配列 (Array)

などの高レベルな型はあるけど、

  • 「生のバイト列(8bit 配列)を決め打ちの型で解釈する」
  • 「C言語の struct 的に、決められたバイトレイアウトを読む/書く」

といった処理には向いていませんでした。

しかし Web で:

  • 画像・音声・動画データを直接いじる
  • WebGL / WebGPU で GPU に渡す頂点バッファを組み立てる
  • 独自バイナリプロトコル(WebSocket や WebRTC など)を扱う

といった需要が出てきて、高性能にバイナリを扱う仕組みとして導入されたのが

  • ArrayBuffer
  • TypedArrayUint8Array など)
  • DataView

です。

ArrayBuffer:生のバイト配列(ただの「器」)

ArrayBuffer の正体

ArrayBuffer は「一定長のバイト列」を表すオブジェクトです。
中身を直接読む・書くことはできず、ビュー(TypedArray / DataView)を通して操作します。

const buffer = new ArrayBuffer(16); // 16バイトのバッファを確保

console.log(buffer.byteLength); // 16

この時点では「16バイトのメモリを確保しただけ」で、中身は未定義です(一般的には 0 初期化されます)。

イメージ:

buffer (16 bytes)
[ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ]

ArrayBuffer は「共有される」

複数の TypedArray が 同じ ArrayBuffer を別の見え方で参照できます。

const buffer = new ArrayBuffer(4); // 4バイト
const u8 = new Uint8Array(buffer);
const u32 = new Uint32Array(buffer);

u8[0] = 0x78;
u8[1] = 0x56;
u8[2] = 0x34;
u8[3] = 0x12;

console.log(u32[0]); // 環境のエンディアンに応じて 0x12345678 か 0x78563412
  • 同じ bufferUint8Array(1バイト単位)としても、
  • Uint32Array(4バイト整数)としても見られる

というのがポイントです。

TypedArray:型付き配列ビュー

代表的な TypedArray 種類

TypedArray にはいくつかの「型付きビュー」があります(全部暗記は不要、よく使うものだけ把握でOK):

  • 整数系
    • Int8Array / Uint8Array / Uint8ClampedArray
    • Int16Array / Uint16Array
    • Int32Array / Uint32Array
  • 浮動小数系
    • Float32Array
    • Float64Array

よく使う例

  • Uint8Array:純粋なバイト列(画像のピクセル、バイナリプロトコル、etc.)
  • Float32Array:WebGL 等で頂点バッファ(XYZ 座標など)を表すことが多い

基本的な作り方

1) 指定長で作る

const arr = new Uint8Array(4);
console.log(arr.length);     // 4
console.log(arr.byteLength); // 4
console.log(arr[0]);         // 0

2) 通常の配列からコピー

const arr = new Uint8Array([10, 20, 30]);
console.log(arr[0]); // 10

3) 既存の ArrayBuffer からビュー作成

const buffer = new ArrayBuffer(8);

const u8  = new Uint8Array(buffer);      // 8バイト全体を1バイト単位で
const u16 = new Uint16Array(buffer);     // 2バイト単位で4要素
const u8sub = new Uint8Array(buffer, 2); // オフセット2バイトからのビュー

コンストラクタの形:

new TypedArray(buffer, byteOffset?, length?)
  • byteOffset:バッファ先頭からのオフセット(バイト単位)
  • length:要素数(型のサイズ単位)

ふつうの配列っぽく使える(けど Array とは別物)

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

console.log(arr[0]);   // 1
arr[1] = 255;

console.log(arr.length);      // 3
console.log(arr instanceof Array); // false
console.log(Array.isArray(arr));   // false
  • インデックスアクセスや lengthArray と同じ感覚でOK
  • ただし TypedArray は Array のサブクラスではない(別物)

ES2015 以降、多くの Array メソッドと似たメソッドを持っています(map, forEach, filter など)
ただし完全に同じではないので、「Array とほぼ同じだけど細部が違う」くらいの認識でOK。

例:

const a = new Uint8Array([1, 2, 3, 4]);

const b = a.map(x => x * 10); // 新しい Uint8Array が返る
console.log(b); // [10, 20, 30, 40]

DataView:エンディアン指定&柔軟な読み書きビュー

TypedArray は「一定の型で等間隔に並んでいる配列」を想定していますが、

  • 「ヘッダ4バイトは符号なし32bit整数、次の2バイトは符号付き16bit、次は1バイトのフラグ…」のような構造化バイナリ
  • リトルエンディアン / ビッグエンディアンを意識して読みたい

といった時には DataView を使います。

基本の作り方

const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);

コンストラクタ:

new DataView(buffer, byteOffset?, byteLength?)

TypedArray とほぼ同じですが、「何バイト単位で切るか」はメソッドに依存します。

読み書きメソッド

メソッド名のパターンは:

  • 読み込み:getUint8 / getInt16 / getFloat32 / …
  • 書き込み:setUint8 / setInt16 / setFloat32 / …

シグネチャ(読み取り):

view.getUint32(byteOffset, littleEndian?);
  • byteOffset:バッファ内の読み出し位置
  • littleEndian:true ならリトルエンディアン、false or 省略ならビッグエンディアン(仕様による)

例:ヘッダ付きバイナリの読み書き

const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);

// 書き込み(リトルエンディアン)
view.setUint16(0, 0x1234, true); // offset 0〜1
view.setUint32(2, 0x89abcdef, true); // offset 2〜5

// 読み込み
const header = view.getUint16(0, true);
const value  = view.getUint32(2, true);

console.log(header.toString(16)); // "1234"
console.log(value.toString(16));  // "89abcdef"
  • 「どのバイト位置から、何バイトの値を、どのエンディアンで読むか」を自由に制御できる
  • ネットワークプロトコルやバイナリファイル形式のパーサで必須

文字列 ⇔ バイト列:TextEncoder / TextDecoder

バイナリ処理では、文字列をバイト列に変換する場面も多いです(UTF-8 エンコードなど)。

TextEncoder(文字列 → Uint8Array

const encoder = new TextEncoder(); // デフォルト UTF-8

const text = "こんにちは";
const bytes = encoder.encode(text);

console.log(bytes); // Uint8Array([...])
console.log(bytes.length); // バイト数(文字数とは異なる)
  • 戻り値は Uint8Array
  • ブラウザ / Node.js どちらでも利用可能(比較的新しめの環境なら)

TextDecoderUint8Array → 文字列)

const decoder = new TextDecoder("utf-8");

const text2 = decoder.decode(bytes);
console.log(text2); // "こんにちは"
  • 第1引数にエンコーディング名(”utf-8″ が基本)
  • 部分的なストリームデコードなどもできますが、最初は decode 一発でOK

TypedArray 実用イメージ

簡単なバイナリパケット構造を自作してみる

たとえば、次のようなバイナリ構造を送受信したいとします。

  • 0〜1バイト目:パケットタイプ(Uint16
  • 2〜5バイト目:データ長(Uint32
  • 6〜(6+N-1)バイト目:UTF-8 文字列本文

パケット生成

function createPacket(type, text) {
  const encoder = new TextEncoder();
  const body = encoder.encode(text); // Uint8Array
  const length = body.length;

  const buffer = new ArrayBuffer(6 + length);
  const view = new DataView(buffer);

  view.setUint16(0, type, true);   // type
  view.setUint32(2, length, true); // length

  const bodyView = new Uint8Array(buffer, 6);
  bodyView.set(body);              // 本文コピー

  return buffer;
}

パケット解析

function parsePacket(buffer) {
  const view = new DataView(buffer);

  const type   = view.getUint16(0, true);
  const length = view.getUint32(2, true);

  const bodyBytes = new Uint8Array(buffer, 6, length);
  const decoder = new TextDecoder("utf-8");
  const text = decoder.decode(bodyBytes);

  return { type, length, text };
}
  • 「バイト単位で仕様を決めて」「DataView / TypedArray で組み立て・分解」する、という流れをイメージできればOKです。

<<前へ(日付・正規表現・国際化(Date / JSON / RegExp / Intl))

>>次へ(エラーと例外処理)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次