ukyo/lz4.jsでnode.jsのTransform streamを扱えるようにしたのでメモ。実装はC言語。今回の実装によって
- jsオブジェクトとcの構造体の紐付け
- 複数のstreamを同時に実行(多分)
が実現できた感じです。
実装メモ
c: 常に構造体のポインタを介するコードにする。
LZ4JS_compressionContext_t* LZ4JS_createCompressionContext(LZ4F_blockSizeID_t blockSizeID, LZ4F_blockMode_t blockMode, LZ4F_contentChecksum_t contentChecksum, unsigned int compressionLevel);
LZ4F_decompressionContext_t* LZ4JS_createDecompressionContext();
void LZ4JS_freeCompressionContext(LZ4JS_compressionContext_t* cctxPtr);
void LZ4JS_freeDecompressionContext(LZ4F_decompressionContext_t* dctxPtr);
int LZ4JS_compressBegin(LZ4JS_compressionContext_t* cctxPtr);
int LZ4JS_compressUpdate(LZ4JS_compressionContext_t* cctxPtr);
int LZ4JS_compressEnd(LZ4JS_compressionContext_t* cctxPtr);
int LZ4JS_decompress(LZ4F_decompressionContext_t* dctxPtr);
js: 返ってきたポインタをキーにしてオブジェクトにインスタンスを保存。
function BaseCompressor(options) {
this.options = assign({}, defaultCompressOptions, options);
this.cctxPtr = _LZ4JS_createCompressionContext(/* ...中略... */); // ポインタ返ってくる
if (!this.cctxPtr) throw new Error('LZ4JS_createCompressionContext');
LZ4JS_instances[this.cctxPtr] = this; // ポインタをキーにしてインスタンスを保存
this.$error = null;
}
js: 必要なリソース、今回はバイト列src
と長さsrcSize
を用意してC側の関数を呼び出す。
CompressStream.prototype['_transform'] = function(chunk, encoding, callback) {
try {
/* ...中略... */
for (offset = 0; offset < chunk.length; offset += BUF_SIZE) {
this.srcSize = Math.min(chunk.length - offset, BUF_SIZE);
this.src = chunk.slice(offset, offset + this.srcSize);
this.compressUpdate(); // 以下参照
}
callback();
} catch (error) {
callback(error);
}
};
BaseCompressor.prototype.compressUpdate = function(first_argument) {
_LZ4JS_compressUpdate(this.cctxPtr) || this.cleanup();
};
c: LZ4JS_read
でjsから入力を行う(自作fread
的なやつ、以下参照)。
int LZ4JS_compressUpdate(LZ4JS_compressionContext_t* cctxPtr) {
size_t n = LZ4F_compressUpdate(cctxPtr->cctx, dst, dstMaxSize, src, LZ4JS_read(cctxPtr, src, BUF_SIZE), NULL);
if (LZ4JS_validate(cctxPtr, n)) {
LZ4JS_write(cctxPtr, dst, n);
return 1;
}
return 0;
}
c: EM_ASM_INT
を使ってjs側のコードにキーptr
とバイト列の先頭位置buf
と最大サイズsize
を送る。
size_t LZ4JS_read(void* ptr, char* buf, size_t size) {
return EM_ASM_INT({return LZ4JS_read($0, $1, $2)}, ptr, buf, size);
}
js: キーでインスタンスを参照して、実装されている$read
メソッドを呼び出す。
function LZ4JS_read(id, srcPtr, size) {
return LZ4JS_instances[id].$read(srcPtr, size);
}
js: emscripten上のメモリHEAPU8
に直接書き込む。
CompressStream.prototype.$read = function(srcPtr, size) {
HEAPU8.set(new Uint8Array(this.src.buffer, this.src.byteOffset, this.srcSize), srcPtr);
return this.srcSize;
};
c: すると、LZ4F_compressUpdate
呼び出し時点でデータが入っているという。LZ4JS_validate
、LZ4JS_write
も同様にjs側とやりとりをする。
int LZ4JS_compressUpdate(LZ4JS_compressionContext_t* cctxPtr) {
size_t n = LZ4F_compressUpdate(cctxPtr->cctx, dst, dstMaxSize, src, LZ4JS_read(cctxPtr, src, BUF_SIZE), NULL);
if (LZ4JS_validate(cctxPtr, n)) {
LZ4JS_write(cctxPtr, dst, n);
return 1;
}
return 0;
}
まとめ
EM_ASM
、HEAPU8
や構造体のポインタを直接利用してアクロバティックにjsとやり取りする方法を紹介しました。ちなみに、jsはシングルスレッドなのでc側の配列はstreamごとに作らないで共有しても大丈夫です。