なぜ Node.js に Buffer が必要なのか

JavaScript の文字列でバイナリデータを扱うと何が壊れるのかを検証しながら、Node.js が Buffer という専用の型を用意した理由を解説します。

/ nodejs/ buffer/ javascript
Buffer

Node.js Buffer とは

Buffer objects are used to represent a fixed-length sequence of bytes.

Buffer は一言でいうとバイナリデータを扱うための、固定長のメモリ領域です。

この記事では、実際に Buffer がどういう役割を持っているのかを説明します。

バイナリデータとは

そもそも、バイナリデータとは0と1のデータであり、コンピュータが処理をするために使用されるものです。 画像・音声・ネットワークパケット・暗号鍵などのコンピュータが直接扱う「生のデータ」はすべてこの0と1の並びとして表現されます。

Node.js が登場した当時の JavaScript には、標準で生のバイト列を扱うための十分な仕組みがありませんでした。 そこで Node.js は、バイナリデータを扱うための専用の型としてBufferを用意しました。

なぜ文字列ではダメなのか

バイト列なら文字列に入れればいいのでは?と思うかもしれませんが、これは不可逆になるためできません。

JavaScript の文字列は、仕様上 UTF-16 コード単位の列として扱われる「テキスト」を表現するための型です。 任意のバイト列を無理やり特定の文字エンコーディングで文字列として扱うと、データが壊れます。

実際に、以下で試してみると分かります。

const buf = Buffer.from([0xff, 0xfe, 0x00, 0x81]); // 適当なバイナリデータ
const str = buf.toString('utf8'); // 文字列に変換
const back = Buffer.from(str, 'utf8'); // 文字列からまたバイナリに戻す

console.log(buf.equals(back));

実行結果としては false が出力されます。 buf と back は同じバイト列にはなりません。 この例では、buf.toString('utf8') がバイト列を UTF-8 テキストとしてデコードします。 UTF-8 として不正なバイト列は、その過程で�(U+FFFD)に置き換えられ、元の情報が失われてしまうからです。

つまり、バイナリデータを文字列として扱うと、不可逆な形でデータが破壊される可能性があります。 だからこそ Node.js の多くの API は、生のバイト列を扱う場面でBufferを返す、または受け取れる設計になっています。

Buffer の正体

JavaScript には、生のメモリ領域を表すArrayBufferという型があります。 ただしArrayBuffer自体は「メモリを確保しているだけ」で、直接中身を読み書きする手段を持っていません。 試しに直接書き込んでみます。

const ab = new ArrayBuffer(10);
ab[0] = 123;

console.log(ab[0]); // 123
console.log(new Uint8Array(ab)); // Uint8Array(10) [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]

ab[0] = 123はエラーにはなりませんが、これは単なる通常のプロパティに 123 を代入しただけで、確保したメモリの中身は変わっていません。

ArrayBufferの中身を実際に読み書きするには、Uint8Arrayのようなview(=このメモリをどう解釈して読み書きするかを決めるインターフェース)を通す必要があります。

const view = new Uint8Array(ab);
view[0] = 123;
console.log(new Uint8Array(ab)); // Uint8Array(10) [ 123, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]

Node.js のBufferは、この Uint8Array を継承したクラスです。 つまり Buffer は、Node.js 独自の特別な型というより、生のメモリ領域(ArrayBuffer)に対する、便利メソッド付きの view と言えます。

Buffer.from('a') instanceof Uint8Array; // true

同じArrayBufferを複数の Buffer が覗いている場合、片方への書き込みがもう片方にも反映されます。

const ab = new ArrayBuffer(10);
const view1 = Buffer.from(ab);
const view2 = Buffer.from(ab);

view1[0] = 0xff;
console.log(view2[0]); // 255

この「Buffer は独立したメモリを持つとは限らず、共有される場合もある view」という性質を覚えておくと、Buffer の操作で起きる副作用を理解しやすくなります。 次回扱う Buffer.alloc と Buffer.allocUnsafe では、Buffer が確保したメモリをどう初期化するかに注目します。

まとめ

  • Buffer は、バイナリデータ(0と1の並び)を扱うための固定長のメモリ領域
  • JS 文字列でバイナリを扱うと、データが不可逆に壊れることがある
  • Buffer は Uint8Array を継承しており、その正体は生メモリ領域への「view」

参考文献

https://nodejs.org/api/buffer.html#buffer https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer