📏

TypedArray のサイズが可変になる Stage 3 Resizable ArrayBuffers

2023/01/08に公開

現状の ArrayBuffer の問題

現状 JavaScript の ArrayBuffer はサイズが変わることを想定していません。そのためサイズを変える必要がある API の仕様を作るとなると ECMAScript サイドではなく、その API サイドでの対応が必要な状況になっています。

例えば WebAssembly.Memory を見てみましょう。

https://webassembly.github.io/spec/js-api/#memories

[LegacyNamespace=WebAssembly, Exposed=(Window,Worker,Worklet)]
interface Memory {
  constructor(MemoryDescriptor descriptor);
  unsigned long grow([EnforceRange] unsigned long delta);
  readonly attribute ArrayBuffer buffer;
};

バッファのサイズを大きくしたいとき grow メソッドを呼び出します。しかし ArrayBuffer はサイズが変わることを想定していないため、今まで buffer プロパティで取得していた ArrayBuffer が detached されます。再度 buffer プロパティにアクセスして新しい ArrayBuffer を取得しなければなりません。

いつ detached されるかわからない場合、以下のようにバッファにアクセスするたびに確認する必要があります。これでは扱いにくい上に処理速度がかなり遅くなります。

let uint8 = new Uint8Array(memory.buffer);

function getUint8MemoryValue(index) {
  // memory.buffer が detached されていたら、再度 Uint8Array を作り直す
  if (uint8.length === 0) {
    uint8 = new Uint8Array(memory.buffer);
  }
  
  return uint8[index];
}

Stage 3 Resizable ArrayBuffers

ArrayBuffer のサイズを可変にする提案が Stage 3 Resizable ArrayBuffers です。

https://github.com/tc39/proposal-resizablearraybuffer

Stage 3 ですが全てのモダンブラウザにフラグ付きで実装されており、既に WHATWG HTML に取り込まれている状況なため、間もなく ECMAScript に入りそうです。

https://github.com/whatwg/html/pull/8559

扱い方

ArrayBuffer コンストラクタの第二引数にオプションが追加され、maxByteLength が指定できるようになります。それ以下の数値なら resize メソッドでサイズを変更できます。

const buffer = new ArrayBuffer(100, { maxByteLength: 200 });
console.log(buffer.length); // 100
console.log(buffer.maxByteLength); // 200
console.log(buffer.resizable); // true

buffer.resize(150);
console.log(buffer.length); // 150

これが ECMAScript に入ることによって WebAssembly.Memorygrow メソッドが呼ばれても、 buffer プロパティが detached されず常に同じ ArrayBuffer オブジェクトを返すようになることが期待されます。

注意点

今まで ArrayBuffer は固定サイズを扱うものだったため、予めそのサイズをキャッシュしておいてそれを使い回すコードを書いている場合注意が必要そうです。

特に注意が必要な点として TypedArrayDataView をコンストラクタから作る際に第一引数にサイズが可変な ArrayBuffer を入力し、かつ第三引数(length)が undefined の場合、ArrayBuffer のサイズ変更に引きずられることが挙げられます。

const buffer = new ArrayBuffer(100, { maxByteLength: 200 });
const uint8 = new Uint8Array(buffer);
console.log(uint8.length); // 100

buffer.resize(150);
console.log(uint8.length); // 150

解決したい問題のことを考えるとこうならざるを得ませんが、今までの前提と異なる挙動となります。今一度既存のコードを確認して、その都度 length プロパティにアクセスする方針に修正したほうがいいかもしれません。

Growable SharedArrayBuffers

SharedArrayBuffer はサイズを小さくすることが想定されないため少し異なりますが、ほとんど Resizable ArrayBuffers と同じような機能追加がなされます。

関連する提案

Stage 2 ArrayBuffer.prototype.transfer

C における realloc のように、メモリを再確保するメソッドを追加する提案です。このメソッドが呼ばれたあとは元の ArrayBuffer は detached されます。

https://github.com/tc39/proposal-arraybuffer-transfer

結び

今回は Resizable ArrayBuffers について取り上げてみました。この提案は人によっては対応が必要そうかなと思い記事にしてみました。

その他の提案の記事も Zenn に書いているのでよかったら読んでください。最後まで読んでいただきありがとうございました!

Discussion