LoginSignup
12
10

More than 3 years have passed since last update.

WebAssemblyをhtmlから呼び出す

Last updated at Posted at 2020-06-13

はじめに

前回の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に処理を記載しています。独自関数の連携を見たい場合は最後らへんのところを見てください。

cmain.cpp

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のフォーマット

WebAssembly.instantiateStreaming(fetch(コンパイル済みwasmファイル名), インポート設定)
.then(インポート後の処理)
my_hello.html

<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が実行できないため、ハンドラーに追加しています。

server_sample.py

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で開きます。左の画面がブラウザの画面で右側が開発者ツールになります。

2.png

インポート確認用のhtmlにアクセスしてwsamの設定を確認する

ブラウザ画面のmy_hello.htmlをクリックすると開発者ツールにwasmファイルが表示されるのでクリックして設定を確認します。
今回は最低限wasmの関数を呼び出せるようにしたいため、以下のようなimport xxxと書かれたところを確認します。

1.png

抜粋


 (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にありました。

my_hello.html

<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++との連携ができたことがわかります。

3.png

開いたらいきなり値が入っていたのでクリックして加算するように変えてみました

my_hello.html

<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"で囲みます。

cmain.cpp

#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と同じように追加した関数を呼び出すだけで連携できます。

my_hello.html
<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を使うのか選ぶのが良い気がします。

12
10
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
12
10