はじめに
前回のWebAssemblyをwindows上で使用する では、C++で書いたソースをコンパイルしてNode.jsで呼び出してみました。しかし、普通のWebページはブラウザを使用してhtmlとJavaScriptを読み込みWebページを見ています。そのため、Webアセンブリでも同じようにブラウザからアクセスしてhtml、JavaScriptから呼び出して実現をする方法をまとめます。
環境
- windows 10
- python:3.6.5
- emcc:1.39.16
- clang:11.0.0
- python:3.6.5
環境作成
前回のWebAssemblyをwindows上で使用するを見てください。
Webアセンブリファイルを作成する
Webアセンブリ用のC++を作成してコンパイルをします。前回と同じですが、サンプルのコードとコンパイルのオプションが若干異なります。
C/C++ファイルの作成
今回は単純に10を返す関数をC++で用意します。ちなみにmain関数以外の独自関数は、他の設定が必要なためmainに処理を記載しています。独自関数の連携を見たい場合は最後らへんのところを見てください。
int main() {
return 10;
}
Emscripten用のプロンプト起動
ダウンロードしてきたemsdk-masterの下にあるemcmdprompt.bat
をダブルクリックしてEmscripten用のコマンドプロンプトを起動します。
コマンドプロンプト
C:\my_program\webassenble\emsdk-master>emcmdprompt.bat
Adding directories to PATH:
~~~ 略 ~~~
Setting environment variables:
~~~ 略 ~~~
JAVA_HOME = C:\my_program\webassenble\emsdk-master\java\8.152_64bit
コンパイル
作成したC++ファイルのフォルダまで移動してem++
コマンドでコンパイルを行います。
コマンド
em++ 入力元C++ファイル -o 出力先wasmファイル -s WASM=1
WASMが0のときはasm.jsファイルになります。他のオプションについては公式を参照してください。
コンソール
>em++ cmain.cpp -o hello.wasm -s WASM=1
>
コマンドが終わると同フォルダ内にhello.wasmができているはずです。
wasmのインポート情報の確認
htmlからwasmを呼び出すために必要な情報を見るためにwasmを実行するhtmlを作成してブラウザ上から確かめます。
※アセンブリなので中のファイルを見ることのできるソフトがあれば不要な手順だと思います。
インポート情報確認htmlを作成する
インポート情報を確認したいだけなのでwasnを呼び出すだけのhtmlファイルを作成します。wasnファイルの読み込みはWebAssemblyのinstantiateStreamingを使用して読み込みます。インポート用の設定を見たいだけなのでインポート後は何もしません。(厳密にはインポートも失敗しています)
WebAssembly.instantiateStreaming(fetch(コンパイル済みwasmファイル名), インポート設定)
.then(インポート後の処理)
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>WebAssembly</title>
</head>
<body>
<script>
var importObject = {};
WebAssembly.instantiateStreaming(
fetch('hello.wasm'), importObject
).then(obj => a = obj);
</script>
</body>
</html>
実行用のサーバを起動するスクリプトを作成する
Webアセンブリを実行するときはhtmlやJavaScriptの実行のようにファイルをクリックしただけでは実行できません。今回はpythonで簡易的なサーバを作成して試します。
注意点としてはMIME Typeに'application/wasm'を追加していないとwasmが実行できないため、ハンドラーに追加しています。
import http.server
import socketserver
Handler = http.server.SimpleHTTPRequestHandler
# MIME Typeに'application/wasm'を追加
Handler.extensions_map['.wasm'] = 'application/wasm'
with socketserver.TCPServer(("", 8000), Handler) as httpd:
print("サーバ起動 port", 8000)
httpd.serve_forever()
実行用のサーバを起動する
起動するには実行用のスクリプトを起動するだけです。
> python .\server_sample.py
サーバ起動 port 8000
サーバにアクセスして開発者ツールを開く
上の設定ではポートを8000に設定しているのでブラウザのURLにlocalhost:8000を入れてアクセスします。CromeではF12で開きます。左の画面がブラウザの画面で右側が開発者ツールになります。
インポート確認用のhtmlにアクセスしてwsamの設定を確認する
ブラウザ画面のmy_hello.htmlをクリックすると開発者ツールにwasmファイルが表示されるのでクリックして設定を確認します。
今回は最低限wasmの関数を呼び出せるようにしたいため、以下のようなimport xxxと書かれたところを確認します。
抜粋
(import "wasi_snapshot_preview1" "proc_exit" (func $wasi_snapshot_preview1.proc_exit (;0;) (param i32)))
wasmを呼び出すhtmlを作成する
必要な情報がそろったのでwasmを呼び出すhtmlを作成します。基本的にはインポート確認用のhtmlを改造していくことになります。
importObjectの設定
先ほど確認した情報をimportObjectに設定していきます。ここではmemoryやポインタの扱いなど色々な設定ができますが今回は最低限インポートに必要な値のみを入れておきます。import後の値をキーにした連想配列をimportObjectとして設定します。
上で確認したインポート設定
(import "wasi_snapshot_preview1" "proc_exit" (func $wasi_snapshot_preview1.proc_exit (;0;) (param i32)))
htmlに記載するimportObject
var importObject = { wasi_snapshot_preview1: { proc_exit: arg => console.log(arg) } };
インポート後の処理
インポート後の情報はobjに格納されているため、obj.instance.exports.関数名()でC++の関数を実行して、戻り値をテキストボックスに格納しています。ちなみにexportsや関数名もインポートの設定確認で見たwasmにありました。
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>WebAssembly</title>
</head>
<body>
<input type="text" id="output"/>
<script>
var importObject = { wasi_snapshot_preview1: { proc_exit: arg => console.log(arg) } };
WebAssembly.instantiateStreaming(
fetch('hello.wasm'), importObject
).then(obj => document.getElementById("output").value = obj.instance.exports.main());
</script>
</body>
</html>
実行
インポート設定の確認用のサーバを再起動して上のhtmlを実行します。my_hello.htmlを開くとテキストボックスにC++の戻り値10が入っているためC++との連携ができたことがわかります。
開いたらいきなり値が入っていたのでクリックして加算するように変えてみました
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>WebAssembly</title>
</head>
<body>
<input type="button" id="add_num" value="加算" /><input type="text" id="output"/>
<script>
var importObject = { wasi_snapshot_preview1: { proc_exit: arg => console.log(arg) } };
WebAssembly.instantiateStreaming(
fetch('hello.wasm'), importObject
).then(obj => document.getElementById("output").value = obj.instance.exports.main());
document.getElementById('add_num').addEventListener('click', () => {
WebAssembly.instantiateStreaming(
fetch('hello.wasm'), importObject
).then(obj =>
document.getElementById("output").value = parseInt(document.getElementById("output").value) + parseInt(obj.instance.exports.main()));
}, false
);
</script>
</body>
</html>
独自のC++関数の連携
main関数の連携ができたため、次は独自の関数の連携を行います。
C++の設定
C++の方にヘッダと独自関数を作成します。ヘッダは<emscripten/emscripten.h>
、独自関数には関数名の前にEMSCRIPTEN_KEEPALIVE
をつけて、extern "C"
で囲みます。
#include <string>
#include <emscripten/emscripten.h>
int main() {
return 10;
}
extern "C" {
int EMSCRIPTEN_KEEPALIVE addOne() {
return 1;
}
}
コンパイル
コンパイルにはEXPORTED_FUNCTIONS
オプションに関数を追加します。この時に_(アンダーバー)
を関数名の前につけるのとオプション全体の"(ダブルクォーテーション)
を忘れないようにしてください。
コンソール
em++ cmain.cpp -o hello.wasm -s WASM=1 -s "EXPORTED_FUNCTIONS=['_main', '_hello']"
htmlの追加
htmlには、mainと同じように追加した関数を呼び出すだけで連携できます。
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>WebAssembly</title>
</head>
<body>
<input type="button" id="add_num" value="加算" /><input type="text" id="output"/>
<script>
var importObject = { wasi_snapshot_preview1: { proc_exit: arg => console.log(arg) } };
WebAssembly.instantiateStreaming(
fetch('hello.wasm'), importObject
).then(obj => document.getElementById("output").value = obj.instance.exports.main());
document.getElementById('add_num').addEventListener('click', () => {
WebAssembly.instantiateStreaming(
fetch('hello.wasm'), importObject
).then(
obj => document.getElementById("output").value = parseInt(document.getElementById("output").value) + parseInt(obj.instance.exports.addOne())
)}, false);
</script>
</body>
</html>
おわりに
Webアセンブリについて、htmlからJavaScript経由でwasmを呼び出してみました。インストールを簡単にするためにEmscriptenを使用しましたが、そのために自作のhtmlやJavaScriptとの連携が難しくなってしまいました。最初に楽するか途中に楽をするかによって標準の方法かEmscriptenを使うのか選ぶのが良い気がします。