はじめに
昨日の私の記事では、Qt for WebAssemblyを使ったGUIアプリの開発方法を紹介しました。今日の記事ではQtは一切関係ありません。C++とJavaScriptとの間で、関数を呼び合ったりデータを受け渡しする方法を紹介します。
Ubuntu 18で開発する想定です。昨日の記事を参考にして、開発環境をインストールしてください。Qtは必要ありませんが、統合開発環境のQt Creatorを使うと、C++開発に最適化されたテキストエディタの他、シンボル名補完や、宣言と定義の間のジャンプ、リファクタリング機能も利用できますので、あると便利です。もちろん使い慣れたテキストエディタで構いません。
最初のアプリ
基本的な手順は、Mozillaのサイトに全部書いてあります。
昨日の記事の手順に従って、環境変数を定義し、Emscriptenの動作確認をしておきます。
source ~/emsdk/emsdk_env.sh
em++ -v
適当な作業用ディレクトリを作って、最初のソースファイルを作成します。
#include <stdio.h>
int main()
{
printf("Hello, world\n");
return 0;
}
ビルドします。
emcc hello.c -s WASM=1 -o hello.html
上の例ではC言語のソースをコンパイルしました。C++であれば次のようにします。
em++ hello.cpp -s WASM=1 -o hello.html
成功したら3つのファイルが生成されます。それらをウェブサイトのディレクトリにコピーします。
cp hello.js /var/www/html/wasmtest/
cp hello.html /var/www/html/wasmtest/
cp hello.wasm /var/www/html/wasmtest/
ウェブブラウザでF12キーを押して、開発者機能を開き、コンソール出力を表示したら、 http://localhost/wasmtest/hello.html にアクセスします。コンソールに「Hello, world」が表示されたら成功です。
HTMLの改造
hello.html を index.html にコピーして、これを編集します。WebAssemblyモジュールをロードする以外のヴィジュアル要素を全て取り除きます。
<!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>
</body>
</html>
C++側ソースを編集
C/C++によるWebAssemblyのプログラムは、初めの方でお決まりのヘッダファイルをインクルードします。
#include <emscripten.h>
C言語のまま進めても問題ありませんが、将来のためC++にしておきます。
mv hello.c hello.cpp
コンパイルするには、前述の手順のとおり、emcc
の代わりにem++
コマンドを使用します。
em++ hello.cpp -o hello.js
これを実行するとhello.js
とhello.wasm
が生成されますので、ウェブサイトのディレクトリにコピーします。コンパイルしてインストールするためのスクリプトを書いておくと便利です。
#!/bin/sh
em++ hello.cpp -o hello.js
cp hello.js /var/www/html/wasmtest/
cp hello.wasm /var/www/html/wasmtest/
JavaScriptからC/C++の関数を呼び出す方法
C言語の関数として定義します。C++の場合はextern "C"
が必要です。また、EMSCRIPTEN_KEEPALIVE
というのを付けておきます。ここでは例として2つの関数を用意します。
extern "C" void EMSCRIPTEN_KEEPALIVE initialized()
{
puts("initialized");
}
extern "C" void EMSCRIPTEN_KEEPALIVE clicked1()
{
puts("clicked1");
}
JavaScriptからは'_
'をつけた関数名で呼び出しします。
Module._initialized();
または、単純に以下のように書いても動作します。
_initialized();
これを踏まえて、HTMLの最後の方に、次のように書いておきます。
<script>
Module.onRuntimeInitialized = function(){
_initialized();
}
</script>
<button onclick="_clicked1()">OK</button>
モジュールの初期化が終わるまで待って initialized() 関数を呼んでいます。また、ボタンが押されたら clicked1() 関数を呼んでいます。
これを実行すると、モジュールの初期化が完了したら initialized と表示され、次に main() 関数が実行されて Hello, world が表示されます、ボタンが押される度に clicked1() が呼ばれていることも確認できます。
HTML要素の値を取得する
例えば、次のようなHTMLがあるとします。
<input id="input1" value="hogehoge">
C++から次のような感じにvalue
の値を取得したいです。
std::string value = getElementValue("input1");
これをやるためにgetElementValue
関数を作ってみます。
#include <string>
//(略)
EM_JS(void *, getElementValue_, (char const *id),
{
var e = document.getElementById(UTF8ToString(id));
var str = e.value;
var len = lengthBytesUTF8(str) + 1;
var heap = _malloc(len);
stringToUTF8(str, heap, len);
return heap;
});
std::string getElementValue(std::string const &id)
{
void *p = getElementValue_(id.c_str());
std::string s((char const *)p);
free(p);
return s;
}
C++のソースの中にJavaScriptを埋め込むために、EM_JS
というキーワードを使っています。EM_JS(戻り型, 関数名, 仮引数, { スクリプトコード });
の様に定義します。JavaScriptからC++にデータを返すために、malloc
関数でヒープからメモリを確保しています。それを呼び出す側のC++プログラムで、文字列オブジェクトを作成したら、free
関数でメモリを開放しています。
HTML要素の値を設定する
前項とは逆に、値の設定をします。C++中にインラインでJavaScriptを記述するためにEM_ASM
というキーワードを使っています。これに渡した引数は、$0
、$1
の様にしてJavaScriptから使用できます。UTF-8文字列をJavaScriptの文字列に変換するためにUTF8ToString()
という関数を使っているところがポイントです。
void setElementValue(std::string const &id, std::string const &value)
{
EM_ASM({
var e = document.getElementById(UTF8ToString($0));
e.value = UTF8ToString($1);
}, id.c_str(), value.c_str());
}
次のようにすると、ボタンを押すとinput1要素の値が書き換わります。
extern "C" void EMSCRIPTEN_KEEPALIVE clicked1()
{
setElementValue("input1", "Hello, world");
}
HTML要素の内部HTMLに書き込む
次のようなタグがあるとします。
<div id="contents"></div>
このdiv
タグのinnerHTML
に書き込みます。C++中にインラインでJavaScriptを埋め込み、要素のinnerHTML
を変更しています。
void setElementInnerHTML(std::string const &id, std::string const &html)
{
EM_ASM({
var e = document.getElementById(UTF8ToString($0));
e.innerHTML = UTF8ToString($1);
}, id.c_str(), html.c_str());
}
次のようにすると、ボタンを押すとcontents要素内のHTMLが書き換わります。
extern "C" void EMSCRIPTEN_KEEPALIVE clicked1()
{
setElementInnerHTML("contents", "<p>Hello, world</p>");
}