유형이 있는 배열 - 브라우저의 바이너리 데이터

Ilmari Heikkinen

소개

유형 배열은 WebGL에서 바이너리 데이터를 처리하는 효율적인 방법이 필요하여 브라우저에 비교적 최근에 추가된 기능입니다. 유형 지정된 배열은 C에서 배열이 작동하는 것과 마찬가지로 유형 지정된 뷰가 있는 메모리 슬래브입니다. 유형 배열은 원시 메모리의 지원을 받으므로 JavaScript 엔진은 데이터를 네이티브 표현으로 번거롭게 변환하지 않고도 메모리를 네이티브 라이브러리에 직접 전달할 수 있습니다. 따라서 유형이 지정된 배열은 바이너리 데이터를 처리하는 WebGL 및 기타 API에 데이터를 전달하는 데 JavaScript 배열보다 훨씬 더 우수한 성능을 보입니다.

유형이 지정된 배열 뷰는 ArrayBuffer의 세그먼트에 단일 유형 배열처럼 작동합니다. Float32Array, Float64Array, Int32Array, Uint8Array와 같이 자체 설명이 포함된 이름을 가진 모든 일반적인 숫자 유형에 대한 뷰가 있습니다. Canvas의 ImageData에서 픽셀 배열 유형을 대체한 특수 뷰인 Uint8ClampedArray도 있습니다.

DataView는 두 번째 유형의 뷰이며 이질적인 데이터를 처리하기 위한 것입니다. DataView 객체는 배열과 같은 API를 사용하는 대신 임의의 바이트 오프셋에서 임의의 데이터 유형을 읽고 쓰는 get/set API를 제공합니다. DataView는 파일 헤더 및 기타 구조체와 유사한 데이터를 읽고 쓰는 데 매우 유용합니다.

유형 지정 배열 사용에 관한 기본사항

유형이 지정된 배열 뷰

유형 지정된 배열을 사용하려면 ArrayBuffer와 뷰를 만들어야 합니다. 가장 쉬운 방법은 원하는 크기와 유형의 유형 지정 배열 뷰를 만드는 것입니다.

// Typed array views work pretty much like normal arrays.
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

유형이 지정된 배열 뷰에는 여러 유형이 있습니다. 모두 동일한 API를 공유하므로 사용법을 알면 사용법도 거의 다 알 수 있습니다. 다음 예에서는 현재 존재하는 각 유형의 배열 뷰 중 하나를 만들겠습니다.

// Floating point arrays.
var f64 = new Float64Array(8);
var f32 = new Float32Array(16);

// Signed integer arrays.
var i32 = new Int32Array(16);
var i16 = new Int16Array(32);
var i8 = new Int8Array(64);

// Unsigned integer arrays.
var u32 = new Uint32Array(16);
var u16 = new Uint16Array(32);
var u8 = new Uint8Array(64);
var pixels = new Uint8ClampedArray(64);

마지막은 약간 특별합니다. 입력 값을 0과 255 사이로 제한합니다. 이제 8비트 범위 오버플로를 방지하기 위해 이미지 처리 수학을 수동으로 고정할 필요가 없으므로 캔버스 이미지 처리 알고리즘에 특히 유용합니다.

예를 들어 다음은 Uint8Array에 저장된 이미지에 감마 계수를 적용하는 방법입니다. 별로 예쁘지 않음:

u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));

Uint8ClampedArray를 사용하면 수동 고정을 건너뛸 수 있습니다.

pixels[i] *= gamma;

유형이 지정된 배열 뷰를 만드는 다른 방법은 먼저 ArrayBuffer를 만든 다음 이를 가리키는 뷰를 만드는 것입니다. 외부 데이터를 가져오는 API는 일반적으로 ArrayBuffers를 처리하므로 이렇게 유형이 지정된 배열 뷰를 가져올 수 있습니다.

var ab = new ArrayBuffer(256); // 256-byte ArrayBuffer.
var faFull = new Uint8Array(ab);
var faFirstHalf = new Uint8Array(ab, 0, 128);
var faThirdQuarter = new Uint8Array(ab, 128, 64);
var faRest = new Uint8Array(ab, 192);

동일한 ArrayBuffer에 여러 뷰를 보유할 수도 있습니다.

var fa = new Float32Array(64);
var ba = new Uint8Array(fa.buffer, 0, Float32Array.BYTES_PER_ELEMENT); // First float of fa.

유형이 지정된 배열을 다른 유형이 지정된 배열에 복사하는 가장 빠른 방법은 유형이 지정된 배열 세트 메서드를 사용하는 것입니다. memcpy와 유사한 용도로는 뷰의 버퍼에 Uint8Array를 만들고 set를 사용하여 데이터를 복사합니다.

function memcpy(dst, dstOffset, src, srcOffset, length) {
  var dstU8 = new Uint8Array(dst, dstOffset, length);
  var srcU8 = new Uint8Array(src, srcOffset, length);
  dstU8.set(srcU8);
};

DataView

이질적인 유형의 데이터가 포함된 ArrayBuffer를 사용하려면 가장 쉬운 방법은 버퍼에 DataView를 사용하는 것입니다. 8비트 부호 없는 정수가 포함된 헤더와 그 뒤에 16비트 정수 2개, 그 뒤에 32비트 부동 소수점의 페이로드 배열이 포함된 파일 형식이 있다고 가정해 보겠습니다. 유형이 지정된 배열 뷰로 이것을 다시 읽을 수는 있지만 약간의 어려움이 있습니다. DataView를 사용하면 헤더를 읽고 부동 소수점 배열에 유형이 지정된 배열 뷰를 사용할 수 있습니다.

var dv = new DataView(buffer);
var vector_length = dv.getUint8(0);
var width = dv.getUint16(1); // 0+uint8 = 1 bytes offset
var height = dv.getUint16(3); // 0+uint8+uint16 = 3 bytes offset
var vectors = new Float32Array(width*height*vector_length);
for (var i=0, off=5; i<vectors.length; i++, off+=4) {
  vectors[i] = dv.getFloat32(off);
}

위 예에서 읽은 값은 모두 big-endian입니다. 버퍼의 값이 리틀엔디언인 경우 선택적 littleEndian 매개변수를 getter에 전달할 수 있습니다.

...
var width = dv.getUint16(1, true);
var height = dv.getUint16(3, true);
...
vectors[i] = dv.getFloat32(off, true);
...

유형이 지정된 배열 뷰는 항상 네이티브 바이트 순서입니다. 속도를 높이기 위한 조치입니다. 엔디언이 문제가 되는 경우 DataView를 사용하여 데이터를 읽고 써야 합니다.

DataView에는 버퍼에 값을 쓰는 메서드도 있습니다. 이러한 setter는 getter와 동일한 방식으로 이름이 지정됩니다. 'set' 뒤에 데이터 유형이 옵니다.

dv.setInt32(0, 25, false); // set big-endian int32 at byte offset 0 to 25
dv.setInt32(4, 25); // set big-endian int32 at byte offset 4 to 25
dv.setFloat32(8, 2.5, true); // set little-endian float32 at byte offset 8 to 2.5

엔디언에 대한 설명

엔디언 또는 바이트 순서는 컴퓨터의 메모리에 다중 바이트 숫자가 저장되는 순서입니다. big-endian이라는 용어는 최상위 바이트를 먼저 저장하는 CPU 아키텍처를 나타내고 little-endian은 최하위 바이트를 먼저 저장하는 CPU 아키텍처를 나타냅니다. 특정 CPU 아키텍처에서 사용되는 엔디언은 완전히 임의적이며, 둘 중 하나를 선택해야 할 합당한 이유가 있습니다. 실제로 일부 CPU는 빅엔디언 데이터와 리틀엔디언 데이터를 모두 지원하도록 구성할 수 있습니다.

엔디언에 대해 걱정해야 하는 이유는 무엇인가요? 이유는 간단합니다. 디스크나 네트워크에서 데이터를 읽거나 쓸 때는 데이터의 엔디언을 지정해야 합니다. 이렇게 하면 데이터를 사용하는 CPU의 엔디언과 관계없이 데이터가 올바르게 해석됩니다. 점점 더 네트워크화되어가는 세상에서는 빅 엔디언이나 리틀 엔디언 같은 모든 종류의 장치를 올바르게 지원하는 것이 필수적입니다. 이러한 장치는 네트워크의 서버 또는 다른 피어에서 오는 바이너리 데이터를 처리해야 할 수 있습니다.

DataView 인터페이스는 특히 파일과 네트워크 간에 데이터를 읽고 쓰도록 설계되었습니다. DataView는 지정된 엔디언을 가진 데이터에 대해 작동합니다. 모든 값에 대한 모든 액세스와 함께 엔디언(대소)을 지정해야 합니다. 이렇게 하면 브라우저가 실행 중인 CPU의 엔디언과 관계없이 바이너리 데이터를 읽거나 쓸 때 일관되고 올바른 결과를 얻을 수 있습니다.

일반적으로 애플리케이션이 서버에서 바이너리 데이터를 읽을 때는 애플리케이션이 내부적으로 사용하는 데이터 구조로 변환하기 위해 한 번 스캔해야 합니다. 이 단계에서는 DataView를 사용해야 합니다. 멀티바이트 유형 배열 뷰(Int16Array, Uint16Array 등)를 XMLHttpRequest, FileReader 또는 기타 입력/출력 API를 통해 가져온 데이터와 직접 사용하는 것은 좋지 않습니다. 유형 배열 뷰가 CPU의 네이티브 엔디언을 사용하기 때문입니다. 이건 나중에 다시 설명하죠

간단한 예를 몇 가지 살펴보겠습니다. Windows BMP 파일 형식은 Windows 초기에 이미지를 저장하는 표준 형식이었지만 위에 링크된 문서에서는 파일의 모든 정수 값이 little-endian 형식으로 저장된다고 명시되어 있습니다. 다음은 이 도움말과 함께 제공되는 DataStream.js 라이브러리를 사용하여 BMP 헤더의 시작 부분을 파싱하는 코드 스니펫입니다.

function parseBMP(arrayBuffer) {
  var stream = new DataStream(arrayBuffer, 0,
    DataStream.LITTLE_ENDIAN);
  var header = stream.readUint8Array(2);
  var fileSize = stream.readUint32();
  // Skip the next two 16-bit integers
  stream.readUint16();
  stream.readUint16();
  var pixelOffset = stream.readUint32();
  // Now parse the DIB header
  var dibHeaderSize = stream.readUint32();
  var imageWidth = stream.readInt32();
  var imageHeight = stream.readInt32();
  // ...
}

다음은 WebGL 샘플 프로젝트HDR 렌더링 데모에 나온 다른 예입니다. 이 데모는 HDR 텍스처를 나타내는 원시 리틀엔디언 부동 소수점 데이터를 다운로드하고 WebGL에 업로드해야 합니다. 다음은 모든 CPU 아키텍처에서 부동 소수점 값을 올바르게 해석하는 코드 스니펫입니다. 'arrayBuffer' 변수가 XMLHttpRequest를 통해 서버에서 다운로드된 ArrayBuffer라고 가정해 보겠습니다.

var arrayBuffer = ...;
var data = new DataView(arrayBuffer);
var tempArray = new Float32Array(
  data.byteLength / Float32Array.BYTES_PER_ELEMENT);
var len = tempArray.length;
// Incoming data is raw floating point values
// with little-endian byte ordering.
for (var jj = 0; jj < len; ++jj) {
  tempArray[jj] =
    data.getFloat32(jj * Float32Array.BYTES_PER_ELEMENT, true);
}
gl.texImage2D(...other arguments...,
  gl.RGB, gl.FLOAT, tempArray);

대략적인 규칙은 웹 서버에서 바이너리 데이터를 수신하면 DataView를 사용하여 한 번만 전달하는 것입니다. 개별 숫자 값을 읽고 다른 데이터 구조, 즉 자바스크립트 객체 (소량의 구조화된 데이터용) 또는 유형이 지정된 배열 보기 (데이터 블록의 경우)에 저장합니다. 이렇게 하면 모든 종류의 CPU에서 코드가 올바르게 작동합니다. 또한 DataView를 사용하여 파일이나 네트워크에 데이터를 쓰고, 만들거나 사용하는 파일 형식을 생성하려면 다양한 set 메서드에 littleEndian 인수를 적절하게 지정해야 합니다.

네트워크를 통해 전송되는 모든 데이터에는 형식과 엔디언이 암시적으로 있습니다 (적어도 다중 바이트 값의 경우). 애플리케이션이 네트워크를 통해 전송하는 모든 데이터의 형식을 명확하게 정의하고 문서화해야 합니다.

유형이 지정된 배열을 사용하는 브라우저 API

현재 유형 배열을 사용하는 다양한 브라우저 API에 대해 간략히 살펴보겠습니다. 현재는 WebGL, Canvas, Web Audio API, XMLHttpRequests, WebSockets, Web Workers, Media Source API, File API가 포함됩니다. API 목록에서 Typed Arrays는 성능에 민감한 멀티미디어 작업과 효율적인 방식으로 데이터를 전달하는 데 적합하다는 것을 알 수 있습니다.

WebGL

유형 배열은 버퍼 데이터와 이미지 데이터를 전달하는 데 사용되는 WebGL에서 처음 사용되었습니다. WebGL 버퍼 객체의 콘텐츠를 설정하려면 유형 배열과 함께 gl.bufferData() 호출을 사용합니다.

var floatArray = new Float32Array([1,2,3,4,5,6,7,8]);
gl.bufferData(gl.ARRAY_BUFFER, floatArray);

유형 지정 배열은 텍스처 데이터를 전달하는 데도 사용됩니다. 다음은 유형이 있는 배열을 사용하여 텍스처 콘텐츠를 전달하는 기본 예입니다.

var pixels = new Uint8Array(16*16*4); // 16x16 RGBA image
gl.texImage2D(
  gl.TEXTURE_2D, // target
  0, // mip level
  gl.RGBA, // internal format
  16, 16, // width and height
  0, // border
  gl.RGBA, //format
  gl.UNSIGNED_BYTE, // type
  pixels // texture data
);

또한 WebGL 컨텍스트에서 픽셀을 읽으려면 유형이 지정된 배열도 필요합니다.

var pixels = new Uint8Array(320*240*4); // 320x240 RGBA image
gl.readPixels(0, 0, 320, 240, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

캔버스 2D

최근에 Canvas ImageData 객체가 Typed Arrays 사양과 호환되도록 만들어졌습니다. 이제 캔버스 요소의 픽셀을 Typed Arrays로 표현할 수 있습니다. 이제 캔버스 요소를 조작하지 않고도 캔버스 픽셀 배열을 만들고 수정할 수 있으므로 유용합니다.

var imageData = ctx.getImageData(0,0, 200, 100);
var typedArray = imageData.data // data is a Uint8ClampedArray

XMLHttpRequest2

XMLHttpRequest에 Typed Array가 지원되므로 이제 JavaScript 문자열을 Typed Array로 파싱하지 않고도 Typed Array 응답을 받을 수 있습니다. 가져온 데이터를 멀티미디어 API에 직접 전달하고 네트워크에서 가져온 바이너리 파일을 파싱하는 데 유용합니다.

XMLHttpRequest 객체의 responseType를 'arraybuffer'로 설정하기만 하면 됩니다.

xhr.responseType = 'arraybuffer';

네트워크에서 데이터를 다운로드할 때는 엔디언 문제를 주의해야 합니다. 위의 엔디언 섹션을 참고하세요.

File API

FileReader는 파일 콘텐츠를 ArrayBuffer로 읽을 수 있습니다. 그런 다음 유형이 지정된 배열 뷰와 DataView를 버퍼에 연결하여 콘텐츠를 조작할 수 있습니다.

reader.readAsArrayBuffer(file);

여기서도 엔디언을 염두에 두어야 합니다. 자세한 내용은 엔디언 섹션을 참고하세요.

이전 가능한 객체

postMessage의 전송 가능한 객체를 사용하면 바이너리 데이터를 다른 창과 웹 워커에 훨씬 더 빠르게 전달할 수 있습니다. 객체를 전송 가능한 항목으로 Worker에게 전송하면 전송 스레드에서 객체에 액세스할 수 없게 되며 수신 Worker가 객체의 소유권을 갖게 됩니다. 이렇게 하면 전송된 데이터가 복사되지 않고 유형이 있는 배열의 소유권만 수신자에게 전송되는 고도로 최적화된 구현이 가능합니다.

전송 가능한 객체를 Web Workers와 함께 사용하려면 작업자에서 webkitPostMessage 메서드를 사용해야 합니다. webkitPostMessage 메서드는 postMessage와 동일하게 작동하지만 하나가 아닌 두 개의 인수를 사용합니다. 추가된 두 번째 인수는 작업자에게 전송하려는 객체 배열입니다.

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

작업자에서 객체를 다시 가져오려면 작업자가 동일한 방식으로 객체를 기본 스레드에 다시 전달하면 됩니다.

webkitPostMessage({results: grand, youCanHaveThisBack: oneGBTypedArray}, [oneGBTypedArray]);

사본이 0개입니다.

미디어 소스 API

최근에는 미디어 요소에 Media Source API의 형태로 유형 지정 배열의 이점도 제공되었습니다. webkitSourceAppend를 사용하여 동영상 데이터가 포함된 유형 배열을 동영상 요소에 직접 전달할 수 있습니다. 이렇게 하면 동영상 요소가 기존 동영상 뒤에 동영상 데이터를 추가합니다. SourceAppend는 단일 동영상 요소를 사용하여 여러 동영상을 재생해야 하는 전면 광고, 재생목록, 스트리밍 등에 유용합니다.

video.webkitSourceAppend(uint8Array);

바이너리 WebSocket

WebSockets와 함께 유형 지정된 배열을 사용하여 모든 데이터를 문자열로 변환하지 않아도 됩니다. 효율적인 프로토콜을 작성하고 네트워크 트래픽을 최소화하는 데 적합합니다.

socket.binaryType = 'arraybuffer';

휴, 다행이다! 이로써 API 검토를 마칩니다. 이제 유형이 지정된 배열을 처리하는 서드 파티 라이브러리를 살펴보겠습니다.

제3자 라이브러리

jDataView

jDataView는 모든 브라우저에 DataView shim을 구현합니다. DataView는 WebKit 전용 기능이었지만 이제는 대부분의 다른 브라우저에서 지원됩니다. Mozilla 개발자팀은 Firefox에서도 DataView를 사용 설정하는 패치를 출시하는 중입니다.

Chrome 개발자 관계팀의 Eric Bidelman은 jDataView를 사용하는 소형 MP3 ID3 태그 리더의 예를 작성했습니다. 다음은 블로그 게시물의 사용 예입니다.

var dv = new jDataView(arraybuffer);

// "TAG" starts at byte -128 from EOF.
// See http://en.wikipedia.org/wiki/ID3
if (dv.getString(3, dv.byteLength - 128) == 'TAG') {
  var title = dv.getString(30, dv.tell());
  var artist = dv.getString(30, dv.tell());
  var album = dv.getString(30, dv.tell());
  var year = dv.getString(4, dv.tell());
} else {
  // no ID3v1 data found.
}

문자열 인코딩

현재 Typed 배열에서 문자열을 사용하는 것은 약간 번거롭지만 이를 도와주는 stringencoding 라이브러리가 있습니다. Stringencoding은 제안된 유형 지정 배열 문자열 인코딩 사양을 구현하므로 향후 출시될 사양을 파악하는 데도 유용합니다.

다음은 stringencoding의 기본 사용 예입니다.

var uint8array = new TextEncoder(encoding).encode(string);
var string = new TextDecoder(encoding).decode(uint8array);

BitView.js

BitView.js라는 유형 배열을 위한 작은 비트 조작 라이브러리를 작성했습니다. 이름에서 알 수 있듯이 비트로 작동한다는 점을 제외하고 DataView와 매우 유사하게 작동합니다. BitView를 사용하면 ArrayBuffer의 지정된 비트 오프셋에서 비트 값을 가져오고 설정할 수 있습니다. BitView에는 임의 비트 오프셋에서 6비트 및 12비트 int를 저장하고 로드하는 메서드도 있습니다.

디스플레이의 긴 변의 픽셀 수가 4,096개 미만인 경향이 있으므로 12비트 int는 화면 좌표를 다루는 데 적합합니다. 32비트 정수 대신 12비트 정수를 사용하면 크기가 62% 줄어듭니다. 더 극단적인 예로, 좌표에 64비트 부동 소수점을 사용하는 Shapefile을 사용하고 있었지만 모델이 화면 크기에서만 표시될 것이기 때문에 정밀도가 필요하지 않았습니다. 6비트 델타가 있는 12비트 기본 좌표로 전환하여 이전 좌표의 변경사항을 인코딩하자 파일 크기가 10분의 1로 줄었습니다. 여기에서 데모를 확인할 수 있습니다.

다음은 BitView.js를 사용하는 예입니다.

var bv = new BitView(arrayBuffer);
bv.setBit(4, 1); // Set fourth bit of arrayBuffer to 1.
bv.getBit(17); // Get 17th bit of arrayBuffer.

bv.getBit(50*8 + 3); // Get third bit of 50th byte in arrayBuffer.

bv.setInt6(3, 18); // Write 18 as a 6-bit int to bit position 3 in arrayBuffer.
bv.getInt12(9); // Read a 12-bit int from bit position 9 in arrayBuffer.

DataStream.js

유형이 지정된 배열의 가장 흥미로운 점은 JavaScript에서 바이너리 파일을 더 쉽게 처리할 수 있다는 점입니다. 문자별로 문자열을 파싱하고 문자를 이진수로 수동으로 변환하는 대신 이제 XMLHttpRequest를 사용하여 ArrayBuffer를 가져오고 DataView를 사용하여 직접 처리할 수 있습니다. 이렇게 하면 예를 들어 MP3 파일을 로드하고 오디오 플레이어에서 사용할 메타데이터 태그를 쉽게 읽을 수 있습니다. 또는 셰이프파일에서 로드하여 WebGL 모델로 변환할 수 있습니다. 또는 JPEG에서 EXIF 태그를 읽고 슬라이드쇼 앱에 표시할 수 있습니다.

ArrayBuffer XHR의 문제는 버퍼에서 구조체와 같은 데이터를 읽는 것이 다소 번거롭다는 점입니다. DataView는 엔디언 안전 방식으로 한 번에 몇 개의 숫자를 읽는 데 적합하고, 유형이 지정된 배열 뷰는 요소 크기가 정렬된 네이티브 엔디언 숫자의 배열을 읽는 데 적합합니다. 편리한 엔디언 안전 방식으로 데이터 배열과 구조체를 읽는 방법이 없다고 생각했습니다. DataStream.js를 입력합니다.

DataStream.js는 ArrayBuffers에서 파일과 같은 방식으로 데이터의 스칼라, 문자열, 배열, 구조체를 읽고 쓰는 유형 배열 라이브러리입니다.

ArrayBuffer에서 부동 소수점 배열을 읽는 예시:

// without DataStream.js
var dv = new DataView(buffer);
var f32 = new Float32Array(buffer.byteLength / 4);
var littleEndian = true;
for (var i = 0; i<f32.length; i++) {
  f32[i] = dv.getFloat32(i*4, littleEndian);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = DataStream.LITTLE_ENDIAN;
var f32 = ds.readFloat32Array(ds.byteLength / 4);

DataStream.js가 유용하게 사용되는 경우는 더 복잡한 데이터를 읽을 때입니다. JPEG 마커를 읽는 메서드가 있다고 가정해 보겠습니다.

// without DataStream.js
var dv = new DataView(buffer);
var objs = [];
for (var i=0; i<buffer.byteLength;) {
  var obj = {};
  obj.tag = dv.getUint16(i);
  i += 2;
  obj.length = dv.getUint16(i);
  i += 2;
  obj.data = new Uint8Array(obj.length - 2);
  for (var j=0; j<obj.data.length; j++,i++) {
    obj.data[j] = dv.getUint8(i);
  }
  objs.push(obj);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = ds.BIG_ENDIAN;
var objs = [];
while (!ds.isEof()) {
  var obj = {};
  obj.tag = ds.readUint16();
  obj.length = ds.readUint16();
  obj.data = ds.readUint8Array(obj.length - 2);
  objs.push(obj);
}

또는 DataStream.readStruct 메서드를 사용하여 데이터 구조체를 읽습니다. readStruct 메서드는 구조체 멤버의 유형을 포함하는 구조체 정의 배열을 가져옵니다. 복잡한 유형을 처리하기 위한 콜백 함수가 있으며 데이터 배열과 중첩 구조체도 처리합니다.

// with DataStream.readStruct
ds.readStruct([
  'objs', ['[]', [ // objs: array of tag,length,data structs
    'tag', 'uint16',
    'length', 'uint16',
    'data', ['[]', 'uint8', function(s,ds){ return s.length - 2; }], // get length with a function
  '*'] // read in as many struct as there are
]);

구조체 정의는 [name, type]-쌍의 플랫 배열입니다. 중첩된 구조체는 유형의 배열을 사용하여 실행됩니다. 배열은 세 개의 요소 배열을 사용하여 정의되며 여기서 두 번째 요소는 배열 요소 유형이고 세 번째 요소는 배열 길이(숫자, 이전에 읽은 필드 참조 또는 콜백 함수)입니다. 배열 정의의 첫 번째 요소는 사용되지 않습니다.

이 유형의 가능한 값은 다음과 같습니다.

Number types

Unsuffixed number types use DataStream endianness.
To explicitly specify endianness, suffix the type with
'le' for little-endian or 'be' for big-endian,
e.g. 'int32be' for big-endian int32.

  'uint8' -- 8-bit unsigned int
  'uint16' -- 16-bit unsigned int
  'uint32' -- 32-bit unsigned int
  'int8' -- 8-bit int
  'int16' -- 16-bit int
  'int32' -- 32-bit int
  'float32' -- 32-bit float
  'float64' -- 64-bit float

String types

  'cstring' -- ASCII string terminated by a zero byte.
  'string:N' -- ASCII string of length N.
  'string,CHARSET:N' -- String of byteLength N encoded with given CHARSET.
  'u16string:N' -- UCS-2 string of length N in DataStream endianness.
  'u16stringle:N' -- UCS-2 string of length N in little-endian.
  'u16stringbe:N' -- UCS-2 string of length N in big-endian.

Complex types

  [name, type, name_2, type_2, ..., name_N, type_N] -- Struct

  function(dataStream, struct) {} -- Callback function to read and return data.

  {get: function(dataStream, struct) {}, set: function(dataStream, struct) {}}
  -- Getter/setter functions to reading and writing data. Handy for using the
     same struct definition for both reading and writing.

  ['', type, length] -- Array of given type and length. The length can be either
                        a number, a string that references a previously-read
                        field, or a callback function(struct, dataStream, type){}.
                        If length is set to '*', elements are read from the
                        DataStream until a read fails.

JPEG 메타데이터 읽기의 실시간 예는 여기에서 확인할 수 있습니다. 이 데모에서는 일부 EXIF 파싱과 함께 JPEG 파일의 태그 수준 구조를 읽는 데 DataStream.js를 사용하고 JavaScript에서 JPEG 이미지를 디코딩하고 표시하는 데 jpg.js를 사용합니다.

유형이 있는 배열의 역사

유형이 있는 배열은 JavaScript 배열을 그래픽 드라이버에 전달하면 성능 문제가 발생한다는 것을 알게 된 WebGL의 초기 구현 단계에서 시작되었습니다. JavaScript 배열에서 WebGL 바인딩은 네이티브 배열을 할당하고 JavaScript 배열을 지나 배열의 모든 JavaScript 객체를 필수 네이티브 유형으로 변환하여 채워야 했습니다.

데이터 변환 병목 현상을 해결하기 위해 Mozilla의 Vladimir Vukicevic은 JavaScript 인터페이스를 사용하는 C 스타일 부동 소수점 배열인 CanvasFloatArray를 작성했습니다. 이제 바인딩에서 추가 작업을 하지 않고도 JavaScript에서 CanvasFloatArray를 수정하고 WebGL에 직접 전달할 수 있습니다. 이후 반복에서 CanvasFloatArray는 WebGLFloatArray로 이름이 바뀌었고, 이는 다시 Float32Array로 이름이 바뀌고 버퍼에 액세스하기 위한 백엔드 ArrayBuffer와 유형이 지정된 Float32Array 뷰로 분할되었습니다. 다른 정수 및 부동 소수점 크기와 부호 있는/부호 없는 변형에 관한 유형도 추가되었습니다.

설계 고려사항

처음부터 Typed Arrays의 설계는 바이너리 데이터를 네이티브 라이브러리에 효율적으로 전달해야 하는 필요성에 따라 이루어졌습니다. 따라서 유형이 지정된 배열 뷰는 호스트 CPU의 네이티브 엔디언에서 정렬된 데이터를 기반으로 작동합니다. 이러한 결정을 통해 JavaScript는 그래픽 카드로 정점 데이터를 전송하는 등의 작업 중에 최대 성능을 달성할 수 있습니다.

DataView는 데이터가 항상 지정된 엔디언을 가지며 최대 성능을 위해 정렬되지 않을 수 있는 파일 및 네트워크 I/O용으로 특별히 설계되었습니다.

인메모리 데이터 조합(유형이 지정된 배열 뷰 사용)과 I/O(DataView 사용) 간의 설계 분할은 의식적인 결정이었습니다. 최신 JavaScript 엔진은 유형이 지정된 배열 뷰를 대폭 최적화하고 이를 사용하여 수치 연산에서 높은 성능을 달성합니다. 유형이 지정된 배열 뷰의 현재 성능 수준은 이러한 설계 결정에 따라 가능했습니다.

참조