EmscriptenのHEAPを使ってC++/JavaScript間でバッファやvectorを参照する
はじめに
TL;DR
- JS からでバッファを参照する時は HEAP におけるポインタと Length を使って TypedArray で取り出す
- C++で受け取る時は int 型のアドレスをポインタ型にキャストする
概要
本記事では、Emscripten で C++を WebAssembly にコンパイルして使うプロジェクトにおいて、バッファのやり取りをする方法を解説します。
JS 側では ArrayBuffer・TypedArray として、C++側では vector として取り扱う前提となっています。
調べてみるといろいろな方法があるみたいですが、今回ご紹介するのは HEAP を使うシンプル方法です。
理解できてしまえば簡単なのですが、ポインタを扱う関係で一瞬難しそうに感じたのと、日本語文献も少なかったので共有しようと考えました。
また、別の方法でemscripten::val
を使う方法もあるみたいですが、今回は取り扱いません。
本当はサンプルプロジェクトを公開したかったのですが、ひとりアドカレという環境ゆえに間に合わず、コードの断片でのみの説明となりますことをお詫びいたします。
検証環境
- Windows 11 Home
- Docker Desktop
- Emscripten(emsdk) 3.1.72
まずはembindする
vector はプリミティブな型ではないため、まずは embind して JS から使えるようにしておきます。
今回はuint8_t(char/byte)
型とfloat
型の vector を使うと想定しましょう。
#include <emscripten.h>
#include <emscripten/bind.h>
// ...
EMSCRIPTEN_BINDINGS(my_module)
{
register_vector<float>("VectorFloat32");
register_vector<uint8_t>("VectorUInt8T");
}
この状態で、-lembind
、--emit-tsd
オプションを使って型定義まで出力してみると、こんな感じになります。
export interface ClassHandle {
isAliasOf(other: ClassHandle): boolean;
delete(): void;
deleteLater(): this;
isDeleted(): boolean;
clone(): this;
}
export interface VectorFloat32 extends ClassHandle {
size(): number;
get(_0: number): number | undefined;
push_back(_0: number): void;
resize(_0: number, _1: number): void;
set(_0: number, _1: number): boolean;
}
export interface VectorUInt8T extends ClassHandle {
push_back(_0: number): void;
resize(_0: number, _1: number): void;
size(): number;
get(_0: number): number | undefined;
set(_0: number, _1: number): boolean;
}
分かる通り、VectorFloat32
やVectorUInt8T
はClassHandle
を継承している interface になっており、このままではどうやら TypedArray として使えるわけではないようです。
JSからC++へTypedArrayを渡す
基本的な方針として、JS から C++の関数を呼び出すときにバッファを渡したい場合、
HEAP に確保したメモリ領域へバッファを書き込み、そのアドレスを渡す感じになります。
たとえば次の例では、binaryBuffer
というUint8Array
を C++の関数へ渡す処理です。
// MainModuleを取得
const wasmModule = await MainModuleFactory();
// pointerはnumber型
// バッファのbyteSize分メモリを確保する
const pointer = wasmModule._malloc(Uint8Array.BYTES_PER_ELEMENT * binaryBuffer.length);
if (pointer === null) {
throw new Error("could'nt allocate memory");
}
// Uint8Arrayの場合はHEAPU8のビューを使う
wasmModule.HEAPU8.set(binaryBuffer, pointer / Uint8Array.BYTES_PER_ELEMENT);
// C++の関数にはバッファの先頭アドレスと長さを渡す
wasModule.someFunc(pointer, binaryBuffer.length);
// ...
wasmModule._free(pointer)
一方で C++側の関数では、ポインタとバッファ長から vector を取得します。
void someFunc(const int ptr, const int length)
{
auto pointer = (uint8_t *)ptr;
auto uint8Vector = vector<uint8_t>(pointer, pointer + length);
// ...
}
EMSCRIPTEN_BINDINGS(my_module)
{
emscripten::function("someFunc", &someFunc, allow_raw_pointers());
}
やっていることはシンプルです。
- JS/C++間では型付きのビューとしての HEAP を利用する
- JS では number 型で先頭アドレスを取得できる
- C++では int 型のアドレスをポインタ型にキャストし、そこから vector を得る
C++のvectorデータをJSからTypedArrayとして参照する
次に C++から渡ってきた vector を JS 側で TypedArray に変換する方法です。
例としてvector<float>
を返す関数を作ってみました。
vector<float> create_vec()
{
vector<float> v{1, 2, 3};
return v;
}
EMSCRIPTEN_BINDINGS(my_module)
{
emscripten::function("create_vec", &create_vec);
}
前述のように、vector は TypedArray として扱えない、ClassHandle になっています。
ではどうするかというと、先ほどの逆をやればよいのですね。
つまり vector のポインタを int にキャストして返してやれば、あとはそのアドレスをもとに JS 側で HEAP からバッファを取り出せます。
そのために C++側で、vector のポインタを int 型で返すためのユーティリティを作ってみました。
int vf32_ptr(vector<float> &v)
{
return (int)(v.data());
}
EMSCRIPTEN_BINDINGS(my_module)
{
emscripten::function("vf32_ptr", &vf32_ptr, allow_raw_pointers());
}
このユーティリティを使ってポインタが取得出来れば、あとは HEAP からバッファを取り出すだけです。
const floatVector = wasmModule.create_vec();
const pointer = wasmModule.vf32_ptr(floatVector);
const size = floatVector.size();
// これでTypedArrayが得られた(コピーではなくビューなので注意)
const buffer = new Float32Array(wasmModule.HEAPF32.buffer, pointer, size);
おわりに
Emscripten で C++/JS 間のバッファのやり取りを取り上げました。
HEAP を使うことでシンプルにバッファのやり取りができるので、バイナリデータなどを扱う際には活用していきたいです。
参考文献
Discussion