通信データ量を削減するため、圧縮したデータをダウンロードしてブラウザ側で解凍するという場面は結構あるかと思います。
はやりのZstandardのWASMでブラウザの解凍処理を試そうとしたところ、ビルドする際に色々はまったので作業ログを残しておきます。
この記事では解凍(decoder)だけを扱います
要約
Zstandardのビルド時にemcc
を-s MALLOC=dlmalloc or 指定なし
でビルドすればOK
試行結果をGitHubに置いておきます。
WASMのビルドが通らない
先人たちがZstandardをWASMでビルドした例を公開してくれています。ありがたいですね。
https://github.com/donmccurdy/zstddec-wasm
しかし、2025年1月現在ではこのコマンドでビルドが通らなくなっているためコマンドを見直します。
$ git clone https://github.com/facebook/zstd.git
$ cd zstd/build/single_file_libs/
$ ./combine.sh -r ../../lib -o zstddeclib.c zstddeclib-in.c # ここまではOK
# Emscriptenのビルドが通らない・・・
$ emcc zstddeclib.c -Oz \
-s EXPORTED_FUNCTIONS="['_ZSTD_decompress', '_ZSTD_findDecompressedSize', '_ZSTD_isError', '_malloc', '_free']" \
-s ALLOW_MEMORY_GROWTH=1 \
-s MALLOC=emmalloc \
-o zstddec.wasm
wasm-ld: error: /home/hogehoge/work/wasm/emsdk/upstream/emscripten/cache/sysroot/lib/wasm32-emscripten/libstandalonewasm-nocatch-memgrow.a(__main_void.o): undefined symbol: main
emcc: error: '/home/hogehoge/work/wasm/emsdk/upstream/bin/wasm-ld -o zstddec.wasm /tmp/emscripten_temp_z46po2ux/zstddeclib_0.o -L/home/hogehoge/work/wasm/emsdk/upstream/emscripten/cache/sysroot/lib/wasm32-emscripten /home/hogehoge/work/wasm/emsdk/upstream/emscripten/cache/sysroot/lib/wasm32-emscripten/crt1.o -lGL-getprocaddr -lal -lhtml5 -lc_optz -lstandalonewasm-nocatch-memgrow -lstubs -lc -lemmalloc -lcompiler_rt -lc++-noexcept -lc++abi-noexcept -lsockets -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr /tmp/tmp13wdvq7blibemscripten_js_symbols.so --strip-debug --export=ZSTD_decompress --export=ZSTD_findDecompressedSize --export=ZSTD_isError --export=malloc --export=free --export=emscripten_stack_get_current --export=_emscripten_stack_restore --export-if-defined=__start_em_asm --export-if-defined=__stop_em_asm --export-if-defined=__start_em_lib_deps --export-if-defined=__stop_em_lib_deps --export-if-defined=__start_em_js --export-if-defined=__stop_em_js --export-table -z stack-size=65536 --max-memory=2147483648 --initial-heap=16777216 --table-base=1 --global-base=1024' failed (returned 1)
main関数がないというエラーなので--no-entry
を追加します。
$ emcc zstddeclib.c -Oz \
-s EXPORTED_FUNCTIONS="['_ZSTD_decompress', '_ZSTD_findDecompressedSize', '_ZSTD_isError', '_malloc', '_free']" \
-s ALLOW_MEMORY_GROWTH=1 \
-s MALLOC=emmalloc \
--no-entry \
-o zstddec.wasm
$ base64 -w 0 zstddec.wasm > zstddec.txt
無事にビルドできましたのでJavaScript側から利用するためにBase64に変換しておきます。
ビルドしたWASMでZstandardのテストが通らない
上記のリポジトリにはZstandardのWASMで正常に解凍できているか確認するテストコードがありますので、まずはgit cloneしたままの内容でテストが通ることを確認します。
$ git clone https://github.com/donmccurdy/zstddec-wasm.git
$ cd zstddec-wasm/
$ npm install
$ npm run dist
$ npm run test
> zstddec@0.1.0 test
> npm run test:node && npm run test:browser
> zstddec@0.1.0 test:node
> tape *.test.cjs | tap-spec
zstddec
✔ decodes text
total: 1
passing: 1
duration: 37ms
無事にテストが通りました。次は本命のビルドしたWASMで試してみましょう。
WASMをBASE64に変換したzstddec.txt
の内容をzstddec-wasm/zstddec.ts
のconst wasm = '...'
に上書きして、再度テストを実行します。
$ npm run dist
$ npm run test
> zstddec@0.1.0 test
> npm run test:node && npm run test:browser
> zstddec@0.1.0 test:node
> tape *.test.cjs | tap-spec
zstddec
✖ RuntimeError: memory access out of bounds
--------------------------------------------
operator: error
stack: |-
RuntimeError: memory access out of bounds
at wasm://wasm/00031c36:wasm-function[43]:0xbb14
at wasm://wasm/00031c36:wasm-function[42]:0xb991
at wasm://wasm/00031c36:wasm-function[44]:0xbc60
at ZSTDDecoder.decode (/home/hoge/work/wasm/temp/zstddec-wasm/dist/zstddec.cjs:47:44)
at Test.<anonymous> (/home/hoge/work/wasm/temp/zstddec-wasm/zstddec.test.cjs:17:20)
Failed Tests: There was 1 failure
zstddec
✖ RuntimeError: memory access out of bounds
total: 1
passing: 0
failing: 1
duration: 35ms
テストが失敗しました。✖ RuntimeError: memory access out of bounds
のエラーですが、dist/zstddec.cjs
の47行目のmallocが問題のようです。
// dist/zstddec.cjs
var compressedPtr = instance.exports.malloc(compressedSize); # このmallocはエラーにならない
heap.set(array, compressedPtr);
// Decompress into WASM memory.
uncompressedSize = uncompressedSize || Number(instance.exports.ZSTD_findDecompressedSize(compressedPtr, compressedSize));
var uncompressedPtr = instance.exports.malloc(uncompressedSize); # 47行目 このmallocでエラー
var actualSize = instance.exports.ZSTD_decompress(uncompressedPtr, uncompressedSize, compressedPtr, compressedSize);
ビルド時のMALLOC
の指定をemmalloc
からデフォルト値のdlmalloc
に変えてみます。
$ emcc zstddeclib.c -Oz \
-s EXPORTED_FUNCTIONS="['_ZSTD_decompress', '_ZSTD_findDecompressedSize', '_ZSTD_isError', '_malloc', '_free']" \
-s ALLOW_MEMORY_GROWTH=1 \
-s MALLOC=dlmalloc \
--no-entry \
-o zstddec.wasm
$ base64 -w 0 zstddec.wasm > zstddec.txt
再度zstddec.txt
のconst wasm = '...'
に上書きしてテスト実行したところ、テストが通りました。とりあえず動いたのですが、なんでemmalloc
だと動かないんでしょうね…
$ npm run dist
$ npm run test
> zstddec@0.1.0 test
> npm run test:node && npm run test:browser
> zstddec@0.1.0 test:node
> tape *.test.cjs | tap-spec
zstddec
✔ decodes text
total: 1
passing: 1
duration: 39ms