LoginSignup
20
20

More than 3 years have passed since last update.

WebAssemblyでC++とJavaScript間のやり取り

Last updated at Posted at 2019-12-03

はじめに

昨日の私の記事では、Qt for WebAssemblyを使ったGUIアプリの開発方法を紹介しました。今日の記事ではQtは一切関係ありません。C++とJavaScriptとの間で、関数を呼び合ったりデータを受け渡しする方法を紹介します。

Ubuntu 18で開発する想定です。昨日の記事を参考にして、開発環境をインストールしてください。Qtは必要ありませんが、統合開発環境のQt Creatorを使うと、C++開発に最適化されたテキストエディタの他、シンボル名補完や、宣言と定義の間のジャンプ、リファクタリング機能も利用できますので、あると便利です。もちろん使い慣れたテキストエディタで構いません。

最初のアプリ

基本的な手順は、Mozillaのサイトに全部書いてあります。

昨日の記事の手順に従って、環境変数を定義し、Emscriptenの動作確認をしておきます。

source ~/emsdk/emsdk_env.sh
em++ -v

適当な作業用ディレクトリを作って、最初のソースファイルを作成します。

hello.c
#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」が表示されたら成功です。

image.png

HTMLの改造

hello.html を index.html にコピーして、これを編集します。WebAssemblyモジュールをロードする以外のヴィジュアル要素を全て取り除きます。

index.html
<!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.jshello.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の最後の方に、次のように書いておきます。

index.html
    <script>
        Module.onRuntimeInitialized = function(){
          _initialized();
        }
    </script>
    <button onclick="_clicked1()">OK</button>

モジュールの初期化が終わるまで待って initialized() 関数を呼んでいます。また、ボタンが押されたら clicked1() 関数を呼んでいます。

これを実行すると、モジュールの初期化が完了したら initialized と表示され、次に main() 関数が実行されて Hello, world が表示されます、ボタンが押される度に clicked1() が呼ばれていることも確認できます。

image.png

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>");
}
20
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
20