C++ to TS
Emscriptenを使ってC++のコードをJSに変換して,TS用の型定義ファイルを準備する.
主に過去に作ったC/C++の資産をWASM化したいときを想定する.
新規でWASMを作るならRustでするのがいいと思う.
Emscriptenをインストールのするのはなんか面倒なので,dockerのemscripten/emsdkでトランスコンパイルさせる.
docker pull emscripten/emsdk
適当なmain関数だけを定義したhello.cppをsrcディレクトリに配置.以下コマンドでjsファイルを作成&実行.
docker run --rm -v ./src:/src -u $(id -u):$(id -g) emscripten/emsdk emcc hello.cpp -o hello.js -sWASM=0
node hello.js
emccのオプション:
- sWASM: WASMを作るかどうか.0でjsのみ.1でWASMとそれを読み込むjsファイル.2は0と1で作られるファイルすべて(0のファイルは.wasm.jsという名前).デフォルトで1.
- sMODULARIZE: モジュールから関数を取り出す方法をPromiseによるものにするかどうか.0の時はグローバルにインポートされる.1でPromiseによるものに変わる.
- sEXPORT_ES6: ES6のexport形式でモジュール化する.自動的にsMODULARIZE=1となる.
ES6モジュールにしたいので,やるならこっちのコマンドのほうが良さそう.
docker run --rm -v ./src:/src -u $(id -u):$(id -g) emscripten/emsdk emcc hello.cpp -o hello.mjs -sEXPORT_ES6=1
JS側の呼び出し例.hello.cppにhello関数を定義し公開する.js側では作成した"src/hello.mjs"をインポートして,awaitでモジュールのインスタンスを取得し,hello関数を実行する.
import createModule from "./src/hello.mjs";
(async () => {
const module = await createModule()
module.hello();
})();
そのほか関係ありそうなオプション:
- sWASM_WORKERS: C++のコードでマルチスレッドを使用するときに,Web上ではWeb Workersを使用する (WASM Workers) ときのオプション.
-
sSHARED_MEMORY: Web Workersを使用するときに共有メモリ (SharedArrayBuffer)を使うオプション.通常はWeb Workersでメモリは共有できないが,SharedArrayBufferとAtomics APIにより利用可能となる.
- sAUDIO_WORKLET: C++のコードで音声処理の別スレッド実行をする際に,WASMではWeb Audio APIのAudioWorkletインターフェースを用いて行う (Wasm Audio Worklets API)ときのオプション.
- sEMVIRONMENT: モジュールを使用用途に応じた形式に書き出す(web, workerなど).カンマ区切りで複数選択可能.
- sUSE_PTHREADS: C++のコードをpthreadを使って別スレッドで動作させる.つまりWeb Workerを使わずともWASMモジュールを読み込んだだけで別スレッドでモジュールが動作する.
- sPTHREAD_POOL_SIZE: pthreadで起動させるスレッド数.
- SHARED_MEMORY: WASMで管理しているメモリ領域 (WebAssembly.Memory) を外部にSharedArrayBufferとして公開する.
- WASMモジュール外からSharedArrayBufferを渡す場合はこのオプションは不要.
C++側のコードの設定.
main関数は何もしていなくても公開される.モジュール読み込み時に自動的に実行される.
公開される関数はmain関数で使用している関数のみ.つまりmainが存在しないファイルをコンパイルすると,なにも公開されない.
関数を明示的に公開するにはEMSCRIPTEN_KEEPALIVEを関数宣言につけるまたはコンパイル時にsEXPORTED_FUNCTIONSを指定する.
コンパイル時にsEXPORT_ALL=1としてすべて公開するのもあり.
embindを使うとクラスなども公開できる.
バインドするものをEMSCRIPTEN_BINDINGSブロック内で登録していく.
関数はemscripten::functionに登録する.
#include <iostream>
#ifdef __EMSCRIPTEN__
#include <emscripten/bind.h>
#endif
void my_func()
{
std::cout << "function called" << std::endl;
}
#ifdef __EMSCRIPTEN__
EMSCRIPTEN_BINDINGS(my_module)
{
emscripten::function("my_func", &my_func);
}
#endif
コンパイルはemccに-lembindオプションをつける.
クラスを公開するときはコンストラクタ関数やプロパティ,メソッドを指定できる.
使用するときはモジュールに登録されているコンストラクタ関数を呼び出す形になる.使用後にdeleteメソッドを呼び出さないとメモリリークするので注意.
C++
#ifdef __EMSCRIPTEN__
#include <emscripten/bind.h>
#endif
class MyClass
{
private:
int hoge_;
public:
MyClass(int value) noexcept : hoge_(value) {}
void hogeHoge() noexcept { ++hoge_; }
int getHoge() const noexcept { return hoge_; }
};
#ifdef __EMSCRIPTEN__
EMSCRIPTEN_BINDINGS(my_module)
{
emscripten::class_<MyClass>("MyClass")
.constructor<int>()
.function("hogeHoge", &MyClass::hogeHoge)
.property("hoge", &MyClass::getHoge);
}
#endif
JS
import createModule from "./src/hello.mjs";
(async () => {
const module = await createModule()
module.my_func();
let myClass1 = new module.MyClass(1);
console.log(myClass1.hoge);
myClass1.hogeHoge();
console.log(myClass1.hoge);
myClass1.delete(); // 注意!
})();
TS用型定義(.d.tsファイル)はコンパイル時に--emit-tsd <path>で作成されるっぽい.
上のdokckerイメージだとtscが入っていなくてエラーが出た.
emcc: error: tsc was not found! Please run "npm install" in Emscripten root directory to set up npm dependencies
わざわざTSをインストールしたdockerイメージを作るまでもないので,コマンドでどうにかする.
npm i -g typescriptするときに,WSLだとDNSで名前解決できないとエラーが出た.大した設定も必要のなさそうな以下の記事を参考にする.
最終コマンド:
docker run --rm -it -v ./src:/src -u $(id -u):$(id -g) --network=host \
emscripten/emsdk bash -c "npm i -g typescript && \
emcc hello.cpp -o hello.mjs -lembind -sEXPORT_ES6=1 --emit-tsd hello.d.ts"
jsファイルとwasmファイルと同じディレクトリにd.tsファイルが作成される.
今日は--network=hostが無くてもコンパイルできた.なぜ...?
makeを使ったプロジェクトでemccを使ってコンパイルする場合は,Emscripten SDKにあるemconfigureとemmakeを使うらしい.make使ってないので試してない...
emconfigure ./configure
emmake make
# 作成されたオブジェクトファイルをリンクする.
# 必要であればオブジェクトファイルをリネームする.
# ライブラリを作成している場合はmain.cもリンク対象に含める.
emcc project.o -o project.js
上の公式ドキュメントにはcmakeの場合は「"./configure"を"cmake ."に置き換えてね」と書いているが,その通りにするとエラーがでる.
error: use `emcmake` rather then `emconfigure` for cmake projects
emcmakeを使ってcmakeすると動いた.
emcmake cmake .
emmake make
emccの出力ファイル名を.jsではなく.wasmにすると,WASIで動かすことを前提としたWASM (standalone WASM) が生成される.このときmain関数が存在しない状態でコンパイルしたい場合は--no-entryオプションをつける.
--no-entryオプションは通常のコンパイル時でも発生することがあるので注意.以下のエラーが表示される.
error: entry symbol not defined (pass --no-entry to suppress): main
最適化オプション:
-
-Oz:-O3かつファイルサイズも小さくなる最適化オプション. -
--closure <level>: Closure Compilerを使ってJSファイルやWASMファイルのコード量を少なくする.levelは1にする(-sWASM=0の時は2にする). -
-flto: リンクの最適化を有効にする. -
-fno-exceptions: 例外機構のコードを省略する.例外処理を使わない場合に有効. -
-fno-rtti: RTTIのコードを省略する.dynamic_castやtypeidを使わない場合に有効. -
-sALLOW_MEMORY_GROWTH:mallocなど動的にメモリ割り当てをすることにより,必要なメモリサイズが事前に計算できない場合に設定する.-sUSE_PTHREADS=1の時は使えない.
-Oz --closure 1 -sFILESYSTEM=0 -fltoくらいが良さそう.
-sENVIRONMENTでは出力するモジュールファイルを動作環境に適した形で出力するための設定を行う.例えばデフォルトだとnodeが有効なため,ダイナミックインポートが有効になるが,webのみにするとこれがなくなる.
web, worker, nodeあたりを設定しておけば良さそう.