일반적으로 C언어에서 struct에 의해 구조화된 바이너리 데이터를 자바스크립트에서 사용할 수 있게 변환하고 이를 다시 조작하여 바이너리로 생성하는 과정이 엘레강스(?)하지가 않아서 C의 struct와 유사하게 자바스크립트에서도 구조체를 사용할 수 있도록 작은 유틸리티를 만들었습니다. 만들고 보니, 다른 곳에서도 유용하게 사용될 수 있을 것 같아 공개합니다.
우선, 자바스크립트만으로 ArrayBuffer를 다루어 보겠습니다. 예제에 사용되는 바이너리 데이터는 0번째 번지에 Uint8 유형의 정수, 1번째 번지에 Int8 유형의 정수, 3번째 번지에는 Uint16 유형의 2바이트짜리 정수이며, 이를 쓰고 다시 읽어내는 것입니다:
// define struct
var struct = {
foo: 255,
bar: 127,
baz: {
qux: 65535
}
};
// write arraybuffer from javascript object
var ab = new ArrayBuffer(4);
var dv = new DataView(ab);
dv.setUint8(0, struct.foo);
dv.setInt8(1, struct.bar);
dv.setUint16(2, struct.baz.qux, true);
console.log(dv.buffer);
// => ArrayBuffer {byteLength: 4, slice: function}
// read data from arraybuffer
var dv2 = new DataView(dv.buffer);
var data = {
foo: dv2.getUint8(0),
bar: dv2.getInt8(1),
baz: {
qux: dv2.getUint16(2, true)
}
};
console.log(data);
// => Obejct {foo: 255, bar: 127, baz: {qux: 65535}}}
옵셋을 손으로 패딩해야하며 형식이 동일한 구조의 데이터를 읽고 생성하려 했지만 도저히 같다고는 느껴지지 않습니다. 그리고 버퍼의 크기가 크면 클수록 사용성이 떨어지는 문제도 있습니다. struct.js를 사용하면 다음과 같이 코드를 작성할 수 있습니다:
// define struct
var struct = new Struct({
foo: ['uint8', 255],
bar: ['int8', 127],
baz: {
qux: ['uint16', 65535]
}
}, 0, true);
// write arraybuffer from javascript object
var ab = struct.write();
console.log(ab);
// => ArrayBuffer {byteLength: 4, slice: function}
// read data from arraybuffer
var data = struct.read(ab);
console.log(data);
// => Obejct {foo: 255, bar: 127, baz: {qux: 65535}}
옵셋을 자동으로 카운트하고, 자바스크립트 형식으로 작성한 데이터 구조체를 재활용하여 새로운 arraybuffer를 생성하거나 반대로 자바스립트에서 읽을 수 있는 데이터로 만들어 사용하기가 수월합니다. 이는 마치 C에서 생성하는 구조체를 사용하는 느낌입니다.
속성(키)/[유형(타입), 값(밸류)]로 구조를 작성해야 하며 '속성/유형'만 지정하면 버퍼를 작성하는 경우 기본값이 할당됩니다. 즉, '밸류'는 write
메서드를 이용하여 ArrayBuffer를 생성하는 곳에만 사용되며, 단순히 read
메서드로 데이터를 읽기만 한다거나, 속성마다 특정한 값을 설정할 필요가 없는 경우라면 타입만 지정해도 된다는 의미입니다. 다음은 read
메서드의 두 번째 인자에 사용자 지정 옵셋을 입력하여 동일한 형식의 데이터가 복수로 담긴 청크를 처리하는 모습입니다.
/**
* read multiple data with custom offset
*/
var struct = new Struct({
sig: 'uint8',
mimeType: 'uint8',
id: 'uint16',
byteLength: 'uint32'
});
...
function parseBinary(chunk, count, callback) {
var offset = 0;
for (var index = 0; index < count; index++) {
var meta = struct.read(chunk, offset)
, buffer = chunk.slice(
offset += struct.byteLength,
offset += meta.byteLength
);
callback(meta, new Uint8Array(buffer));
}
}
write
메서드에 변경할 내용이 담긴 객체를 인자로 전달하여 복수의 값을 갱신할 수 있도 있습니다. 입력 객체는 하위 구조의 값까지 모두 비교하여 값을 할당하기 때문에 다음과 같이 작성해도 무방합니다.(jquery의 $.extend와는 개념이 다름)
// define struct
var struct = new Struct({
foo: ['uint8', 255],
bar: 'int8',
baz: {
qux: ['uint16', 65535],
quux: ['uint32', 0]
}
}, 1, true);
// update values and write arraybuffer
var ab = struct.write({
foo: 0,
baz: {
quux: 4294967295
}
});
// write => ArrayBuffer {byteLength: 8, slice: function}
// read => Obejct {foo: 0, bar: 1, baz: {qux: 65535, quux: 4294967295}}
끝으로, 하나의 속성에 멀티-바이트 타입 배열을 지정할 수 있도록 했습니다. 각각의 번지마다 연속된 값(문자열 또는 배열)을 지정할 수 있으며, 속성에 지정되는 배열의 마지막에 버퍼의 크기를 지정하거나 생략한 경우 그 크기를 자동으로 계산하합니다. 그리고 문자열 형식으로 선언된 속성은 버퍼에서 값을 읽어 올 때 정수들을 모두 문자열로 자동 변환하여 반환합니다.
/**
* create struct with multi-byte value
*/
var struct = new Struct({
foo: ['uint16', [0xffff, 4095]],
bar: ['uint8', 'firejune', 8]
});
var ab = struct.write();
// => ArrayBuffer {byteLength: 12, slice: function}
var obj = struct.read(ab);
// => Object {bar: [65535, 4095], qux: "firejune"}
처음 구성한 구조의 크기(유형)는 변경될 수 없으며, 값만 갱신할 수 있는 규칙을 가집니다. 이 작은 유틸리티의 이름은 거창하게도 struct.js이며, MIT 라이센스를 따릅니다. 대략적인 사용법과 소스코드를 GitHub에 올려 두었으니 필요하신 분은 맘껏 사용하세요.
Comments