e.blog

主にUnity/UE周りのことについてまとめていきます

UnityでWebAssemblyを扱う

概要

今回はUnityと、ビルドしたアプリからWebAssemblyを利用する手順についてまとめたいと思います。

もともと、ビルド後のアプリに対してコードを追加することができないかなと思っていたところ、たるこすさんの以下の記事を見かけたのがきっかけです。

たるこすさんはこれをさらに汎用化し、VRCなどで振る舞いとモデルを組み合わせたものを使えるようにする、というすばらしい思想で個人プロジェクトを作っているようなので興味がある方はご覧になってください。

zenn.dev

今回の実装サンプルはGitHubにアップしてあります。

github.com

WebAssemblyとは

Wikipediaから引用させてもらうと以下ように説明されています。

WebAssemblyは、ウェブブラウザのクライアントサイドスクリプトとして動作するプログラミング言語(低水準言語)である。wasmとも称されており、ブラウザ上でバイナリフォーマットの形で実行可能であることを特徴とする。2017年現在開発が進められており、最初の目標としてCとC++からのコンパイルをサポートすることを目指している他、Rustがバージョン1.14以降で、Goがバージョン1.11以降で、Kotlin/Nativeがバージョン0.4以降でで対応するなど、他のプログラミング言語のサポートも進められている。

ブラウザ上でバイナリフォーマットのプログラムを実行させることを目的としているわけですね。
ただブラウザ上で動くということはクロスプラットフォームでもあるわけで、その汎用性から注目されているようです。

WebAssemblyをUnityで扱えるようにする

さてではこのWebAssemblyをどのようにしてUnityで利用したらいいのでしょうか。
色々調べてみたところ、C#からWebAssemblyを利用できるようにする以下のGitHubプロジェクトを見つけました。今回はこれを使って行きたいと思います。

github.com

WebAssemblyを作る

上記セットアップを終えたらUnityで(C#から)WebAssemblyを扱うことができるようになります。

しかし使えるようになっても肝心のWebAssemblyのファイルがないと始まりませんね。
ということでWebAssemblyファイルを準備します。

以下のサイトで言及されていたWebサービスが手軽に生成できるのでとてもよかったです。

www.freecodecamp.org

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で解説されているのでそれに従います。

developer.mozilla.org

Emscripten環境を構築する

Emscriptenを利用してwasmファイルにコンパイルするようです。なのでまずは環境を作ります。
環境構築自体はとても簡単で、こちらのセットアップ手順を実行するだけです。

まずはGitHubからリポジトリをクローンします。

# 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ファイルをストリーミングアセット配下に起き、ランタイムで読み込んで実行するとしっかりと計算されているのが確認できます。

しっかりと引数の1123が計算されているのが分かりますね。

C側からC#で定義した関数を呼び出す

C側のコードを実行できることが確認できました。
次は逆、つまりC#側に定義されている関数を呼び出す方法を見てみます。

Cコードを以下のように変更します。

int GetParam();

int Test(int a)
{
  int param = GetParam();
  return a + 123 + param;
}

GetParamが定義されていませんが、それをTest関数内で利用しています。このGetParamC#側で用意するわけですね。

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ならではの新しい体験を作ることがメインなのでこうした「あとから追加」の機能は色々なところで利用できそうです。