Open21

C++ to TS

RerrahRerrah

Emscriptenをインストールのするのはなんか面倒なので,dockerのemscripten/emsdkでトランスコンパイルさせる.

docker pull emscripten/emsdk
RerrahRerrah

適当な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
RerrahRerrah

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となる.

https://emscripten.org/docs/tools_reference/settings_reference.html

RerrahRerrah

ES6モジュールにしたいので,やるならこっちのコマンドのほうが良さそう.

docker run --rm -v ./src:/src -u $(id -u):$(id -g) emscripten/emsdk emcc hello.cpp -o hello.mjs -sEXPORT_ES6=1
RerrahRerrah

JS側の呼び出し例.hello.cppにhello関数を定義し公開する.js側では作成した"src/hello.mjs"をインポートして,awaitでモジュールのインスタンスを取得し,hello関数を実行する.

import createModule from "./src/hello.mjs";

(async () => {
    const module = await createModule()
    module.hello();
})();
RerrahRerrah

そのほか関係ありそうなオプション:

  • sWASM_WORKERS: C++のコードでマルチスレッドを使用するときに,Web上ではWeb Workersを使用する (WASM Workers) ときのオプション.
  • sSHARED_MEMORY: Web Workersを使用するときに共有メモリ (SharedArrayBuffer)を使うオプション.
    • 通常はWeb Workersでメモリは共有できないが,SharedArrayBufferとAtomics APIにより利用可能となる.
  • sAUDIO_WORKLET: C++のコードで音声処理の別スレッド実行をする際に,WASMではWeb Audio APIAudioWorkletインターフェースを用いて行う (Wasm Audio Worklets API)ときのオプション.
RerrahRerrah

C++側のコードの設定.

main関数は何もしていなくても公開される.モジュール読み込み時に自動的に実行される.

公開される関数はmain関数で使用している関数のみ.つまりmainが存在しないファイルをコンパイルすると,なにも公開されない.

関数を明示的に公開するにはEMSCRIPTEN_KEEPALIVEを関数宣言につけるまたはコンパイル時にsEXPORTED_FUNCTIONSを指定する.
コンパイル時にsEXPORT_ALL=1としてすべて公開するのもあり.

RerrahRerrah

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オプションをつける.

RerrahRerrah

クラスを公開するときはコンストラクタ関数やプロパティ,メソッドを指定できる.

使用するときはモジュールに登録されているコンストラクタ関数を呼び出す形になる.使用後に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();  // 注意!
})();
RerrahRerrah

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
RerrahRerrah

最終コマンド:

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ファイルが作成される.

RerrahRerrah

今日は--network=hostが無くてもコンパイルできた.なぜ...?

RerrahRerrah

makeを使ったプロジェクトでemccを使ってコンパイルする場合は,Emscripten SDKにあるemconfigureとemmakeを使うらしい.make使ってないので試してない...

emconfigure ./configure
emmake make
# 作成されたオブジェクトファイルをリンクする.
# 必要であればオブジェクトファイルをリネームする.
# ライブラリを作成している場合はmain.cもリンク対象に含める.
emcc project.o -o project.js

https://emscripten.org/docs/compiling/Building-Projects.html

RerrahRerrah

上の公式ドキュメントにはcmakeの場合は「"./configure"を"cmake ."に置き換えてね」と書いているが,その通りにするとエラーがでる.

error: use `emcmake` rather then `emconfigure` for cmake projects

emcmakeを使ってcmakeすると動いた.

emcmake cmake .
emmake make
RerrahRerrah

emccの出力ファイル名を.jsではなく.wasmにすると,WASIで動かすことを前提としたWASM (standalone WASM) が生成される.このときmain関数が存在しない状態でコンパイルしたい場合は--no-entryオプションをつける.

RerrahRerrah

--no-entryオプションは通常のコンパイル時でも発生することがあるので注意.以下のエラーが表示される.

error: entry symbol not defined (pass --no-entry to suppress): main
RerrahRerrah

最適化オプション:

  • -Oz: -O3かつファイルサイズも小さくなる最適化オプション.
  • --closure <level>: Closure Compilerを使ってJSファイルやWASMファイルのコード量を少なくする.levelは1にする(-sWASM=0の時は2にする).
  • -flto: リンクの最適化を有効にする.
  • -fno-exceptions: 例外機構のコードを省略する.例外処理を使わない場合に有効.
  • -fno-rtti: RTTIのコードを省略する.dynamic_casttypeidを使わない場合に有効.
  • -sALLOW_MEMORY_GROWTH: mallocなど動的にメモリ割り当てをすることにより,必要なメモリサイズが事前に計算できない場合に設定する.

https://emscripten.org/docs/optimizing/Optimizing-Code.html

-Oz --closure 1 -sFILESYSTEM=0 -fltoくらいが良さそう.

RerrahRerrah

-sENVIRONMENTでは出力するモジュールファイルを動作環境に適した形で出力するための設定を行う.例えばデフォルトだとnodeが有効なため,ダイナミックインポートが有効になるが,webのみにするとこれがなくなる.

web, worker, nodeあたりを設定しておけば良さそう.