C/C++でWebAssembly――「Emscripten」を体験する:いろんな言語で試す、WebAssembly入門(4)
第4回は、WebAssembly開発で人気のあるC/C++とEmscriptenによる開発事例を紹介します。標準的なC/C++の関数の出力をWebページに反映させる事例の他に、C/C++の関数をJavaScriptから呼び出す事例も紹介します。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
C/C++とWebAssembly
連載第2回ではAssemblyScriptによるTypeScript言語、連載第3回ではBlazor WebAssemblyによるC#言語、それぞれの開発事例を紹介しました。スクリプト言語であるTypeScriptでの取り回しの容易さ、フレームワークBlazorによる充実した機能の利用のしやすさをお伝えできたのではないかと思います。
今回は、スクリプト言語やフレームワークはいったん脇に置き、WebAssembly開発の本命の一翼とされるC/C++による開発を紹介します。これには、C/C++においては最も一般的とされる、Emscriptenというコンパイル環境を利用します。Emscriptenによる開発では、標準ライブラリを含めたC/C++の言語仕様を十分に生かしたWebAssemblyプログラムが作成可能です。
C/C++はネイティブコードをターゲットにしたプログラミング言語であるので、その高い効率と高速性を生かした使い方が、真価を発揮しやすいといえます。EmscriptenはAPIを数多く備えており、その一つを使うことでC/C++コードからのWebページの操作なども可能です。ですが連載第1回でも紹介したように、まずは速度が重視される部分をC/C++が受け持つというのが、自然な使われ方でしょう。
本稿では、それを前提にJavaScriptコードの連携まで含めたEmscriptenの使い方を紹介していきます。
Emscriptenとは
Emscriptenとは、C/C++からWebAssemblyへの、LLVMをベースとしたオープンソースのコンパイル環境です。LLVMとは、C言語系、FORTRANなど、さまざまなプログラミング言語に対応できる共通のコンパイラ基盤です。C/C++のコードを.wasm形式に直接コンパイルできる他、実行のためのJavaScriptグルー(.wasmファイルを読み込むためのJavaScriptコード)、HTMLファイルの生成も可能です。基本としてのWebブラウザ上での動作に加えて、Node.jsやWASIランタイム上での動作もサポートされています(本稿ではWebブラウザ上での動作のみ紹介します)。
C/C++のコードをWebAssemblyにコンパイルできることから、UnityやUnreal Engineなどのゲームエンジン、SQLiteなどのデータベースソフトウェア、AutoCADなどのCADソフトウェアなどで広く利用されています。また、APIを数多く備えており、スタンドアロンのC/C++プログラムと似たようにグラフィックスやデバイス、ファイルシステムやネットワークを利用できます。
LLVMをベースとしているので、これをサポートする多くのプラットフォームで利用可能です。macOS(10.14 Mojave以降)、Linux、Windowsとプラットフォームを選ばずに利用できます。
Main - Emscripten 3.1.32-git (dev) documentation
Emscriptenを使う準備
Emscriptenを使うために、以下に挙げるいくつかのソフトウェアを準備しておきます。
- コードエディタとしてのVisual Studio Code(以降、VSCode)
- Python 3.6以降
- Xcode Command Line Tools(macOSのみ)
- EmscriptenのクローンのためのGit
Emscriptenのインストール
必要なソフトウェアを準備できたら、Emscriptenをインストールします。基本的にmacOS環境を想定しますが、必要に応じてWindows環境にも言及します。Emscriptenの実体はEmscripten SDK(emsdk)と呼ばれています。emsdkのインストールは、下記のWebページに書かれている手順で実施します。
Download and install - Emscripten 3.1.32-git (dev) documentation
まず、emsdkのGitHubリポジトリから、適当な場所に以下のコマンドでクローンを実施します。もし、過去にクローン済みである場合には、git pullコマンドにて最新版をプルしてください。
% git clone https://github.com/emscripten-core/emsdk.git
emsdkをクローンできたら、emsdkフォルダに移動し、./emsdk install latestコマンドで最新版をインストール、./emsdk activate latestコマンドで最新版をアクティブにします。Windows環境では、カレントフォルダを表す「./」は不要です。
% ./emsdk install latest % ./emsdk activate latest
最後に、PATHをはじめとする環境変数などのセットアップをsourceコマンドで実施すれば完了です。Windows環境では、emsdk_env.batファイルを実行します。
% source ./emsdk_env.sh
emsdkのインストールによってem++コマンドが利用可能になります。-vオプションを与えてコマンドを実行して、以下のように出力されればemsdkのインストールは成功です。
% em++ -v emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.31 (e88336121cfe6da4a96c88e46f314552f07dfed0) …中略… InstalledDir: /Users/nao/Documents/atmarkit_webassembly/sample/emsdk/upstream/bin
プログラムの作成と実行
簡単なサンプルを作成してEmscriptenの動作を概観しましょう。まずは、サンプルのためのフォルダを作成します。本連載のためのatmarkit_wasmフォルダの中に、さらにcpp_emscriptenフォルダを作成します。以降、このフォルダを基点に操作します。
プログラムの作成
以下のような内容のC++ソースファイルhello.cppを作成します。これは、C++の入門にもよく使われる、"Hello, World!"と改行を標準出力に書き出す典型的なコードです。
#include <iostream> using namespace std; int main(){ cout << "Hello, World!" << endl; return 0; }
プログラムのコンパイル
早速Emscriptenでコンパイルしてみます。コンパイルは、先ほどのem++コマンドを使用します。
% em++ hello.cpp -o hello.html
指定されているオプション「-o hello.html」は、.wasm実行のためのJavaScriptグルーファイルとHTMLファイルも出力するためのオプションです。-o hello.htmlオプションを指定したem++コマンドの実行後には、以下のファイルが生成されます。
ファイル | 概要 |
---|---|
hello.wasm | コンパイルされたWebAssemblyプログラム |
hello.js | WebAssemblyプログラムを読み込むJavaScriptグルーファイル |
hello.html | WebAssemblyプログラムを使うHTMLファイル |
表 em++で作成される主なファイル |
「hello」の部分は、-oオプションで指定したファイル名と共通となります。なお、このとき-oオプションにhello.jsを指定するとhello.wasmとhello.jsが、hello.wasmを指定するとhello.wasmのみが作成されます。HTMLファイルに手を入れたので作成を省いてほしい場合などには、「-o hello.js」と指定すればHTMLファイルの生成はスキップされます。
プログラムの実行
上記の通り、コンパイルによってWebAssemblyプログラムに加えてJavaScriptグルーファイルやHTMLファイルまで生成されます。これらのファイルをWebサーバに設置し、HTMLファイルをWebブラウザによって開くことで、WebAssemblyプログラムが実行できます。このとき、Emscriptenには簡易的なWebサーバの機能がemrunコマンドとして用意されていますので、開発用のWebサーバを構築したり、VSCodeの拡張機能(Live Serverなど)のインストールが不要になっています。
emrunコマンドを、以下のように引数にHTMLファイルを指定して実行します。すると、デフォルトのWebブラウザにHTMLファイルが読み込まれ、最終的にWebAssemblyプログラムが読み込まれ、実行されます。図1のように、下側の黒いボックスに「Hello, World!」が表示されれば成功です。
% emrun hello.html
これで、C++のソースコードがWebAssemblyにコンパイルされ、実行できることを確認できました。
なお、上の画面の「Hello, World!」が表示された黒いボックスはコンソールと呼ばれるテキストエリアコントロール(<textarea>)です。Emscriptenの既定では、標準出力への書き出しは<textarea>に反映されることになっています。
また、Emscriptenのロゴの右にチェックボックスが2つ、ボタンが1つ、そして「Hello, World!」が表示されたものとは別に、黒いボックスが配置されています。このボックスはキャンバス(<canvas>)であり、本稿では取り上げませんがWebAssemblyプログラムからのグラフィックス描画に利用できます。
ファイルを確認する
ここで、コンパイルによって出力されたファイルの内容を確認してみましょう。出力されたのはWebAssemblyプログラムであるhello.wasm、JavaScriptグルーであるhello.js、そして表示のためのhello.htmlのみです。このうち、hello.htmlファイルの内容を確認してみます。
<!doctype html> <html lang="en-us"> <head> …スタイルの定義などなので省略… </head> <body> <a href="http://emscripten.org"> <svg …ロゴのSVGデータなので省略… </svg> </a> <div class="spinner" id='spinner'></div> <div class="emscripten" id="status">Downloading...</div> <span id='controls'> <span><input type="checkbox" id="resize">Resize canvas</span> <span><input type="checkbox" id="pointerLock" checked>Lock/hide mouse pointer </span> <span><input type="button" value="Fullscreen" onclick="Module.requestFullscreen(document.getElementById('pointerLock').checked, document.getElementById('resize').checked)"> </span> </span> <div class="emscripten"> <progress value="0" max="100" id="progress" hidden=1></progress> </div> <div class="emscripten_border"> <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas> (1) </div> <textarea id="output" rows="8"></textarea> (2) <script type='text/javascript'> …JavaScriptコードは省略… </script> <script async type="text/javascript" src="hello.js"></script> (3) </body> </html>
(1)は、図形の描画に使えるキャンバスの指定です。class属性にemscripten、id属性にcanvasが付与された<canvas>要素が既定のキャンバスになります。
(2)は、テキストの出力に使えるテキストエリアコントロールの指定です。class属性にemscripten、id属性にoutputが付与された<textarea>要素が既定のテキストエリアコントロールになります。
(3)は、WebAssemblyファイルの読み込みと実行のためのJavaScriptグルーファイルを読み込むための指定です。
JavaScriptからC++の関数を呼び出す
前節のサンプルは、C++コードの実行による出力を<textarea>要素に書き込むというものでした。Emscriptenの既定の動作としてC++コード中のmain関数が呼び出されたので、そのような動作になったのです。しかし実際の使われ方としては、C++で速度やメモリ効率が重視される関数を書いておき、それをJavaScriptから呼び出すといったものになるでしょう。ここでは、C++コード中の関数をJavaScriptから呼び出してみます。
フォルダとファイルを用意する
ここまで作業してきたcpp_emscriptenフォルダに、新たにexportフォルダを作成して、そこにhello.cppファイルをコピーします。以降は、このexportフォルダを基点に作業します。
コピーしたhello.cppファイルに、以下のように(1)(2)(3)を追記します。
#include <iostream> #include <emscripten/emscripten.h> (1) using namespace std; int main(){ cout << "Hello, World!" << endl; return 0; } #ifdef __cplusplus (2) #define EXTERN extern "C" #else #define EXTERN #endif EXTERN EMSCRIPTEN_KEEPALIVE int calcSum(int num1, int num2) { (3) return num1 + num2; }
前節のhello.cppのコードには、Emscriptenらしい記述はどこにもなかったのですが、ここではEmscriptenのルールに従った記述が幾つか見られます。それらを見てみましょう。
(1)は、Emscriptenのためのヘッダファイルをインクルードするプリプロセッサ文です。SDKが正しくインストールされて環境変数が適切に設定されていれば、SDKにあるヘッダファイルがここでインクルードされます。(3)のEMSCRIPTEN_KEEPALIVEマクロを使うために必要なインクルードです。
(2)は、JavaScriptから利用する名前をCとC++でそろえる指定です。この場合はC++であることが明らかなので#ifdefプリプロセッサ文は不要ですが、定型的な文として常に使うことをおすすめします。これにより、エクスポートする関数がCで記述されていようがC++で記述されていようが、同じ名前となります。
(3)は、JavaScriptから呼び出す関数calcSumの定義です。先頭に(2)でEXTERNマクロが指定されていますが、(2)の定義の通りC++の場合のみ「extern "C"」が付与されます。続くEMSCRIPTEN_KEEPALIVEマクロが重要で、役割としては定義した関数を外部から利用できるようにすることです。Emscriptenでは、main関数に関与しないコードは自動的に削除されてしまいますが、外部から呼び出すために削除されないようにするのが、EMSCRIPTEN_KEEPALIVEマクロです。calcSum関数の処理としては、2つの整数型の引数を受け取って、その和を返すだけのシンプルなものです。
コンパイルする
ここでhello.cppをコンパイルして、ファイル一式を生成します。
% em++ -o hello.html hello.cpp -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']"
2つの新しいオプションがあります。
「-s NO_EXIT_RUNTIME=1」は、main関数が呼び出されて実行が終了しても、ランタイムルーチン自体は終了させずに残しておくためのオプションです。これがないと、calcSum関数を呼び出せなくなってしまいます。
「-s "EXPORTED_RUNTIME_METHODS=['ccall']"」は、エクスポートする関数名のリストを指定するオプションです。この場合は、ccallのみをエクスポートする指定になっています。ccall関数は、EMSCRIPTEN_KEEPALIVEマクロが付与された関数を、JavaScriptから呼び出すためのEmscriptenの関数です。
ここでemrunコマンドを実行すれば、既定通りの「Hello, World!」が出力される動作となります。
【補足】-sオプション
-sで始まるオプション(settingsオプション)はたくさんあります。詳細は、emsdk/upstream/emscripten/srcフォルダにあるsettings.jsファイルで確認できます(英語です)。
HTMLファイルを修正する
HTMLファイルにボタンコントロールを設置し、ボタンクリックでC++コード中の関数を呼び出すようにしてみます。まず、hello.htmlファイルに以下のように<button>要素を追加します。追加場所は、<textarea>要素の直後です(★の箇所)。
…略… <textarea id="output" rows="8"></textarea> <button id="button">計算!</button> ★ <script type='text/javascript'> var statusElement = document.getElementById('status'); …略…
ボタンクリック時に呼び出すイベントハンドラを追加します。追加場所は、最後の<script>要素の次です(★から★まで)。
…略… <script async type="text/javascript" src="hello.js"></script> <script> ★ document.getElementById("button").addEventListener("click", () => { const num1 = 100, num2 = 200; const result = Module.ccall( // ccall関数の呼び出し "calcSum", // C++の関数名 "number", // 戻り値の型 ["number", "number"], // 引数の型の配列 [num1, num2] // 実引数の配列 ); alert(`${num1} + ${num2} = ${result}`); }); </script> ★ </body> </html>
emrunコマンドを実行すると、テキストエリアコントロールの下にボタンコントロールが現れます。クリックすると、図2のようにalert関数によって計算結果がポップアップ表示されます。
このようにEmscriptenでは、C++の関数を直接呼び出すのではなく、EmscriptenのAPIであるccall関数を使って間接的に呼び出します。上のリストのコメントを見ると分かるように、C++の関数名、戻り値の型、引数の型の配列、実引数の配列を渡して呼び出すと、呼び出した関数の戻り値を返してくれます。
戻り値や引数の型に指定する文字列は、JavaScriptのデータ型の名称です。以下の表に、対応を示します(これ以外のものが指定されると"number"として扱われる)。
型表記 | JavaScriptの型 | C/C++の型 |
---|---|---|
"number" | Number | 整数、浮動小数点数、汎用(はんよう)のポインタ |
"boolean" | Boolean | 論理値 |
"string" | String | 文字列を表すchar * |
"array" | Array | 配列(Uint8ArrayまたはInt8Array) |
表 ccall関数とcwrap関数に渡す型表記 |
なお、ccall関数と同じ目的のcwrap関数もあります。ccall関数が関数を直接呼び出して結果を返すのに対して、cwrap関数は関数オブジェクトを返す点が異なります。cwrap関数では、関数を呼び出す場合には関数オブジェクトに引数を与えて呼び出すだけでよいので、何度も同じ関数を呼び出す場合に便利です。
const calc = Module.cwrap( "calcSum", "number", ["number", "number"], [num1, num2] ); const sum = calc(100, 200);
まとめ
今回は、Emscriptenを使用したC/C++によるWebAssemblyプログラムの開発を紹介しました。
次回は、同じくネイティブコンパイラ言語であるRustを用いたWebAssemblyプログラムの開発を紹介します。
筆者紹介
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.