こんにちは、エンジニアリングマネージャーの渡辺(@mochi_neko_7)です。
唐突ですが、自分は「WebAssembly(Wasm)」という技術はほとんどよく知らずに「Webブラウザ上でJavaScript以外のコードが高パフォーマンスで動かせる」くらいのものだと勝手に想像していて、UnityやC#でアプリケーションの開発をしている自分とは関わりが薄いだろうと思っていました。
ところが、たまたま別の調べ物をしていた際に次の記事を見つけ、Unityのアプリケーション上でWasmのコードを動かすことができることに衝撃を受けました。
特に驚いたポイントは下記でした。
- Wasmがブラウザだけではなくアプリの上でも動作すること
- Wasmの作成は様々な言語で可能なこと
- WasmのバイナリはOSやCPUアーキテクチャ別に用意する必要がないこと*1
通常のソフトウェア開発ではこれらの壁を意識しながら開発をするのが当たり前だと思っていたものが、その壁を取り払う壮大な(それに比例して大変な)取り組みだと認識しました。
それをきっかけにWasmがどんなものか、UnityでWasmを動かすためにはどうすればいいかなどが気になり、趣味で色々触るようになりました。
いったんの区切りとして、UnityにWasmランタイムをネイティブプラグインとして組み込み、macOSのUnityEditor上でWasmを動かすところまでできました。
今回はその過程で得られたWasmのランタイムとC#(.NET)周辺のエコシステム、Hello World、WasmのAoTコンパイルの理解、考えられるUnityでの活用方法などをご紹介します。
検証等に使用した環境は下記になります。
- macOS 13.1 M2(Apple Silicon)
- Unity 2021.3.0f1(Apple Silicon版)
- Wasmtime 5.0.0
目次:
WebAssemblyの概要
WebAssembly(Wasm)についての詳しい説明は公式やMozillaのドキュメントをはじめとして様々な情報が出てくると思いますのであまり詳しくは説明しません。
- WebAssembly
- WebAssembly の概要 - WebAssembly | MDN
- WebAssemblyとは - Qiita
- WebAssembly調べてみた - Qiita
- WebAssemblyはJVMやeBPFのリバイバルではない WasmがWeb以外でもアツい理由 - ログミーTech
Wasmの特徴を一部挙げるなら、下記でしょうか。
Wasmの基本的な開発・実行フローは大まかに説明するとこのようなイメージです。
- Wasmへのコンパイルに対応している好きな開発言語でプログラムを作成する
- 作成したプログラムをコンパイルしてWasmバイナリ(
.wasm
)にビルドする - Wasmランタイムを組み込んだ環境(対応しているブラウザ、もしくはWasmランタイムをライブラリとして組み込んだアプリケーション)で
.wasm
をJITコンパイルして実行する*5
Wasmは通常サンドボックス化されているため外部環境へアクセスするには一手間必要なのですが、WebAssembly System Interface(WASI)を利用することで直接OSの機能へのアクセスも可能になっています。
最近はLinux Kernel内にWebAssemblyのラインタイムが実装されたりもしていました。
自分が当初イメージしていた「Webブラウザ上でJavaScript以外のコードが高パフォーマンスで動かせる」とは既にかけ離れていて、ブラウザには止まっていませんし、JavaScript以外どころかメジャーな言語はほぼ対応していることは驚きでした。
Javaの「Write once, run anywhere」を超えて、好きな言語で書いたコードがどんな環境でも動作する、という世界観が実現しうるのではないでしょうか。
Unityで利用可能なWasmランタイムは?
Wasmは規格なので実際に利用するためにはその実装としてのWasmランタイムを利用することになります。
ざっと調べて出てくる有名そうなWasmランタイムは下記でした。
- ブラウザでのサポート*6
- Node.jsでのサポート*7
- Wasmtime
- WasmランタイムのReference実装
- Wasmer
- Unstableな機能や広い対応言語、周辺ツールなども含んでいる
- WasmEdge
- 組み込みなどの環境向け
- WAMR
- Wasmランタイムの最小限の実装
他にもランタイム自体はたくさんあるようです。
これらを含め、Unityで利用可能なランタイムを調べたり触ってみたりしました。
Wasmer (WasmerSharp)
Wasmを調べるきっかけとなった記事ではWasmerを使っていたため、まずWasmerから触ってみました。
WasmerのGitHubを見ると、C#実装のWasmerSharpも用意されているようです。
ところがWasmerSharpをよく調べてみると、組み込まれているWasmerのRivisionが v0.5.7
前後(2019/7頃)のもので、2023/02/05現在の最新版の v3.1.1
とは大きく離れていることが分かりました。
APIの変更も入っているため最新版のWasmerを利用することはそのままでは難しく、自分でネイティブプラグインのBridgeを書く必要がありそうです。
v0.5.7
前後当時はARM64(AppleSilicon)のmacOSの対応もないためReleaseには自分の環境で使用できるプラグインもなく、該当コミットをチェックアウトして自分でRustのライブラリをビルドすることも試みたのですが、依存しているCrateが古すぎて無理でした。
ビルド環境をDockerで作ってるけど、依存先のcargo_tomlのバージョンが古すぎてエラーが出てるっぽい...
— もちねこ (@mochi_neko_7) 2023年1月29日
これどうすればいいんだ?
結論としては現時点ではWasmerを手軽に利用することは難しそうです。
ちなみに参照先の記事ではWasmerを使ってたのでは?と思われるかもしれませんが、あれはWasmerそのものを組み込んでいるわけではなく、Wasmerを利用して指定のWasmを動かすネイティブライブラリをRustで作っているものでしたので汎用的なものではありませんでした。
AppleSiliconのmacOSなどでWasmerを動かすには、自分で最新のWasmerのC# Bridgeを改修する必要がありそうです。
cs-wasm
「Unity WebAssembly」で検索していると、たるこすさんが cs-wasm というライブラリを使用して実際にUnity上でWasmを動かしていることを知りました。
Repositoryを覗いてどのような実装になっているのか調べてみたのですが、何かネイティブプラグインを組み込んでいる様子もなく、C#で独自実装しているように見えます。
動作させるだけなら問題はないのかもしれませんが、WasmtimeやWasmerなどの活発なOSSと比較すると開発スピードや実績などで不安な点も残るため、できれば他の選択肢がない場合の最終手段としたいのが個人的な所感です。
Wasmtime (wasmtime-dotnet)
Wasmerばかり触っていたのでWasmtimeも触ってみようと調べてみると、WasmtimeもC#(.NET)実装のwasm-dotnetがありました。
wasm-dotnetはWasmtimeの最新版にも追従していて、Wasmerであったバージョンの心配はなさそうです。
実際にUnityに組み込んでHello Worldのサンプルを移植して動かしてみたところ、Editor上では問題なく動作しました。
やっとUnityでWasmのライブラリが動いた! pic.twitter.com/BHCiIwKn7B
— もちねこ (@mochi_neko_7) 2023年2月4日
一応Unityから利用しやすいようUPMで参照できる形に整備をしました
ただしREADMEのトップに書いているように、IL2CPPビルドをすると実行時にエラーが出てしまう不具合が残っているため、まだビルドに組み込むことはできません...!
ですのでまだEditor上でしか触れませんが、それでも自分で触ってみたい方はこちらのRepositoryを参照してください。
結論としてはWasmtimeと言いたいところですが、後述するようにAndroid/iOSをサポートしていない問題もあり、まだ断言できない状況です。
今すぐ組み込んで使用したい方はcs-wasmを、Wasmの最新のProposalの機能なども利用したい方はWasmtimeやWasmerなどの環境が整うのを待つ or 自分で整える、といったところでしょうか。
Hello World
WasmtimeのAPIの触り方を理解するには公式のサンプルやドキュメントを読み込むのが一番おすすめなのですがRustで書かれているので、C#に慣れている方だとwasmtime-dotnetのサンプルの方が読みやすいと思います。
- Introduction - Wasmtime
- wasmtime/hello.rs at main · bytecodealliance/wasmtime · GitHub
- wasmtime-dotnet/Program.cs at main · bytecodealliance/wasmtime-dotnet · GitHub
これらサンプルと併せて、MozillaのドキュメントなどのWasm固有のドメイン用語に関しての説明を読むと理解が早いと思います。
Engine、Store、Module、Instance、Import、Export辺りがわかるとWasmerなどの他のランタイムも触れるようになると思います。
Hello WorldのサンプルをUnity向けに少し調整するとこのような感じになります。
using UnityEngine; using Wasmtime; namespace Mochineko.WastimeDotNetUnity.Demo { internal sealed class WasmHelloWorld : MonoBehaviour { private void Start() { const string wat = @" (module (type $t0 (func)) (import """" ""hello"" (func $.hello (type $t0))) (func $run call $.hello ) (export ""run"" (func $run)) )"; using var engine = new Engine(); using var module = Module.FromText(engine, "hello", wat); using var linker = new Linker(engine); using var store = new Store(engine); linker.Define( "", "hello", Function.FromCallback(store, () => Debug.Log("Hello from C#, WebAssembly!")) ); var instance = linker.Instantiate(store, module); var run = instance.GetAction("run"); if (run is null) { Debug.LogError("error: run export is missing"); return; } run(); } } }
他にもWasm内でUnityのCubeを生成する処理を呼び出すデモも用意してみました。
AoTコンパイル
Wasmでは基本的にはJITコンパイルが使用されることがほとんどですが、AoTコンパイルを使用することもランタイムによっては可能です。
特にJITコンパイルが使えないiOSなど*8の環境では、AoTコンパイルを使用しなくてはなりません。
ただWasmのAoTコンパイルに関しては情報が少なく(需要があまりない?)、自分の手元で動作確認もできていないため理解が怪しいところもあるかもしれません。
Unityを使用してiOS向けにアプリをリリースする場合もあると思いますので、自分の理解している範囲でWasmのAoTコンパイルの仕組みに関して簡単に説明します。
まずWasmのソースコードは2通りのフォーマットが利用できます。
- バイナリ(
.wasm
) - テキスト(
.wat
)
これらをコンパイルしたものはModuleと呼ばれます。
このModuleをSerializeしたコンパイル済みのバイナリは、ランタイムでDeserializeすることでModuleとして利用できます。
.wasm
or .wat
--> (compile) --> Module --> (serialize) --> Serialized Module --> (deserialize) --> Module --> Instance
つまり Serialized Module がAoTコンパイルしたバイナリに相当するため、AoTコンパイルを利用する際はコンパイル済みのバイナリを用意して、ランタイムで直接ModuleにDeserializeして利用する流れになります。
ただこのSerialized Moduleは各ランタイムにAPIが用意されているのは確認できますが、ファイルとしての扱いに関する情報が少なくあまり理解が進んでいません。
Wasmtimeでは .cwasm
(Compiled Wasm?)のCLIが用意されているようです。
https://docs.wasmtime.dev/cli-options.html?highlight=aot#compile
Wasmerでは、公式のドキュメントには情報がないのですが、下記の記事から .wasmu
("u"は何の意味?)が利用できるようです。
おそらくJITコンパイルの場合と異なる下記の点に注意が必要になりそうです。
- 一度コンパイルするためターゲットプラットフォーム別にバイナリが必要
- 別のランタイムとの互換性はなさそう(コンパイラが違ったら動かないので仕方ないかもですが)
JITコンパイルより少し取り回しは悪くなってしまいますが、AoTコンパイルを利用すること自体は可能そうです。
UnityでのWasmの使い道
UnityでWasmを動かせるのは分かりましたが、どのような使い道がありそうでしょうか?
ざっと思いついたものを並べてみました。
ランタイムスクリプトとしてのWasm
こちらの記事にも詳しく書かれているように、アプリケーションのビルド後にロジックを加えたり変更するためのランタイムスクリプトとしてWasmを使うことができます。
UnityのAPI(e.g. GameObject
)やイベント(e.g. OnUpdate
)などをインターフェースとしてImport/Exportに設定して、WebAssembly側からそれらを呼び出すことは可能です。
あるいはこちらのプロジェクトのように、アプリケーションを横断する汎用ロジックのフォーマットとしても活用することができます。
開発環境の整備が必要ですが、ユーザーがどの言語で書くかも選択できるというのはWasmならではではないでしょうか。
あとは稀なケースかもしれませんが、利用する外部サービスの頻繁な仕様変更に耐えるためのBuffer的な立ち位置でも利用することができるかもしれません。
Unityとは関係ないですが、ブロックチェーンのスマートコントラクトでもWasmが注目されているみたいです。
ネイティブプラグインの代替としてのWasm
Unityでは大半のコードはC#で書きますが、求められるパフォーマンスが厳しい場合にはネイティブで書いたコードをネイティブプラグインとして利用することもあると思います。
ですがネイティブプラグインは動作環境ごとにバイナリを用意する必要があり、ビルドやアップデートなどの管理のコストも考慮しなくてはなりません。
その点Wasmは実行速度はネイティブ並みで、かつバイナリはJITコンパイルでいいなら1つだけで済むため、通常のネイティブプラグインより取り回しが良いです。
もちろんまだWasmもできることは限定されているため、全てのネイティブプラグインを代替することはできませんが、ハマるケースもあるかもしれません。
また、C++やPythonを始めとしたC#以外のコード資産をWasmに変換して利用することもできるため、利用できるコード資産が増えるという観点もあります。
現在開発が進んでいるComponent Modelでその環境が整っていく見通しもあります。
Unityはクロスプラットフォーム対応をしている分、動作を想定するプラットフォームが多くなりやすいため、意外と相性は良いのではないでしょうか。
全く検証はしていませんが、WebGLもWasmで動いていますし、JavaScriptから動かすインターフェースなどあれば他のプラットフォームとのライブラリの共通化もできるかもしれません。
あとはWasmのバイナリはビルドに組み込まずにサーバーからダウンロードして利用することも可能なため、アプリケーションのビルドのサイズを減らすメリットもあるかもしれません。
サーバーとクライアントの共有コードとしてのWasm
Unityで動かせる前に、もちろんサーバーやブラウザでも同じWasmのコードが利用できますので、コアなロジックやライブラリを各環境で共有して、サービス全体のコアなコード資産を見通しよく管理する、ということもできるかもしれません。
まだ検証していないので妄想ですが、Unity上でPure C#(UnityのAPIに触らないで)で書いたコードをWasmに変換することもできるはずです。
C#→WasmといえばBlazorが有名ですが、BlazorはC#コンパイラ自体をWasm化して動かしてるみたいなのでちょっとシチュエーションが違うかもしれません。
とはいえそれだけのためにわざわざWasmを使うのは少しやり過ぎな気もするので、実際どれくらい有用なのかは分かりませんが。
実際に導入を検討する上での課題
今回Wasm周辺を触っている中で、実際にUnityでのアプリケーション開発に組み込むことを想定した場合に課題になるであると思った点も挙げます。
WasmランタイムのAndroid/iOSのサポートが弱い
WasmをAndroid/iOSで動かすための情報が調べてもあまり出てこないです。
WasmtimeのReleaseを確認しても、Windows/macOS/Linux向けのビルドしか確認できません。
公式ドキュメントを確認してみると、やはりWindows、macOS、Linuxしかサポートはしていないようです。
ただこちらのIssueやc-apiのCMakeList.txtを見る感じではAndroid向けのビルドも不可能ではなさそうです。(iOSは不明ですが)
そのためスマホ上で動作させたい場合は自分でWasmtimeをRustのライブラリとしてビルドする必要がありそうです。
試してみてはいるのですがビルド環境の構築に苦戦しています...
一方WasmerはiOSはサポートしているらしいです。
MakefileにもiOS向けのHeadless Engine(AoTコンパイルのみ搭載したもの)のビルドオプションが用意されています。
AndroidはこちらのIssueが進行中?
このAndroid/iOSの対応の状況を見て、結局どのランタイムを使うべきか悩み始めています...
Stringの扱いの難しさ
Wasmはいわゆる char や string のサポートはないため、文字列を扱う処理は工夫するしかないそうです。
ただ Reference Type というプロポーザルも進んでいるようで、最新のステータスは調べていないのでわからないのですが、仕様策定が進めば改善されるかもしれません。
Wasmtimeには先行実装とサンプルコードのようなものがあるみたいなので、もう少し触ってみたいです。
WasmやWASIのエコシステムが発展途上
Wasm、WASIの仕様策定も進行中ですし、周辺のツール等の整備もこれからだと思います。
ただComponent Modelの開発が進んだり、Docker ImageとしてWasmが利用されることが進めば良くなる見通しもあります。
所感
今回はUnityで利用することを目的に C#, .NET 向けの対応を中心に色々調べてみましたが、Wasmerの対応バージョンが古かったり、iOS/Androidのプラグインがなかったりと、まだまだ未整備な部分も見られました。
とはいえWasm自体のエコシステムはComponent Model始め改善が進むと思いますので、時間の問題かもしれません。
Unityで利用する観点では、Unityを触れるエンジニアでWasmの知識を持つ人もまだまだ少ないはずで、実際に導入するのもそれなりにハードルは高いでしょう。
この記事などをきっかけに関心を持つ人が増えたら少しはプラスになるかもしれません。
終わりに
UnityにWasmのランタイムをネイティブプラグインとして組み込み、Wasmのバイナリを実際に動かしてみることをゴールに試行錯誤して得られた学びを言語化して整理しました。
個人的にはWasmはかなり面白いのではと思っていて、自分の周辺業務での実用性があるのかは正直まだ分かりませんが、こまめに情報を追っていったり何かしらの形でコミュニティに貢献できればと思っています。
直近だとネイティブプラグインの作成の経験がないのもありWasmerの最新版のC# Bridge対応もチャレンジしています。
今回Wasm周りを自分で触っていく過程で、Wasm以外にもOSやCPUアーキテクチャ、VM、アセンブリ言語、RustとCargo、ネイティブプラグインなど低レイヤー寄りの知識が増えたのも思わぬ収穫でした。
ちなみにこういった知らない技術を触る時にはChatGPTさんが大活躍でした。
もしUnity/C#でWasmを動かしてみたい!という方の参考になれば幸いです。
最後に、自分自身まだWasmなどをキャッチアップしている途中で、低レイヤーの知識も自信があるわけではないため、もし間違っている記述などありましたらご指摘いただけますと嬉しいです。
*1:JITコンパイルを使用する場合は
*2:参考:https://postd.cc/what-makes-webassembly-fast/
*3:参考:https://zenn.dev/0kate/articles/83e48c177ff709
*4:参考:https://www.publickey1.jp/blog/21/webassemblyrustthe_state_of_webassembly_2021.html
*5:後で説明するようにAoTコンパイルを利用することも可能で、コンパイラは使用するランタイムによって異なります
*6:参考:https://www.publickey1.jp/blog/17/webassembly_browsers.html
*7:参考:https://nodejs.dev/en/learn/nodejs-with-webassembly/
*8:AppStoreは規約でJITコンパイルが禁止されています:https://developer.apple.com/jp/app-store/review/guidelines/#software-requirements