中国の動画配信サービス「ビリビリ(bilibili)」から動画をダウンロードしたつもりが、
wasm(ワスム)
という拡張子のバイナリファイルが落ちてきてVLCで再生できなかった。
なんだこれ?
ググってみると、
「WASM(ワスム)」は、Chrome、Mozilla Firefox、Microsoft Edge、SafariなどのWebブラウザーで実行されるWebAssembly形式で保存されたバイナリコードが含まれている。
らしい。
WebAssembly(ウェブ アセンブリ)
って大層な名前だね。
C/C++がで書いたコードがコンパイルできてHTMLやJavaScriptを利用してWeb上で動くの?
と思ってググると、そのとおりだった。
C、C++、またはRust、Golang、TypeScriptからコンパイルされたバイナリWebAssemblyコードであり、WebページやアプリにインポートしてJavaScript経由で使用できます。
世の中の進化スゲーーー。
ようするにWebAssemblyとはプログラミング言語やライブラリの名前ではなく、ブラウザでプログラムを高速実行するための、
「ブラウザ上で動くバイナリコードの新しいフォーマット(仕様)」
である。
何も(知ら)ないから何か見つかる。
出会いはいつも突然に。
ちょっと使ってみよう。
C/C++によるWebAssemblyのhello world
ググってみると日本語の記事は多くない。
【私の環境】
- Windows10(CPU:Intel Core i5 2.40GHz、RAM:16.0GB、HDD 250GB)
- gnupack_devel-13.05-2015.07.19 (cygwin)
インストール
Cygwin上で、マニュアルに従ってコマンドを打つだけ。
1 2 3 4 |
git clone https://github.com/emscripten-core/emsdk.git # Enter that directory cd emsdk |
とすれば……格好良いけど git をインストールしてないので、サイトからZIPで落としてくる。
インストール&有効化コマンドは次のとおり。
1 2 3 4 5 |
git clone https://github.com/emscripten-core/emsdk.git cd emsdk-main ./emsdk install latest ./emsdk activate latest cd .. |
実装&起動
ソースコードは次のようになる。
1 2 3 4 5 6 |
#include <stdio.h> int main() { printf("Hello World\n"); return 0; } |
コンパイル方法は少し特殊。
1 2 |
# コンパイル方法 emsdk-main/upstream/emscripten/em++ hello.cpp -s WASM=1 -o hello.html |
「-s WASM=1」がコンパイル後にwasmファイルを出力するオプションだ。
なおC言語のソースをコンパイルには「emcc」を使う。
これでローカルに3つのファイル「hello.html」「hello.js」「hello.wasm」ができる。
あとは WebAssembly に対応しているブラウザーで hello.html を読み込むだけ。
1 2 |
# 実行方法 /C/Python311/python.exe emsdk-main/upstream/emscripten/emrun.py hello.html |
ローカルの場合には「localhost」にブラウザが起動して画面表示される。
実際のサーバ上で確認する場合には前述の3つのファイルをUploadすれば動作する。
超簡単だった。
なお、既定で有効なのは Firefox 52、Chrome 57、Opera 44 以降らしい。
「Chrome 57」は、2017年3月29日にリリースされているので6年前からある技術だったのか……。
オラーリの日本語書籍は半年前に発売なので、時期的には丁度よいのかも。
C/C++によるWebAssemblyをJava Scriptから呼び出す
ググってみると、さらに入門編の記事は少ない。
とりあえず記事に従って進めてみよう。
HTMLの修正
まず hello.html を index.html にコピーして編集する。
1200行以上ある WebAssemblyモジュールをロードする以外のヴィジュアル要素を全て取り除く。素敵!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<!doctype html> <html lang="en-us"> <head> <meta charset="utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>wasmtest</title> </head> <body> <script type='text/javascript'> var Module = { preRun: [], postRun: [], print: (function() { return function(text) { if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); console.log(text); }; })(), printErr: function(text) { if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); //console.log(text); }, totalDependencies: 0, monitorRunDependencies: function(left) { this.totalDependencies = Math.max(this.totalDependencies, left); } }; console.log('Downloading...'); window.onerror = function(event) { // TODO: do not warn on ok events like simulating an infinite loop or exitStatus }; </script> <script type="text/javascript" src="hello.js"></script> <script> Module.onRuntimeInitialized = function(){ _initialized(); } </script> <button onclick="_clicked1()">OK</button> <input id="input1" value="hogehoge"> <div id="contents"></div> </body> </html> |
とりあえず「button」「input」「div」それぞれ入れてみた。
C++コードの修正
サンプルのソースコードは断片的に記載があったので、何度もエラーに悩まされたけどコード全体としては次のようになった。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
#include <emscripten.h> #include <string> #include <stdio.h> // EM_JS(戻り型, 関数名, 仮引数, { スクリプトコード }); EM_JS(void *, getElementValue_, (char const *id), { console.log('called getElementValue_...'); var e = document.getElementById(UTF8ToString(id)); var str = e.value; var len = lengthBytesUTF8(str) + 1; // コンパイル時に -s EXPORTED_FUNCTIONS="['_malloc']" しないとエラーがでる var heap = _malloc(len); stringToUTF8(str, heap, len); return heap; }); // C++から次のようにvalueの値を取得 std::string getElementValue(std::string const &id) { void *p = getElementValue_(id.c_str()); std::string s((char const *)p); free(p); return s; } void setElementValue(std::string const &id, std::string const &value) { // C/C++ のコード内で JavaScript を記述 EM_ASM({ console.log('called setElementValue...'); var e = document.getElementById(UTF8ToString($0)); // UTF-8文字列をJavaScriptの文字列に変換 e.value = UTF8ToString($1); }, id.c_str(), value.c_str()); } void setElementInnerHTML(std::string const &id, std::string const &html) { // C/C++ のコード内で JavaScript を記述 EM_ASM({ console.log('called setElementInnerHTML...'); var e = document.getElementById(UTF8ToString($0)); e.innerHTML = UTF8ToString($1); }, id.c_str(), html.c_str()); } // モジュールの初期化が終わるまで待って呼びだす extern "C" void EMSCRIPTEN_KEEPALIVE initialized() { puts("initialized"); } // ボタンが押された extern "C" void EMSCRIPTEN_KEEPALIVE clicked1() { puts("clicked1"); setElementInnerHTML("contents", "<p style='color:red'>Hello, world</p>"); std::string value = getElementValue("input1"); puts(value.c_str()); } |
結局「_malloc」を使うときに「-s EXPORTED_FUNCTIONS=”[‘_malloc’]”」がビルドオプションに無かったのが問題だった。
1 2 |
# コンパイル方法 emsdk-main/upstream/emscripten/em++ -s EXPORTED_FUNCTIONS="['_malloc']" hello.cpp -s WASM=1 -o hello.html |
動作画面は寂しいけど、ブラウザ上からC++のコードを呼び出しできている。
何か嬉しい。
デバッグ方法(不具合解析)
コンパイル済のバイナリファイルなので、どこで死んだのか分からない。
そして毎回ログ入れてコンパイルし直すなんて時間の無駄だよ。
そんな時は「-g」オプションが使えるようだ。
1 2 |
# コンパイル方法 emsdk-main/upstream/emscripten/em++ -g hello.cpp -s WASM=1 -o hello.html |
で「hello.wasm」だけ差し替える。
Chromeの場合は「︙」→「その他のツール」→「デベロッパーツール」→「Console」を確認する。
【-gオプション無し(17KB)】
【-gオプションあり(5220KB)】
Call stackが関数名で表示されるようになった。
おわりに
確かに動いた。
技術の進化は凄まじい。
Java Scriptって難読化はできるけどソースコード見えちゃうし、PHPと連携させるとサーバ負荷や処理速度が遅いからWebサービス作るのは以前諦めた。
数十年前に出会っていたら色々と実装してた……かもしれない。