🔖

Uint8ArrayやArraryBufferやBufferとか

2024/06/24に公開

JavaScriptでbinaryを表現するものとしてUint8Array / ArraryBuffer / Bufferとか色々あってそれぞれどう違うんだ?と思ったので改めて調べてみました。

それぞれの概念について

まずは、それぞれの概念について軽く調べていきます

ArrayBuffer

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
ArrayBufferはNode.js環境でもブラウザ環境でもどちらでも使用することが可能で、生のバイナリーデータバッファーを表現するために使用されます。

const arrayBuffer = new ArrayBuffer(8);
console.log(arrayBuffer);
// 出力
// ArrayBuffer {
//  [Uint8Contents]: <00 00 00 00 00 00 00 00>,
//  byteLength: 8
// }

物理メモリ上に指定した領域を確保するだけで、基本的には参照を渡すことしかできません。maxByteLengthオプションを指定してサイズの変更は可能です。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer#arraybuffer_のサイズ変更

ArrayBufferを用いて確保した領域に対し実際のbinaryを確保するにはTypedArrayと呼ばれるビューを使用する必要があります。Uint8Arrayはビューの中の1つです。

Uint8Array

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
Uint8ArrayもNode.js環境でもブラウザ環境でもどちらでも使用することが可能です。ArrayBufferのビューであり指定したbyteの数値だけ0で初期化します。

const uInt8Array = new Uint8Array(8);
console.log(uInt8Array);
// 出力
// Uint8Array(8) [
//  0, 0, 0, 0,
//  0, 0, 0, 0
// ]

binaryを操作するためのインターフェースを提供しているため、Uint8Arrayを用いると下記のようにbinaryの操作が可能です。

// Uint8Arrayの作成(16進数で"Hello, world"と記述しています)
const uint8Array = new Uint8Array([
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64,
]);
uint8Array[1] = 0x6f; // Uint8Arrayの要素を変更

// TextDecoderでUint8Arrayを文字列に変換
const decoder = new TextDecoder("utf-8");
const decodedString = decoder.decode(uint8Array);

console.log(decodedString); // 出力: "Hollo, world"

ちなみにUint8ArrayなどのTypedArrayArrayBufferの参照を操作しているため、下記のように記述するとArrayBufferの値が変わっているのがわかります。

const arrayBuffer = new ArrayBuffer(8);
const uint8Array = new Uint8Array(arrayBuffer);
uint8Array[0] = 100;

console.log(arrayBuffer);
// 出力
// ArrayBuffer {
//   [Uint8Contents]: <64 00 00 00 00 00 00 00>,
//   byteLength: 8
// }

console.log(uint8Array);
// 出力
// Uint8Array(8) [
//   100, 0, 0, 0,
//     0, 0, 0, 0
// ]

Buffer

https://nodejs.org/api/buffer.html#buffer
BufferはNode.js専用のクラスでUint8Arrayのサブクラスとして定義されており、更なるユースケースをカバーすることができるようになっています。
バイナリーデータの作成はもちろん、操作、読み込みなどバイナリデータを直接扱う方法を提供します。
ちなみに今はBuffer()を用いてBufferオブジェクトを生成することは非推奨とされているため、Buffer.from()などを用いて生成します。
https://github.com/nodejs/node/blob/v22.3.0/lib/buffer.js#L255C1-L264C4

const buffer = Buffer.from("Hello, World!");
console.log(buffer);
// <Buffer 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21>

また、toString()を使えばBufferオブジェクトから元の文字列を生成できます。

const buffer = Buffer.from("Hello, World!");
console.log(buffer.toString());
// Hello, World!

wirte()を使うとバイナリデータの操作も可能です。

const buffer = Buffer.from("Hello, World!");
buffer.write("o", 1); 
console.log(buffer.toString());
// "Hollo, World!"

実践

webサービスでbinaryと聞くと主に画像の扱いを自分は連想するので(普段触っているものが何かにもよりますね)今回はfetchも絡めて実際にwebから画像をpostして、どのようなデータでやり取りされてるのかを確認してみます。

まずは、local serverをexpressで立ち上げるためのsetupです。ミドルウェアとしてmulterを使用します。
https://github.com/expressjs/multer

const express = require("express");
const cors = require("cors");
const multer = require("multer");
const app = express();
const port = 3000;

app.use(cors());
const upload = multer({ storage: multer.memoryStorage() });
app.post("/upload", upload.single("file"), (req, res) => {
  console.log(req.file);
  res.send("Success");
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

次にhtmlです。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>画像処理の例</title>
  </head>
  <body>
    <input type="file" id="fileInput" accept="image/*" />

    <script>
      const fileInput = document.getElementById("fileInput");
      const canvas = document.getElementById("canvas");

      fileInput.addEventListener("change", (event) => {
        const file = event.target.files[0];
        const form = new FormData();
        form.append("file", file);

        fetch("http://localhost:3000/upload", {
          method: "POST",
          body: form,
        }).then((response) => {
          console.log(response, "response");
        });
      });
    </script>
  </body>
</html>

まず、ここの出力を確認してみるとFile objectのデータを確認することができます。

const file = event.target.files[0];

https://developer.mozilla.org/ja/docs/Web/API/File
File interfaceはBlobをベースにしており、Blobの機能を継承してユーザーのシステム上のファイルをサポートするように拡張されています。

次に実際にpostされたデータを見てみます。Middlewareとしてmulterを使用しているため下記はmulter obectとなります。中身を見てみるとbufferプロパティの中にBufferオブジェクトが格納されています。

{
  fieldname: 'file',
  originalname: 'ã\x82¹ã\x82¯ã\x83ªã\x83¼ã\x83³ã\x82·ã\x83§ã\x83\x83ã\x83\x88 2024-06-21 11.54.45.png',
  encoding: '7bit',
  mimetype: 'image/png',
  buffer: <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 07 20 00 00 02 a2 08 06 00 00 00 9e a6 cb 8d 00 00 0a aa 69 43 43 50 49 43 43 20 50 72 6f 66 69 ... 252759 more bytes>,
  size: 252809
}

では、実際にwebからpostされたfileがmulterを介してどのようにBufferになっているのかを見てみましょう。entry pointはここです。
https://github.com/expressjs/multer/blob/master/index.js#L11C1-L23C2

下記のstorageによる分岐で処理が変わります。今回はmemoryStorageを選択しています。

  if (options.storage) {
    this.storage = options.storage
  } else if (options.dest) {
    this.storage = diskStorage({ destination: options.dest })
  } else {
    this.storage = memoryStorage()
  }

memoryStorageの処理はこちらです。_handleFileの中でstream.pipe()が実行されていおり、concatを使用しstreamを連結しています。
https://github.com/expressjs/multer/blob/master/storage/memory.js

そして、callbackとしてdataを受け取り、bufferに格納します。
最後はmake-middleware.js_handleFileが実行されます。
https://github.com/expressjs/multer/blob/master/lib/make-middleware.js#L145C4-L164C11

最後に

最後はmulterのコードリーディングになってしまいましたが、Uint8Array / ArraryBuffer / Bufferをまとめてみました。次はStreamについてもう少し詳しく調査してみようと思います。
検証として使ったrepositoryは下記にあります。
https://github.com/Ryoto-kubo/learn-binary

参考

https://stackoverflow.com/questions/42416783/where-to-use-arraybuffer-vs-typed-array-in-javascript
https://ja.javascript.info/arraybuffer-binary-arrays
https://github.com/expressjs/multer

Discussion