EmscriptenでALLOW_MEMORY_GROWTH=1した時ハマった話
WebAssemblyをターゲットとしてEmscriptenを用いてビルドする場合には-s ALLOW_MEMORY_GROWTH=1
を指定して自動的なメモリ領域の拡張を利用することができるが、その時思いがけない問題に遭遇したのでメモ。
状況
EmscriptenでコンパイルされるRustの関数に(C言語風の)数値の配列に対するポインタを渡したいと思って、次のようなコードを書いていた。
const f_ = Module.cwrap('_f', 'number', ['number']); const f = (() => { const sizeofFloat32 = 4; const length = 3; const pointer = Module._malloc(sizeofFloat32 * length); const view = new Float32Array(Module.HEAPU8.buffer, pointer, length); return (a: number[]) => { view.set(a); // !!! f_(pointer); }; })();
このようにArrayBufferViewをクロージャを使ってガメておけば、オブジェクト生成のコストがなくってTypedArray.prototype.set
を使うことができて便利だと思っていたが、このコードはまれにview.set(a);
のところでエラーを吐いて死ぬ。
エラー
このときに出力されたエラーメッセージはこのようであった。
Uncaught TypeError: Cannot perform %TypedArray%.prototype.set on a detached ArrayBuffer
TypedArray
が detached
なステートになってしまったらしい。
ArrayBuffer.transfer()
ArrayBufferにはtransfer
というメソッドが生えていて、C言語で言うところのrealloc
のように、引数として取るArrayBufferに結びついたViewをDetached
状態にし、指定したサイズのArrayBufferを新しく返すというメソッドがある。
何が起きていたのか
コンパイルされたjsのコードからメモリ拡張部分のコードを抜き出すとこうなっている。
var wasmReallocBuffer = (function (size) { var PAGE_MULTIPLE = Module["usingWasm"] ? WASM_PAGE_SIZE : ASMJS_PAGE_SIZE; size = alignUp(size, PAGE_MULTIPLE); var old = Module["buffer"]; var oldSize = old.byteLength; if (Module["usingWasm"]) { try { var result = Module["wasmMemory"].grow((size - oldSize) / wasmPageSize); // WebAssembly.Memory.prototype.grow if (result !== (-1 | 0)) { return Module["buffer"] = Module["wasmMemory"].buffer // Updating Module['buffer'] } else { return null } } catch (e) { return null } } else { exports["__growWasmMemory"]((size - oldSize) / wasmPageSize); return Module["buffer"] !== old ? Module["buffer"] : null } }); Module["reallocBuffer"] = (function (size) { if (finalMethod === "asmjs") { return asmjsReallocBuffer(size) } else { return wasmReallocBuffer(size) } }); function enlargeMemory() { var PAGE_MULTIPLE = Module["usingWasm"] ? WASM_PAGE_SIZE : ASMJS_PAGE_SIZE; var LIMIT = 2147483648 - PAGE_MULTIPLE; if (HEAP32[DYNAMICTOP_PTR >> 2] > LIMIT) { return false } var OLD_TOTAL_MEMORY = TOTAL_MEMORY; TOTAL_MEMORY = Math.max(TOTAL_MEMORY, MIN_TOTAL_MEMORY); while (TOTAL_MEMORY < HEAP32[DYNAMICTOP_PTR >> 2]) { if (TOTAL_MEMORY <= 536870912) { TOTAL_MEMORY = alignUp(2 * TOTAL_MEMORY, PAGE_MULTIPLE) } else { TOTAL_MEMORY = Math.min(alignUp((3 * TOTAL_MEMORY + 2147483648) / 4, PAGE_MULTIPLE), LIMIT) } } var replacement = Module["reallocBuffer"](TOTAL_MEMORY); if (!replacement || replacement.byteLength != TOTAL_MEMORY) { TOTAL_MEMORY = OLD_TOTAL_MEMORY; return false } updateGlobalBuffer(replacement); // Update Module['buffer'] updateGlobalBufferViews(); // Update HEAP*** return true }
コードを読んだ感じだとModule['buffer']
を書き換えているし、WebAssembly.Memory.prototype.grow
が自身のbuffer
を別物にすり替えることがあるらしい*1。内部でArrayBuffer.transfer
を呼んでいると考えると今回のエラーの説明がつく。WebAssembly.Memory
の実装はまだ読んでいないので、確定的なことはなんとも言えないけど。
結論
どうやら、Emscriptenの管理しているヒープはそもそも別オブジェクトにすり替わったりするので、クロージャの内部に持ったりすることが無いようにしなければならないようだ。呼び出す側から持たせるデータはポインタのみにして、毎回毎回ArrayBufferViewを作り直すか、Module.setValue
等で各要素書き込んでやるなどする必要がありそうだ。少しつらい気持ちになった。
*1:Cのrealloc()が別のポインタを返す事があることを考えれば自然ではある、同じ場所で連続なメモリを確保できるとは限らないし