kikitori Tech Blog

株式会社kikitoriは、農業流通現場のDXを実現するSaaS『nimaru』と青果店『KAJITSU』を運営する会社です。

JavaScript エコシステムでバイナリデータを扱う API の歴史+チートシート

こんにちは。kikitori の永谷です。

テックブログの執筆が一巡したので、改めて私からスタートです。 前回の一巡はテックブログと言いつつテックではない話も多かったので、今回はもう少し技術的な内容も書いていければなと考えています。

というわけで今回は JavaScript エコシステムにおけるバイナリデータの扱いについての記事です。

どうしてこの記事を書こうと思ったのか

弊社のプロダクト「nimaru」はフルスタック TypeScript 構成で開発しています。 農産物という不確実な商品を扱う都合上、画像や PDF ファイルなどバイナリデータを取り扱う処理が多く登場します。

最近もそのあたりのコードレビューを何度か行いましたが、よくコメントするのが、Web API に準拠するのか、Node.js API に寄せるのかという点についてです。

日本語で体系的にまとまっている資料が少なく、しかも歴史的な経緯を知らないと混乱しやすいテーマです。

この記事では、それぞれの API がどのように生まれ、どう関係しているのかを整理してみます。

JavaScript がバイナリを扱えるようになるまで

JavaScript が最初にバイナリデータを扱えるようになったのは、2009 年の Node.js の登場がきっかけでした。

Node.js はネットワーク通信やファイルI/Oを効率的に行うために、文字列ではなくバイト列を直接扱う必要があり、そのために Buffer オブジェクト が導入されました。

Buffer は低レベルなメモリ領域を直接操作できる仕組みで、サーバーサイドでのバイナリ処理を支える重要な要素になりました。

一方で、2010 年ごろにはブラウザ側でも似たようなニーズが生まれます。 WebGL が登場し、GPU に数値配列を渡すために高速なメモリ操作が必要になったのです。

この問題を解決するために考案されたのが ArrayBufferTypedArray で、これは WebGL のための技術として誕生しました。

後にその有用性が広く認められ、2015 年の ECMAScript 2015(ES6) で正式に言語仕様に統合されます。 この時点で、ブラウザ側では ArrayBuffer / TypedArray、サーバー側では Buffer という二系統のバイナリAPI が併存する形になりました。

さらに 2012 年以降、Browserify や Babel、Webpack などのツールが登場し、Node.js のモジュールをブラウザでも動かせるようになります。

その結果、Node.js の Buffer をブラウザで再現する polyfill(npm パッケージ buffer)が普及。 Node.js とブラウザのエコシステムが急速に融合し、現在のように両方の API が混在する環境が生まれました。

バイナリ APIの関係図(チートシート

こうした背景のもとで、JavaScript エコシステムには現在

  • ECMAScript 標準の ArrayBuffer 系列 (ArrayBuffer, TypedArray, DataView)
  • Node.js 由来の Buffer

の二系統が存在します。

その全体像を以下の図にまとめました。

JavaScript エコシステムでバイナリデータを扱うオブジェクトのチートシート

なお、この図には関連する Web API である File や Blob も併記しています。

API の詳細については次の通りになります。

ArrayBuffer (ECMAScript 標準)

「生のメモリ領域」を表す、バイナリデータを扱うための最も基本的なオブジェクトです。 バイト単位で固定長の領域を確保し、その中身を直接読み書きすることはできません。 実際の操作は TypedArrayDataView などのビューを通じて行います。

TypedArray (ECMAScript 標準)

ArrayBuffer の内容を型付きで効率的に操作するためのビューです。 TypedArray オブジェクト自体は公開されておらず、Int8ArrayFloat64Array のように、値の型ごとに異なるサブクラスを使用します。

通常の配列に近い構文で操作できますが、通常の配列と異なり、インデックスアクセスはプロトタイプチェーンを辿りません。具体的に言えば、Object.prototype["0"] = 123 としても new Uint8Array()[0] === undefined です。これにより、通常の配列へのアクセスよりもロジックを簡略化でき、高速化に寄与しています。

なお、Uint8Array は C でいう char[] に近く、「バイト列」の表現として最も自然な API なので、ReadableStream や Fetch API のレスポンスなど、さまざまな Web API で標準的なバイト列表現として利用されており、単なる「ビュー」以上の存在です。

ArrayBuffer の一部分のみから TypedArray を作ることもできるので、 new Uint8Array(oldArray.buffer) はもとの TypedArray とは必ずしも同じサイズにならないことに注意が必要です (byteOffset, byteLength プロパティで位置が分かる)。

  • Uint8Array: 最も汎用的。0〜255 の整数を扱う。
  • Int8Array: 符号付き 8 ビット整数。
  • Float64Array: 64ビット浮動小数点数
  • Float32Array, Uint16Array, Int32Array など他にも多数。

DataView (ECMAScript 標準)

ArrayBuffer をより柔軟に操作するためのビューです。

TypedArray が単一の数値型を扱うのに対し、DataView は任意のバイト位置から任意の型を読み書きできるほか、プラットフォームのアーキテクチャに依存せず好きなエンディアンを使用できます。

便利そうなのですが、一般的な Web アプリケーションで使うのはたいてい Uint8Array なのであまり出番はありません。独自フォーマットのファイルを解析するときとかには便利かもしれませんね。

Blob (Web API)

Blob は イミュータブルなバイト列を抽象的に参照するオブジェクト です。

この「バイト列」がどこに存在するか(メモリ、ファイル、ネットワークキャッシュなど)は実装に依存し、必ずしもメモリ上に読み込まれているとは限りません。 そのため、内容を一度にすべて扱う textarrayBuffer メソッドの呼び出しは、内部でストレージやネットワークからデータを読み出す可能性があるため非同期処理となり、結果としてコピーも発生します。

File (Web API)

Blob を継承したオブジェクトです。Blob が純粋に「データのかたまり」であるのに対し、File はそれに ファイル名 (name)・更新日時 (lastModified) などの「ファイル」に必要なメタ情報が追加されています。

ブラウザで <input type="file">ドラッグ&ドロップ操作を通じて取得できるのは、この File オブジェクトです。

Buffer (Node.js API)

BufferNode.js が独自に持つ伝統的なバイナリ API です。もともとは C++ のネイティブメモリを扱うために設計されましたが、現在は Uint8Array のサブクラスとして実装されています。 現在でも多くの Node.js 標準 APIBuffer を返しますが、内部的には Uint8Array と互換性があるため、ブラウザとの境界は徐々に薄れつつあります。

まとめ

JavaScript でバイナリデータを扱うために、ECMAScript 由来の API と Node.js 由来の API があることを見てきました。

なお、Node.js の BufferUint8Array として実装されるようになったことからも分かる通り、近年の Node.js は ECMAScript 標準、Web API との互換性を重視するようになっています。

新しく書くコードでは、可能な限り標準に準拠した API を使っていきたいですね!

We're hiring!

私たちは、すべての人の生活に不可欠な「食」の基盤となる「農業」を、テクノロジーの力で支える、縁の下の力持ちとも言えるプロダクトを作っています。 私自身、難しいこと、これまで誰も取り組んだことがなかったこと、他の業界では見られない現場のリアルなど、こういった文面や資料では伝えきれない多くのチャレンジのあるドメインだと感じています。 ぜひ一度、ゆっくりお話ししましょう!

ご興味をお持ちいただけた方はこちらまで↓↓↓

herp.careers