背景
Python のモジュールや Blender の addon(プラグイン)など, クロスプラットホームで C++ で書きたい(すごい性能が必要というわけではないが, C/C++ が開発がラクなときとか).
ただ, 作った module や plugin を配布して使ってもらうにしても, 各プラットフォームにプレビルド用意したりと C/C++ のビルドがめんどい.
(Windows で pip でソースからビルドさせる場合, ユーザーの環境に Visual Studio とか入れてもらう必要がありめんどい)
python だと cibuildwheel で pypi 向けにビルドするのもあるけど, 設定がめんどい
plugin を WASM にし, クロスプラットホームな plugin として扱えないか?
メモリが確保できないなど(C++ STL で new とかしている(e.g. vector.resize()
とかは全滅), 結構制約あることがわかりましたが, 一応動かせはします.
より汎用に C/C++ コードを動かしたいときは, wasi-sdk 利用を検討ください.
wasi-sdk + wasmer-python で C/C++ アプリを WASM でポータブルに動かすメモ
https://qiita.com/syoyo/items/300d4ef7fd105e69ad10
方法
wasmer-python がありました.
ありがとうございます
C/C++ コード
emcc でコンパイルします.
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#else
#define EMSCRIPTEN_KEEPALIVE
#endif
#ifdef __cplusplus
extern "C"
#endif
float EMSCRIPTEN_KEEPALIVE func(float a, float b) {
return a + 2.0f * b;
}
#ifdef __cplusplus
}
#endif
$ emcc test.c
a.out.wasm が出来上がります.
実行
wasmer-python のサンプルを参考にして動かします.
# main.py
from wasmer import engine, Store, Module, Instance
from wasmer_compiler_cranelift import Compiler
# Let's define the store, that holds the engine, that holds the compiler.
store = Store(engine.JIT(Compiler))
# Let's compile the module to be able to execute it!
module = Module(store, open('a.out.wasm', 'rb').read())
# Now the module is compiled, we can instantiate it.
instance = Instance(module)
result = instance.exports.func(5.0, 3.0)
print(result)
$ python main.py
11.0
Voila!
画像データなどポインタを渡す場合
wasmer-python にまともなドキュメントやサンプルコードがなくてつらい
python 側に Float32Array など用意されていますが, これをそのまま使うのはできないです...
(これらは主にPython 側で, メモリ上のデータを Float32Array で扱いたいときなど用)
メモリモデル
64KB 単位のページでメモリ確保されます.
instance.exports.memory
では, WASM 側のデフォルト?メモリ領域にアクセスできます. デフォルトでは 256 pages です.
wasmer-python では, 基本メモリアドレス(ページのオフセット)を渡すことでやりとります.
組み込み C/C++ みたいな感じですね.
Wasmer
wasm 側(C/C++側)で malloc/new で動的メモリ確保というのもできません(シンボルエラーになる. すごい頑張ればできるっぽいようだが... これは JS で動かすときもカナ )
メモリの受け渡し
instance.exports.memory
でないとダメっぽいようです.
なぜか Memory
とか Float32Array
(の view) からオフセット(byteOffset
, offset
)は取得できません ので, オフセットは必要に応じて何かしら記憶しておく必要があります.
# main.py
from wasmer import engine, Store, Memory, MemoryType, Module, Instance, Float32Array
from wasmer_compiler_cranelift import Compiler
# Let's define the store, that holds the engine, that holds the compiler.
store = Store(engine.JIT(Compiler))
# Let's compile the module to be able to execute it!
module = Module(store, open('a.out.wasm', 'rb').read())
print(module)
# Now the module is compiled, we can instantiate it.
instance = Instance(module)
m = instance.exports.memory
print(m.size)
m.grow(2)
print(m.size)
f32_view = m.float32_view(0)
f32_view[0] = 1.0
f32_view[1] = 2.0
f32_view[2] = 3.3
f32_view[3] = 4.4
print(f32_view)
#instance.exports.func
result = instance.exports.func(0, 4)
print(result)
#include <vector>
#include <string>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#else
#define EMSCRIPTEN_KEEPALIVE
#endif
#ifdef __cplusplus
extern "C" {
#endif
float EMSCRIPTEN_KEEPALIVE func(const float *a, size_t n) { //, float *b) {
float sum = 0.0f;
for (size_t i = 0; i < n; i++) {
sum += a[i];
}
return sum;
}
#ifdef __cplusplus
}
10.7 と出るはずです!(正確には丸め誤差とか出て 10.700000762939453)
offset
m.float32_view
とかでは, 実際のメモリアドレスは要素のバイト数倍になります(e.g. float32_view(1)
だと4バイト進む)
export.func
などの WASM とのインターフェースでは, ポインタのオフセット値は常に byte
になります.
C++ 例外
使えません
-fno-exceptions
-fno-rtti
でコンパイルしましょう.
WASM での例外の仕組みがあるようですが, wasmer(WASI) ではまだ対応していないかも?
malloc/new とか
↑で例外 off にしても, abort
シンボルが見つからないエラーが出ます.
host(実行側)で用意しないとダメです.
しかし, それでも emscripten_memcpy_big
などが missing になるので, 自前で python 側で実装するしかないかもですが... めんどうなので実質使えないと思いましょう.
その他
embind は扱えません
まとめ
wasmer などは WASI(サンドボックス環境で動かすのを想定しており, システム系関数などはあまり提供されていない)な WASM を対象としていました.
emscripten は JavaScript をターゲットとしており, JavaScript 側でのバインディング(エミュレーション)は提供されますが, WASI 系のサポートは行っていませんでした.
emscripten 利用はかなり制約があるのがわかりましたので, 任意な C/C++ が動くというのは考えないほうがいいです.
ちょっとした画像フィルタ(メモリ確保などしない)を, C/C++ ではぱぱっとかけるが Python で書くのが面倒なときに使うくらいでしょうか...
より一般的な C/C++ コードを動かすばあい, wasi-sdk で C/C++ コンパイルするのがいいかもしれません.
TODO
- wasi-sdk(or clang + wasi-libc?) でのコンパイルを試す.