概要
今回はUnityと、ビルドしたアプリからWebAssemblyを利用する手順についてまとめたいと思います。
もともと、ビルド後のアプリに対してコードを追加することができないかなと思っていたところ、たるこすさんの以下の記事を見かけたのがきっかけです。
たるこすさんはこれをさらに汎用化し、VRCなどで振る舞いとモデルを組み合わせたものを使えるようにする、というすばらしい思想で個人プロジェクトを作っているようなので興味がある方はご覧になってください。
今回の実装サンプルはGitHubにアップしてあります。
WebAssemblyとは
Wikipediaから引用させてもらうと以下ように説明されています。
WebAssemblyは、ウェブブラウザのクライアントサイドスクリプトとして動作するプログラミング言語(低水準言語)である。wasmとも称されており、ブラウザ上でバイナリフォーマットの形で実行可能であることを特徴とする。2017年現在開発が進められており、最初の目標としてCとC++からのコンパイルをサポートすることを目指している他、Rustがバージョン1.14以降で、Goがバージョン1.11以降で、Kotlin/Nativeがバージョン0.4以降でで対応するなど、他のプログラミング言語のサポートも進められている。
ブラウザ上でバイナリフォーマットのプログラムを実行させることを目的としているわけですね。
ただブラウザ上で動くということはクロスプラットフォームでもあるわけで、その汎用性から注目されているようです。
WebAssemblyをUnityで扱えるようにする
さてではこのWebAssemblyをどのようにしてUnityで利用したらいいのでしょうか。
色々調べてみたところ、C#からWebAssemblyを利用できるようにする以下のGitHubプロジェクトを見つけました。今回はこれを使って行きたいと思います。
WebAssemblyを作る
上記セットアップを終えたらUnityで(C#から)WebAssemblyを扱うことができるようになります。
しかし使えるようになっても肝心のWebAssemblyのファイルがないと始まりませんね。
ということでWebAssemblyファイルを準備します。
以下のサイトで言及されていたWebサービスが手軽に生成できるのでとてもよかったです。
Webサービスはこちら↓
https://mbebenita.github.io/WasmExplorer/
ちなみに上記サイトはCとC++両方でコードを書けてWebAssemblyにコンパイルできます。が、C++だとマングリングされてしまって、C#から利用する際に関数名などが分からなくなってしまうのでCでコンパイルするといいでしょう。
今回はサンプルとしてTest
関数を以下のように定義してコンパイルします。
int Test(int a) { return a + 123; }
コマンドラインでCをWebAssemblyに変換する
いちおう正攻法というか、サービスを利用せずにC/C++からwasm
ファイルにコンパイルする方法も書いておきます。
手順についてはMozillaのMDNで解説されているのでそれに従います。
Emscripten環境を構築する
Emscriptenを利用してwasm
ファイルにコンパイルするようです。なのでまずは環境を作ります。
環境構築自体はとても簡単で、こちらのセットアップ手順を実行するだけです。
# Get the emsdk repo $ git clone https://github.com/emscripten-core/emsdk.git # Enter that directory $ cd emsdk
※ Python3.6以降が必要なので、もし環境がない人は別途Pythonのセットアップが必要になります。
自分はAnacondaを使っているので、そちらのコマンドプロンプトで実行しました。
次に、必要なツールをインストールします。
以下はMacOSでの手順です。
# Download and install the latest SDK tools. $ ./emsdk install latest # Make the "latest" SDK "active" for the current user. (writes .emscripten file) $ ./emsdk activate latest # Activate PATH and other environment variables in the current terminal $ source ./emsdk_env.sh
自分はWindowsなので、Windowsでの手順も書いておきます。(MacOSとちょっとだけ異なります)
ドキュメントにも以下のように注釈があります。
On Windows, run emsdk instead of ./emsdk, and emsdk_env.bat instead of source ./emsdk_env.sh.
これに従って操作を書き直すと以下の手順になります。
$ emsdk install latest $ emsdk activate latest $ emsdk_env.bat
上記を実行するとemcc
コマンドが利用できるようになります。
Cでコードを書く
今回はサンプルなので簡単なコードでコンパイルしてみます。
以下のように(上記と同じ)Test
関数をひとつだけ定義したc
ファイルを作成します。
#include <emscripten/emscripten.h> int EMSCRIPTEN_KEEPALIVE Test(int a) { return a + 123; }
なお、EMSCRIPTEN_KEEPALIVE
を関数の頭に入れておかないと、利用されていない関数はコンパイラによって削除されてしまうので注意が必要です。
このマクロを使う場合は<emscripten/emscripten.h>
をインクルードする必要があります。
CコードをWebAssemblyにコンパイルする
準備ができました。あとはこれをwasm
にコンパイルします。コンパイルにはemcc
コマンドを利用します。
今回の場合、hello.c
ファイルを作成したので以下のようにコマンドを実行しました。
$ emcc hello.c -s WASM=1
これでwasm
ファイルが生成されます。
C#からWebAssemblyを読み込んで利用する
すべての準備が終わりました。あとは最初に紹介したライブラリを使って実際にwasm
ファイルを読み込んで実行してみましょう。
using System.Collections.Generic; using UnityEngine; using Wasm; using Wasm.Interpret; public class WebAssemblyTest : MonoBehaviour { private void Start() { // wasmファイルを読み込む string path = Application.streamingAssetsPath + "/test.wasm"; WasmFile file = WasmFile.ReadBinary(path); // importerを生成 var importer = new PredefinedImporter(); // wasmをインスタンス化 ModuleInstance module = ModuleInstance.Instantiate(file, importer); // インスタンスから、定義済み関数の取得を試みる if (module.ExportedFunctions.TryGetValue("Test", out FunctionDefinition funcDef)) { // 関数が見つかったらそれを実行 IReadOnlyList<object> results = funcDef.Invoke(new object[] { 1, }); Debug.Log(results[0]); } } }
非常にシンプルな記述でWebAssemblyを利用できるのが分かるかと思います。
上で紹介したサービスでコンパイルしたwasm
ファイルをストリーミングアセット配下に起き、ランタイムで読み込んで実行するとしっかりと計算されているのが確認できます。
しっかりと引数の1
と123
が計算されているのが分かりますね。
C側からC#で定義した関数を呼び出す
C側のコードを実行できることが確認できました。
次は逆、つまりC#側に定義されている関数を呼び出す方法を見てみます。
Cコードを以下のように変更します。
int GetParam(); int Test(int a) { int param = GetParam(); return a + 123 + param; }
GetParam
が定義されていませんが、それをTest
関数内で利用しています。このGetParam
をC#側で用意するわけですね。
C#側で定義したものを呼び出せるようにするには以下のようにimporter
に定義を設定します。
// C#側の関数の定義 private IReadOnlyList<object> GetParam(IReadOnlyList<object> args) { return new object[] { 100, }; } // ------------------ // 上記関数をimporterに設定 var importer = new PredefinedImporter(); importer.DefineFunction( "GetParam", new DelegateFunctionDefinition( new WasmValueType[] { }, new [] { WasmValueType.Int32, }, GetParam));
定義を設定するにはimporterのDefineFunction
メソッドを利用します。
第一引数にはC側で利用を想定している関数名を文字列で、第二引数にはDelegateFunctionDefinition
クラスのインスタンスを渡します。
DelegateFunctionDefinition
クラスのコンストラクタは以下のシグネチャを持ちます。
public void DefineFunction(string name, FunctionDefinition definition);
実行すると、しっかりと100
が足されているのが分かるります。
これでC#側で定義したコードをC側から呼び出すことができました。
これができれば、あとはMonoBehaviour
側とやり取りするためのインターフェースを定義しておけば自由にコードを実行することができるようになります。
サーバからWebAssemblyをダウンロードして利用する
最後に、サーバからバイナリを取得して実行する部分もメモしておこうと思います。
といってもやることはシンプルです。
まず、サーバにアクセスするときと同様にUnityWebRequest
を使ってデータを取得します。
取得後、byte配列をストリーム経由で読み出せるようにセットアップしてWasmFile
クラスのコンストラクタに渡せばOKです。
コード断片を載せておきます。
// UnityWebRequestのGetリクエストを生成 UnityWebRequest req = UnityWebRequest.Get(_url); // 取得されたらコールバックで処理 req.SendWebRequest().completed += operation => { // メモリストリームを生成 MemoryStream stream = new MemoryStream(); // ストリームに、取得したbyte配列を設定 stream.Write(req.downloadHandler.data, 0, req.downloadHandler.data.Length); stream.Seek(0, SeekOrigin.Begin); // メモリストリームから`WasmFile`オブジェクトを生成 WasmFile file = WasmFile.ReadBinary(stream); // 以下は上記サンプルと同様の手順で関数を呼び出す };
最後に
ライブラリのおかげでだいぶ簡単にWebAssemblyが利用できることが分かったかと思います。
iOS/Androidの実機でも動作することが確認できたので、ちょっとした機能の追加など、WebAssemblyを利用することで可能となることが出てくると思います。
自分は特に今、ARアプリを作っていて、さらにARならではの新しい体験を作ることがメインなのでこうした「あとから追加」の機能は色々なところで利用できそうです。