はじめに
最近WebAssemblyがあついですね。
というわけでC言語からWebAssemblyを作成してHello WebAssembly
してみようと思います。
コンパイラにはWebAssembly界隈で広く使われているEmscriptenを使用します。
本記事では、以下のステップで進めます。
使用する環境はWindows10となっています。
- 1. 環境構築-Emscriptenのインストール
- 2. C言語でサンプルプログラムを実装
- 3. C言語からWebAssemblyへコンパイル
- 4. WebAssemblyをブラウザで実行
- 5. デベロッパーツールでWebAssemblyの中身を見る
なお、以下の公式ページでもC言語からWebAssemblyを動かす手順が示されています。
1. 環境構築-Emscriptenのインストール
はじめにEmscriptenをインストールします。
Emscriptenは以下のページからダウンロードします。
パッケージマネージャーを利用したインストール方法が書かれていますが、gitレポジトリからクローンするだけでも使えるようになります。
今回は以下のgitレポジトリをクローンしたものを使用します。
クローンしたのち、こちらに記載の通り、クローンしたフォルダ内で以下のコマンドを実行します。
emsdk install latest
emsdk activate latest
emsdk_env.bat
特にエラーが出なければインストールは完了です。
2. C言語でサンプルプログラムを実装
続いてサンプルプログラムを作成します。
echo.c
ファイルを作成し、下記のように実装します。
なお、本記事ではブラウザでWebAssemblyを実行するまでの流れに注目していますので、プログラムの詳細は省きます。
処理内容としては、引数で与えられた文字列に対してHello, <文字列> !
と表示するだけの簡単なものになっています。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char* echo(char* ptr){
int n;
char* str;
n = 10 + strlen(ptr);
str = (char*)malloc(n*sizeof(char));
memset(str, '\0', (n*sizeof(char)));
strcpy(str, "Hello, ");
strcat(str, ptr);
strcat(str, " !");
return str;
}
int main(int argc, char **argv) {
char* ptr = echo("WebAssembly");
printf("%s", ptr);
free(ptr);
return 0;
}
3. C言語からWebAssemblyへコンパイル
Emscriptenを利用する場合、emcc
コマンドでコンパイルします。
パッケージマネージャを利用せずにインストールした場合、emcc
コマンドを実行するプロンプトでemsdk_env.bat
を実行します。
Linuxの場合はemsdk_env.sh
となります。
以下はemsdk_env.bat
実行時の出力です。
D:\webassembly>d:\Tools\emsdk\emsdk_env.bat
Setting up EMSDK environment (suppress these messages with EMSDK_QUIET=1)
Adding directories to PATH:
PATH += D:\Tools\emsdk
PATH += D:\Tools\emsdk\upstream\emscripten
PATH += D:\Tools\emsdk\node\14.18.2_64bit\bin
Setting environment variables:
PATH = D:\Tools\emsdk;D:\Tools\emsdk\upstream\emscripten;D:\Tools\emsdk\node\14.18.2_64bit\bin;<以下略>
EMSDK = D:/Tools/emsdk
EM_CONFIG = D:\Tools\emsdk\.emscripten
EMSDK_NODE = D:\Tools\emsdk\node\14.18.2_64bit\bin\node.exe
EMSDK_PYTHON = D:\Tools\emsdk\python\3.9.2-nuget_64bit\python.exe
JAVA_HOME = D:\Tools\emsdk\java\8.152_64bit
Clearing existing environment variable: EMSDK_PY
それではここで、emcc
コマンドを用いでC言語のプログラムをWebAssemblyにコンパイルしてみます。
コマンドは以下の通りです。
D:\webassembly>emcc echo.c -o echo.html -sWASM=1 -sEXIT_RUNTIME=1
これにより以下のファイルが出力されます。
-
echo.html
:emscriptenが出力するサンプルhtml -
echo.js
:html内からWebAssemblyを呼び出すためのjavascript -
echo.wasm
:WebAssemblyの実行ファイル
参考までに、emcc
コマンドでよく使いそうなオプション引数等を(参考) emccコマンドのオプションについてを示します。
読み込み時にmain関数を実行するかしないか、などもオプション引数でコントロールできます。
4. WebAssemblyをブラウザで実行
作成されたecho.html
を使ってWebAssemblyを実行してみます。
ただし、単純にecho.html
を開くだけではwasmファイルは実行されません。これはセキュリティ上の理由からで、ブラウザのCORSポリシーによりjavascriptやwebassemblyのファイルが読み込まれないためです。
ブラウザのCORSを無効にすることもできますが、ここでは真っ当にWebサーバを起動することにします。
Pyhtonを使うのであれば以下のコマンドで簡易的なhttpサーバーは起動できます。
python -m http.server 8000
起動したWebサーバへアクセスすると以下のようにコンパイルされた資材が確認できるかと思います。
ここでecho.html
を選択すると、プログラムのmain関数が実行され。以下のような表示になります。
これでWebAssemblyが実行されていることが確認できました。
5. デベロッパーツールでWebAssemblyの中身を見る
ブラウザで実行されているWebAssemblyについては、ブラウザのデベロッパーツールで確認することが出来ます。
Chromeの場合だと以下のように、「ソース」タブを開きecho.wasm
を選択するとテキスト形式で実行ファイルの中身が表示されます。
※ WebAssemblyを使用しているサイトやサービスについても同様に、デベロッパーツールでWebAssemblyのファイルを覗くことが出来ます。処理内容が丸裸になりますのでWebAssemblyを本番運用する際は注意しましょう。
(参考) emcc
コマンドのオプションについて
emcc
コマンドでよく使われそうなオプションを以下に示します。
オプション引数 | 説明 |
---|---|
-o | 出力ファイル名とファイルタイプ |
-O0 | 最適化を実施しない(デフォルト) |
-O1 | 簡単な最適化を実施。コンパイル時にLLVMの-O1オプションを適用。リンク時にいくつかのランタイムアサーションを含めない。 |
-O2 | -O1よりも多くの最適化を行う。リンク時にjsの最適化も実施。 |
-O3 | -O2よりも多くの最適化を行う。リリース時の推奨。 |
-Os | -O3と同様だが、よりコードサイズを小さくするような最適化を行う。 |
-Oz | -Osと同様だが、さらにコードサイズを小さくするような最適化を行う。 |
-g0 | コードがデバッグできるように何もしない。 |
-g1 | リンク時にjsのスペースを保持する。 |
-g2, --profiling | リンク時に関数名を保持する。 |
-g3, -g | コンパイル時にデバッグ情報やjsのスペース、関数名、LLVMデバッグ情報を保持する。 |
--emit-symbol-map | リンク時に関数名とwasmの関数インデックスの対応を出力する。 |
-sOPTION[=VALUE] | オプション |
-sOPTION
で指定できるオプションは以下のようなものがあります。
オプション引数 | 説明 |
---|---|
-sWASM | 0:wasmではなくjsを出力する。1:wasmを出力する。2:wasmとjsの両方を出力する。wasmの実行に失敗したときはWsm2JSモードでjsが実行される。 |
-sEXPORTED_FUNCTIONS | deadコードと見做されて削除されないようにexportする関数一覧。"_関数名"の形で記載する。指定しない際は"_main"が暗黙的に適用される。STANDALONE_WASMモードの際は"__start"が暗黙的に適用される。 |
-sEXPORT_ALL | DCE(Dead Code Elimination)を実行せずにすべてのシンボルを保持する。 |
-sEXPORTED_RUNTIME_METHODS | ランタイムからModuleへexportする関数名一覧。 |
-sINVOKE_RUN | wasm読み込み時にmain関数を実行するか否か。falseに設定した場合でもModule.callMain()で実行できる。 |
-sWARN_ON_UNDEFINED_SYMBOLS | jsで定義されていない関数があった際にwarningを出力する。 |
-sERROR_ON_UNDEFINED_SYMBOLS | jsで定義されていない関数があった際にerrorを出力する。 |
-sEXIT_RUNTIME | 0を指定した場合main関数終了後にランタイムを終了させない。 |
-sSTANDALONE_WASM | JavaScript無しで実行できるwasmを出力する。実行にはwasiなどのAPIが必要。 |
なお、オプションに複数の値を設定する際は以下のような複数の書き方ができます
これらもリファレンスに記載されていますので詳細はこちらをご参照ください。
-sEXPORTED_FUNCTIONS=foo,bar
-sEXPORTED_FUNCTIONS="foo","bar"
-sEXPORTED_FUNCTIONS=["foo","bar"]
-sEXPORTED_FUNCTIONS=[foo,bar]
-sEXPORTED_FUNCTIONS="['liblib.so']"
-s"EXPORTED_FUNCTIONS=['liblib.so']"
emcc
コマンドのオプション引数の全量はこちらのページをご参照ください。
-sOPTION
で指定できるオプションはこちらに記載されています。