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

Ilmari Heikkinen

소개

유형 배열은 상대적으로 최근에 브라우저에 추가된 것으로, WebGL에서 바이너리 데이터를 처리할 수 있는 효율적인 방법이 필요하기 때문에 탄생했습니다. 유형이 있는 배열은 C에서 배열이 작동하는 방식과 마찬가지로 유형이 지정된 뷰가 포함된 메모리 슬랩입니다. 유형 배열은 원시 메모리에 의해 지원되므로 자바스크립트 엔진이 데이터를 네이티브 표현으로 힘들게 변환할 필요 없이 메모리를 네이티브 라이브러리에 직접 전달할 수 있습니다. 따라서 유형이 지정된 배열은 WebGL 및 바이너리 데이터를 처리하는 기타 API에 데이터를 전달할 때 JavaScript 배열보다 훨씬 더 나은 성능을 발휘합니다.

유형이 있는 배열 뷰는 ArrayBuffer의 세그먼트에 대해 단일 유형 배열처럼 작동합니다. Float32Array, Float64Array, Int32Array 및 Uint8Array와 같은 자기 설명적 이름을 사용하는 모든 일반적인 숫자 유형에 대한 뷰가 있습니다. 캔버스의 ImageData: Uint8ClampedArray에서 픽셀 배열 유형을 대체하는 특수 뷰도 있습니다.

DataView는 두 번째 유형의 뷰로 이종 데이터를 처리하기 위한 것입니다. 배열과 같은 API를 사용하는 대신 DataView 객체는 임의의 바이트 오프셋에서 임의의 데이터 유형을 읽고 쓸 수 있는 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비트의 부호 없는 int와 2개의 16비트 정수, 32비트 부동 소수점 수의 페이로드 배열이 차례로 있는 헤더가 있다고 가정해 보겠습니다. 유형이 지정된 배열 뷰로 이것을 다시 읽는 것은 가능하지만 약간의 어려움이 있습니다. DataView를 사용하면 헤더를 읽고 유형이 있는 배열 뷰를 float 배열에 사용할 수 있습니다.

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입니다. 버퍼의 값이 little-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는 big-endian 및 little-endian 데이터를 모두 지원하도록 구성할 수 있습니다.

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

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

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

몇 가지 간단한 예를 살펴보겠습니다. 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 샘플 프로젝트에 있는 High Dynamic Range 렌더링 데모의 예입니다. 이 데모는 HDR(High Dynamic Range) 텍스처를 나타내는 원시 리틀 엔디언 부동 소수점 데이터를 다운로드하여 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를 사용하여 해당 데이터를 한 번 전달하는 것입니다. 개별 숫자 값을 읽고 JavaScript 객체 (소량의 구조화된 데이터용) 또는 유형이 지정된 배열 보기 (큰 데이터 블록용)와 같은 다른 데이터 구조에 저장합니다. 그래야 코드가 모든 종류의 CPU에서 올바르게 작동합니다. 또한 DataView를 사용하여 파일이나 네트워크에 데이터를 쓰고, 만들거나 사용 중인 파일 형식을 생성하려면 다양한 set 메서드에 littleEndian 인수를 적절하게 지정해야 합니다.

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

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

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

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 객체가 유형 지정 배열 사양과 호환되도록 만들어졌습니다. 이제 캔버스 요소에서 픽셀의 유형이 지정된 배열 표현을 가져올 수 있습니다. 이 기능은 이제 캔버스 요소를 이리저리 돌아다닐 필요 없이 캔버스 픽셀 배열을 만들고 수정할 수 있으므로 유용합니다.

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

XMLHttpRequest2

XMLHttpRequest는 유형이 지정된 배열 부스트를 얻었으며 이제 JavaScript 문자열을 유형이 있는 배열로 파싱하는 대신 유형이 있는 배열 응답을 받을 수 있습니다. 가져온 데이터를 멀티미디어 API에 직접 전달하고 네트워크에서 가져온 바이너리 파일을 파싱하는 데 매우 유용합니다.

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

xhr.responseType = 'arraybuffer';

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

파일 API

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

reader.readAsArrayBuffer(file);

이때도 엔디언을 염두에 두어야 합니다. 자세한 내용은 엔디언 섹션을 확인하세요.

전송 가능한 객체

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

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

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

객체를 worker로부터 다시 가져오기 위해 worker는 동일한 방식으로 객체를 기본 스레드에 다시 전달할 수 있습니다.

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

사본 없음!

미디어 소스 API

최근에는 미디어 요소에 Media Source API 형식의 유형 지정 배열 기능이 추가되었습니다. 동영상 데이터가 포함된 Typed Array를 webkitSourceAttach를 사용하여 동영상 요소에 직접 전달할 수 있습니다. 이렇게 하면 동영상 요소가 기존 동영상 뒤에 동영상 데이터를 추가합니다. SourceAppend는 전면 광고, 재생목록, 스트리밍 및 하나의 동영상 요소로 여러 동영상을 재생하고자 하는 경우에 유용합니다.

video.webkitSourceAppend(uint8Array);

바이너리 WebSocket

유형이 지정된 배열을 WebSocket과 함께 사용하면 모든 데이터를 문자열화할 필요가 없습니다. 효율적인 프로토콜을 작성하고 네트워크 트래픽을 최소화하는 데 적합합니다.

socket.binaryType = 'arraybuffer';

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

제3자 라이브러리

jDataView

jDataView는 모든 브라우저에서 DataView shim을 구현합니다. DataView는 이전에는 WebKit 전용 기능이었지만 이제 대부분의 다른 브라우저에서 지원됩니다. Mozilla 개발자 팀은 Firefox에서도 DataView를 사용할 수 있도록 패치를 제공하고 있습니다.

Chrome 개발자 관계팀의 에릭 비델만이 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.
}

문자열 인코딩

유형이 지정된 배열에서 문자열을 사용하는 것은 현재 다소 어려운 일이지만 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비트 정수를 저장하고 로드하는 메서드도 있습니다.

디스플레이는 일반적으로 더 긴 쪽을 따라 4096픽셀 미만을 차지하기 때문에 12비트 정수는 화면 좌표로 작업하는 데 적합합니다. 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

유형이 지정된 배열에 관한 가장 흥미로운 점 중 하나는 자바스크립트에서 바이너리 파일을 더 쉽게 처리할 수 있도록 하는 방법입니다. 문자별로 문자열을 파싱하고 문자를 이진수로 수동으로 변환하는 대신, 이제 XMLHttpRequest를 사용하여 ArrayBuffer를 가져오고 DataView를 사용하여 직접 처리할 수 있습니다. 따라서 손쉽게 MP3 파일을 로드하고 메타데이터 태그를 읽어 오디오 플레이어에서 사용할 수 있습니다. 또는 Shapefile을 로드하고 WebGL 모델로 변환할 수 있습니다. 또는 EXIF 태그를 JPEG로 읽고 슬라이드쇼 앱에 표시할 수 있습니다.

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

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

ArrayBuffer에서 float 배열을 읽는 예:

// 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]-pair의 플랫 배열입니다. 중첩 구조체는 유형의 배열을 보유하는 방식으로 수행됩니다. 배열은 3요소 배열을 사용하여 정의됩니다. 여기서 두 번째 요소는 배열 요소 유형이고 세 번째 요소는 배열 길이입니다 (숫자, 이전에 읽은 필드에 대한 참조 또는 콜백 함수로 사용). 배열 정의의 첫 번째 요소는 사용되지 않습니다.

유형에 사용할 수 있는 값은 다음과 같습니다.

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 메타데이터로 읽는 실제 예를 확인할 수 있습니다. 데모에서는 DataStream.js를 사용하여 JPEG 파일의 태그 수준 구조 (일부 EXIF 파싱 포함)를 읽고, jpg.js를 사용하여 JPEG 이미지를 JavaScript로 디코딩하고 표시합니다.

유형이 지정된 배열의 기록

유형이 있는 배열은 JavaScript 배열을 그래픽 드라이버에 전달하는 것이 성능 문제를 일으킨다는 사실을 알았을 때 WebGL의 초기 구현 단계에서 시작되었습니다. JavaScript 배열을 사용하면 WebGL 바인딩이 네이티브 배열을 할당하고 JavaScript 배열을 걸어 이 배열을 채우고, 배열의 모든 JavaScript 객체를 필요한 네이티브 유형으로 변환해야 했습니다.

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

디자인 고려사항

처음부터 타입 지정 배열은 바이너리 데이터를 네이티브 라이브러리에 효율적으로 전달해야 한다는 생각에서 비롯되었습니다. 따라서 유형이 있는 배열 뷰는 호스트 CPU의 네이티브 엔디언에 정렬된 데이터에 따라 작동합니다. 이러한 결정을 통해 JavaScript가 그래픽 카드에 꼭짓점 데이터를 보내는 것과 같은 작업 중에 최대 성능에 도달할 수 있습니다.

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

메모리 내 데이터 어셈블리 (입력된 배열 뷰 사용)와 I/O (DataView 사용) 간에 분할된 설계는 의식적이었습니다. 최신 JavaScript 엔진은 유형이 지정된 배열 뷰를 크게 최적화하고 이러한 뷰로 숫자 연산에서 높은 성능을 달성합니다. 유형이 지정된 배열 뷰의 현재 성능 수준은 이 설계 결정으로 가능했습니다.

참조