📝

Unityからブラウザまで! ロジックの切り出しに WASI を使ってみる

2022/12/16に公開

記事の概要

要約

本記事の要約を以下に示します。

  • ゲーム本体からロジックを切り出して利用する場合、Luaがしばしば用いられている
    • 組み込みがしやすい、取り回しがよい(ロジックの保守性が向上する)などのメリットがある
    • スクリプト部分の差し替えが容易
  • ただし、Luaは速度面、記法面でデメリットがある
    • LuaJITかつJIT有効の環境でない限り、基本的に速度は遅い
      • fib(42) の計算時間を例に
    • 記法もやや独特であり、言語としての書きやすさには疑問符がつく
  • 本記事では、代替としてWASI (WebAssembly)を利用することを提案する
    • 高速で、単一の特定言語に縛られない(WASI対応言語である限り、選択肢がある)
    • Lua同様マルチプラットフォームで、Unityからブラウザまで広い環境で動作する
      • Luaとの比較をして動作例を示す
        • JIT無効の場合、最大で40〜50倍ほどWASIの方が高速
  • 一方、WASIは発展途上の技術という点が最大のデメリット
    • 実績不足や仕様面での不足がある
      • 解消される見込みはある
      • 今後に期待
    • また、Luaと比較してWASI (wasm)へのコンパイルの手間がある
      • wasm へのコンパイル時間は言語に依存
      • wasmu へのコンパイルはCraneliftを使う限り高速

サンプルプロジェクト

本記事で用いるコード・プロジェクト一式は以下で公開しています。

  • GitHubリポジトリ[1]
    • xLuaなどサードパーティのライブラリは含みません。
    • コンパイル済みファイルは含みません。すべてソースコードのみでの提供になります。

はじめに

Luaによるロジック切り出し

ゲーム開発において、ロジック切り出しは一つのテーマです。
現在、多くのゲームはUnreal Engine[2]やUnity[3]といったゲームエンジンを用いて開発されています。ゲームエンジンを用いることで、特定の言語を使って実装を行い、さまざまなプラットフォーム向けにビルドすることが可能です。実装の大部分は単一の言語で共通化できると言っても過言ではありません。

とはいえ、例外はつきものです。Unityでロールプレイングゲームを作る場合を考えてみましょう。基本的なゲームシステムはUnity標準のC#で手堅く実装可能ですし、問題はありません。
しかし、会話劇や個々のバトルなどをC#[4]で定義・実装するわけにはいきません。会話劇や個々のバトルを全てC#にしてしまうと、うち一つを修正するだけでも長いコンパイルが走り開発効率が悪化するためです。さらに、配布形態(ストアなど)によってはバイナリの修正をリリースするのに審査含め数営業日を要します。誤字脱字やパラメータ修正のためだけに審査を踏むのは無駄が大きいものです。そういったものはパッチ等で随時差し替え可能な、アプリのバイナリとは別の部分で定義・実装することが望ましいと言えます。つまり、ロジックの切り出しが必要になるわけです。

対応する最も単純な実装としては、パラメータやデータをテキストやJSONなどにまとめておき、それをC#で記述した実装、いわゆるスクリプトエンジンで読み出すことが考えられます。しかし、これは車輪の再発明に近く「会話劇で条件分岐をしたい」という要件のために、評価式のパーサーから実装を検討するようなものです。そういう面倒な実装は既存のものを使って、APIの定義やロジックの中身の開発に注力したいと考えるのが一般的でしょう。

そのため、こうしたロジックの共通化にはしばしばLua[5]が用いられます。Luaは組み込みが容易なスクリプト言語で取り回しがよいのが特徴です。Luaに会話劇やバトルのロジックを記述することで、式の評価などの低レイヤーな部分は既存の実装に任せることが可能になり、開発者はロジックの中身に注力することが可能になります。また、スクリプトは本体と切り離されているので、パッチ等で後から差し替えることも容易です。修正ごとに長いコンパイルが走る心配もありません。

Luaの弱点

処理速度

一方で、Luaには欠点があります。よく挙げられるのは、処理速度と記法でしょう。
まず、処理速度について触れます。Luaは組み込みやすい反面、処理速度は速いといえません。
例えば、フィボナッチ数を計算するコードで速度を測定してみましょう。ある程度の処理負荷になるよう、今回はフィボナッチ数列の42番目を求めます。Luaとの比較対象としてはRust[6]を用いました。

fib(42)を求めるLuaのコード
function fib(n)
  if n == 0 then
    return 0
  elseif n == 1 then
    return 1
  else
    return fib(n - 2) + fib(n - 1)
  end
end

N = 42

print(string.format("fib(%d) is %d", N, fib(N)))
fib(42)を求めるRustのコード
fn fib(n: i32) -> i32 {
    return match n {
        0 => 0,
        1 => 1,
        _ => fib(n - 2) + fib(n - 1)
    }
}

const N: i32 = 42;

fn main() {
    println!("fib({}) is {}", N, fib(42));
}

計測環境は、2021年モデルのMacBook Pro (M1)[7]です。計測にはhyperfine[8]を用い、3回実行した結果の平均所要時間を求めます。
これらの実行結果は以下の通りです。Rust実装を理論値(ネイティブ)と置いています。念のため、高速なLua実装として知られるLuaJIT[9]も比較に加えました。

  • Rust v1.65.0 ( -C opt-level=3 -C debug_assertions=no -C lto=yes )
    • Time (mean ± σ): 853.2 ms ± 1.3 ms [User: 843.1 ms, System: 2.1 ms]
  • Lua 5.4.4
    • Time (mean ± σ): 23.638 s ± 0.261 s [User: 23.557 s, System: 0.058 s]
  • LuaJIT 2.1.0-beta3 ( JIT有効 )
    • Time (mean ± σ): 1.826 s ± 0.006 s [User: 1.813 s, System: 0.004 s]
  • LuaJIT 2.1.0-beta3 ( JIT無効 )
    • Time (mean ± σ): 15.874 s ± 0.020 s [User: 15.820 s, System: 0.035 s]

この結果から分かる通り、少なくとも素のLuaの処理速度は極めて遅いことが分かります。JIT有効のLuaJITにしても、ネイティブと比較し2倍以上の時間を要しています。

文法

もう一つの弱点としては、Luaの文法、仕様が挙げられます。配列(テーブル)の1オリジンなどは有名でしょうか。

array = { "one", "two", "three" }
print(array[0]) -- nil
print(array[1]) -- "one"

さらに、否定の条件も一般的な != ではなく ~= という独特な記法です。

a = 10
if a != 0 then print("error") end -- これはエラーになる
if a ~= 0 then print("ok") end -- "ok"

switch-caseのような文もありませんし、型付けもできません。小さなプログラムを書くのにはよくても、ある程度大きくなってくると処理を追うだけも苦労を伴います。

WASI (WebAssembly)という選択肢

WASIとは

Luaは柔軟な反面、このようないくつかの弱点を抱えていることが分かりました。
そこで、今回はこのようなロジックの切り出しに、近年注目度が高まってきているWASIを用いてみることにします。

WASI[10]は、WebAssembly[11]にPOSIX[12]に似たインターフェースを持たせるための規格です。
WebAssemblyはもともとブラウザ向けの技術だったため、入出力などを扱うには自前でグルーコードを書く必要がありました。ブラウザとその他で同一のWebAssemblyを実行したいと思った場合、関数など諸々呼び出しの仕様を決めなくてはいけません。
WASIを用いると、こうした面倒な点をスキップできます。普通のCLIアプリケーションと全く同じコードをWebAssemblyとしてコンパイル・動作させることが可能です。

例えば、先ほど示したフィボナッチ数を求めるRustのコードは、変更を一切加えることなくWASIのWebAssemblyにコンパイルできます。

$ rustc -C opt-level=3 -C debug_assertions=no -C lto=yes --target wasm32-wasi fib.rs -o fib.wasm

出力された wasm ファイルは、WASI対応の任意のランタイムで実行可能です。代表的なランタイムであるWasmer[13]とWasmtime[14]で実行してみます。

$ wasmer run fib.wasm
fib(42) is 267914296
$ wasmtime run fib.wasm
fib(42) is 267914296

ご覧の通り、標準出力も当然のように利用できていることが分かります。もちろん、引数なども利用可能です。
WASIのランタイムは様々ですが、中でもRustで書かれているWasmerはスマートフォンやブラウザへの組み込みにも対応しており[15] [16]、これを使うと簡単にマルチプラットフォームでWASI (WebAssembly)を実行できるようになります。ライセンスもMIT Licenseと寛容であり、Luaと同様に様々な環境へ組み込むことができるわけです。

WASI (WebAssembly)の性能

肝心の性能について見ていきます。実行するコードは先ほどコンパイルした fib.wasm を、ランタイムにはWasmer 3.0.2を選びました。
また、Wasmerは通常だとJITコンパイルによりWebAssemblyを実行しますが、事前にネイティブコード wasmu へ変換(AoTコンパイル)しておくことで、JIT非対応の環境に対応させることもできます。これについても、比較対象に加えることとします。

結果は以下のようになりました。

  • (再掲)Rust v1.65.0 ( -C opt-level=3 -C debug_assertions=no -C lto=yes )
    • Time (mean ± σ): 853.2 ms ± 1.3 ms [User: 843.1 ms, System: 2.1 ms]
  • Wasmer 3.0.2 ( JIT, Craneliftコンパイラ, fib.wasm を実行 )
    • Time (mean ± σ): 948.0 ms ± 3.3 ms [User: 932.9 ms, System: 3.8 ms]
  • Wasmer 3.0.2 ( AoT, Craneliftコンパイラ, wasmer compile fib.wasm -o fib.wasmu で得られた fib.wasmu を実行 )
    • Time (mean ± σ): 939.9 ms ± 4.7 ms [User: 929.0 ms, System: 4.1 ms]

AoTコンパイルを行った fib.wasmu の方が高速なのは自明ですが、ほぼ誤差です。いずれにせよ、LuaJITに対しおよそ半分ほどの処理時間で済んでいることが分かります[17]

WASIにおける言語選択の自由度

ここまで機能や性能について見てきましたが、WASIにはさらに利点があります。
WASIは既に述べた通り規格でしかありません。つまり、言語がWASIに対応さえしていれば、どの言語で記述しても同じ機能を持つWASIを生成できるのです。

例えば、先ほどのRustのフィボナッチのコードは、以下のGolangコードへ簡単に置き換えることができます。

fib(42)を求めるGolangのコード
package main

import (
	"fmt"
)

func fib(n int32) int32 {
	switch n {
	case 0:
		return 0
	case 1:
		return 1
	default:
		return fib(n-2) + fib(n-1)
	}
}

const N int32 = 42

func main() {
	fmt.Printf("fib(%d) is %d\n", N, fib(N))
}

このコードをTinyGoでWASIのwasmとして出力して実行してみましょう。

$ tinygo build -wasm-abi=generic -target=wasi -o fib.wasm fib.go
$ wasmer run fib.wasm
fib(42) is 267914296
$ time wasmer run fib.wasm > /dev/null  
wasmer run fib.wasm > /dev/null  0.94s user 0.01s system 88% cpu 1.075 total

処理時間もさほど変動せず、結果も一致しています。ランタイムを維持したままロジックの言語・実装だけを差し替えることができました。

Unityへの組み込み

ここまで、WASIのメリットについて説明しました。では、ゲームで使うことを想定して実際にUnityへと組み込んでみましょう。

ただし、先に示したフィボナッチ数を求めるコードでは視覚的な効果が得られないため、Unityに移植しても面白くありません。そこで、マンデルブロ集合[18]のPBM画像をUnityのC#とは別に切り出したロジックで生成することにします。

WASIもLua同様に様々な環境で組み込み・実行できることを示すため、実行対象のプラットフォームは以下の通り複数選定しました[19]

  • Unity Editor (M1 Mac, Apple Silicon版)
  • Android (ARM64)
  • iOS (ARM64)

WASI (WebAssembly)コードの生成

動作確認と比較のために、LuaとRustのコードをそれぞれ用意します。実装の正確性についての議論を避けるため、Debianが提供しているBenchmark Games[20]のコードを借用しました。ただ、実装内容・アプローチに差がみられることから、厳密な速度比較は諦めることにします。

また、両者について組み込みの際に不都合が生じたので、それぞれ軽微な変更を加えました。以下にコードと変更点を示します。

  • Benchmarks Gameの mandelbrot Lua #3 [21]から借用し、xLuaで扱うことを前提に変数へ結果を格納するよう修正。3条項BSDライセンス[22]

https://github.com/flfymoss/202212-ac-wasi-sample/blob/release/mandelbrot/lua/main.lua

  • Benchmarks Gameの mandelbrot Rust #6 [23]から借用し、マルチスレッド対応を削除。3条項BSDライセンス[22:1]

https://github.com/flfymoss/202212-ac-wasi-sample/blob/release/mandelbrot/rust/src/main.rs

このうち、Rustのコードは以下のコマンドで wasm へとコンパイルしておきます。

$ rustc -C opt-level=3 -C debug_assertions=no -C lto=yes --target wasm32-wasi mandelbrot.rs -o mandelbrot.wasm
# または、Cargo用プロジェクトを作成した状態で
$ cargo build --release --target wasm32-wasi

さて、ここまででWASIの wasm ファイルの準備ができました。しかし、これはバイトコードに過ぎないため、実行するには機械語へのコンパイルが必要です。
とはいえ、WasmerはデフォルトでCraneliftと呼ばれるコンパイラを内蔵しているので、 wasm ファイルをJITコンパイルによりそのまま実行することができます。
ただし、ゲームを実行するプラットフォームによっては、セキュリティ上の都合などからJITコンパイルに対応していないこともしばしばです。例えば、iOSはJITコンパイルを認めていません。つまり、AoTコンパイルが必要になります。

折角ですから、今回はiOS対応も兼ねて、AoTコンパイルしたネイティブコードである wasmu ファイルも生成してみましょう。WasmerのAPIを利用し、 wasmu の生成を目指します。

Cargoプロジェクト全体はサンプルリポジトリ[27]を参照いただくとして、具体的にコンパイラAPIを呼び出すコードは以下のようになります。

https://github.com/flfymoss/202212-ac-wasi-sample/blob/release/compiler/src/main.rs

Cargoプロジェクトが準備できたら、以下のコマンドでビルドしましょう。

# プロジェクトのディレクトリで実行
$ cargo build --release

ビルドしてできたコンパイラを用いて、以下のように wasmwasmu へとコンパイルします。
生成された wasmu ファイルは、iOSはもちろん、JITコンパイルに対応した環境でも実行することができます。

$ ./target/release/my-compiler ../mandelbrot.wasm wasmu/mandelbrot
$ ls wasmu
mandelbrot.android.wasmu	mandelbrot.arm64.wasmu		mandelbrot.ios.wasmu

これで、JITをサポートしないiOSのような環境でもWASIを動作させる準備が整いました。

Wasmer(WASIランタイム)の組み込み

Unityで wasmwasmu を実行させるため、Wasmerをネイティブプラグインとして組み込みます。
WasmerはRustで書かれているので、Rustを使ってネイティブプラグインをビルドしてみましょう。Cargoプロジェクト全体はサンプルリポジトリ[28]を参照いただくとして、プラグインの実コードは以下のようになります。

https://github.com/flfymoss/202212-ac-wasi-sample/blob/release/loader/src/lib.rs

Cargoプロジェクトが準備できたら、以下のコマンドでビルドしましょう。ビルドにはTripleの指定が必要です。

ビルドするためのコマンド
# プロジェクトのディレクトリで実行
# wasmu用 Wasmer(headless)
# M1 Mac 向けプラグインを作成
$ cargo build --release --lib
# Android (ARM64) 向けプラグインを作成
$ cargo build --release --lib --target aarch64-linux-android
# iOS (ARM64) 向けプラグインを作成
$ cargo build --release --lib --target aarch64-apple-ios
# wasm用 Wasmer(Cranelift)
# M1 Mac 向けプラグインを作成
cargo build --release --lib --features wasi_jit --target-dir target_jit
# Android (ARM64) 向けプラグインを作成
cargo build --release --lib --features wasi_jit --target-dir target_jit --target aarch64-linux-android
# iOS はJIT非対応なので割愛

ビルドが完了したら、後述するUnityプロジェクトの Assets/Plugins/(アーキテクチャ名) 配下にビルドしたプラグインのファイルを配置する必要があります。
例えば、M1 Mac, Android, iOS向けのファイルの配置は以下のようになります。

バイナリの配置
  • Assets/Plugins
    • Android
      • libs
        • arm64-v8a
          • libloader.so
          • libloader_jit.so
    • arm64
      • libloader.dylib
      • libloader_jit.dylib
    • iOS
      • libloader.a

WASI (WebAssembly)の実行用C#コードの実装

WASIを扱う部分は完成したので、Unityからそれを実行する部分を実装しましょう。
Unityは2022年12月12日時点で最新のLTSである、2021.3.15f (Apple Silicon版)を利用しました。

比較が分かりやすいように、実行時間を計測して表示する機能も入れておきます。Unityプロジェクト全体はサンプルリポジトリを参照いただくとして[29]、C#のコードは以下の通りです。

https://github.com/flfymoss/202212-ac-wasi-sample/blob/release/unity/mandelbrot/Assets/MandelbrotController.cs

コード中にもあるように、 Assets/StreamingAssets 以下にここまでの節で作成した .lua, .wasm, .wasmu ファイルの格納が必要です。配置先ディレクトリをコードに沿って適宜作成の上、ファイルをコピーしてください。

また、比較用のLuaの実行のためにxLua v2.1.16を用いています。xLuaのリリースページ[30]からダウンロードの上、ドキュメントに従って組み込んでください。

各プラットフォームで実行してみる

ようやく実行できるようになったので、早速試してみましょう。
対象とするプラットフォームは Unityへの組み込み の節で示した通り、Unity Editor (M1 Mac, Apple Silicon版)、Android (ARM64)、iOS(ARM64)です。

比較用に以下のランタイムを組み込みます。

  • xLua v2.1.16 (LuaJIT)
  • Wasmer 3.0.2 ( wasmu を実行)
  • Wasmer 3.0.2 + Cranelift ( wasm を実行) [31]

生成するマンデルブロ集合の画像は、正方形画像の一辺N(px)を以下3パターンで設定し、それぞれ3回ずつ生成した際の平均実行時間を測定します。

  • N = 120
  • N = 1000
  • N = 4000

Unity Editor

まず、Unity Editorで試します。以下の動画は実行した結果を2.5倍速にしたものです。

実行に要した平均時間は以下のようになります。

  • N = 120
    • xLua : 15.7ms
    • Wasmer (wasmu) : 6.7ms
    • Wasmer (wasm) : 40.3ms
  • N = 1000
    • xLua : 788.0ms
    • Wasmer (wasmu) : 23.0ms
    • Wasmer (wasm) : 49.3ms
  • N = 4000
    • xLua : 12547.3ms
    • Wasmer (wasmu) : 327.3ms
    • Wasmer (wasm) : 344.3ms

xLuaがかなり遅いですが、これはM1 Mac用のxLuaバイナリ(LuaJIT)でおそらくJITが有効になっていないことが原因と考えられます。
JITコンパイルの wasm は N = 120 のような小さな処理には向きませんが、 N = 4000 のように重い処理では wasmu に近い速度で処理ができていることが分かります。

Android (ARM64 エミュレータ)

次に、Androidで試します。今回はAndroid 13のエミュレータ上で実行しました。以下の動画は実行した結果を2.5倍速にしたものです。

実行に要した平均時間は以下のようになります。

  • N = 120
    • xLua : 4.7ms
    • Wasmer (wasmu) : 14.3ms
    • Wasmer (wasm) : 50.0ms
  • N = 1000
    • xLua : 100.3ms
    • Wasmer (wasmu) : 27.0ms
    • Wasmer (wasm) : 51.7ms
  • N = 4000
    • xLua : 1402.0ms
    • Wasmer (wasmu) : 346.3ms
    • Wasmer (wasm) : 367.3ms

Unity Editorでの結果と違いxLuaが明らかに高速なため、JIT有効のLuaJITが利用されているようです。
Nが小さい時は wasm よりもxLuaが優位ですが、N が上がるにつれて wasm が逆転し、 N = 4000 ではおよそ3倍ほど wasm の方が高速になることが示されました。

iOS (iPhone SE 2)

最後に、iOSで試します。こちらはiPhone SE 2 (iOS 15.3.1) の実機上で実行しました。また、iOS ではJITコンパイルが認められていないため、JIT無効のxLua (LuaJIT)と wasmu のみで比較します。
以下の動画は実行した結果を2.5倍速にしたものです。

実行に要した平均時間は以下のようになります。

  • N = 120
    • xLua : 44.3ms
    • Wasmer (wasmu) : 17.7ms
  • N = 1000
    • xLua : 1047.3ms
    • Wasmer (wasmu) : 75.3ms
  • N = 4000
    • xLua : 16067.3ms
    • Wasmer (wasmu) : 497.7ms

ネイティブの wasmu とJIT無効のインタプリタでの速度差が出るのは自明ですが、JIT禁止の環境であれば、WASIが圧倒的に有利であることが分かります。

結果の考察

これら3プラットフォームの結果から、以下のことが分かりました。

  • wasm は短時間で終わる処理に不向きで、その場合xLua (LuaJIT)の方が速い
    • 処理が重くなるにつれて逆転し、 wasmu と同程度の速度まで漸近する
    • 従って、JITコンパイルがボトルネックになっているものとみられる
  • wasmu は全プラットフォーム、全パターンを通して極めて高速
    • ただし、JIT有効の環境かつ短時間で終わる処理については、LuaJITに負けている点に注意が必要
      • Wasmerの初期化コストがボトルネックになっていると考えられる
  • xLuaでLuaJITを用いる場合、プラットフォームによって速度が明らかに異なる
    • JIT有効の環境であれば、悪い選択肢ではない
    • JIT無効の環境では、短時間で終わる処理以外全く向かない

ある程度重い処理を切り出して実行する場合は、WASIを選択した際のメリットが大きいと言えます。JIT無効の環境では事実上の無双状態です。Unity EditorやiOSの結果において、xLuaと wasmu の差は30倍以上、実測で10秒以上も差が生まれている点は特筆すべき点でしょう。
一方で軽い処理しか切り出して実行しない場合、WASIによる高速化はわずかで、コンパイルの手間を踏まえるとメリットが大きいとまでは言えないでしょう。JIT有効かつN = 120の環境下ではLuaJITの方が wasmu よりも高速である点も注意が必要です。

ブラウザ上での実行

ここまで、WASIとして切り出したロジックをUnityを使う例を見てきました。
ですが、既に述べた通り、WASIはブラウザでも実行することができます。ブラウザゲームで効果を発揮することはもちろん、ゲーム開発用の管理画面などでロジックを直接実行し、素早くデバッグ・調整を行うことも可能になります。

マルチプラットフォーム対応の大詰めとして、マンデルブロ集合の wasm をブラウザ上でも実行できるようにしてみます。コードはHTMLやJavaScriptに散らばっているので、サンプルリポジトリ[32]を参照ください。

コードを用意したら、同じディレクトリ内に mandelbrot.wasm をコピーしておきます。完了したら、以下のコマンドで開発用のローカルサーバーを立ち上げてみましょう[33]

# Node.jsがインストールされていることが前提。なければ適宜インストールする
$ npm install
$ npx vite
  VITE v3.2.4  ready in 106 ms

  ➜  Local:   http://127.0.0.1:5173/
  ➜  Network: use --host to expose

Localの部分のURLにアクセスするとマンデルブロ集合の生成ページが表示されるはずです。Google Chrome[34]で閲覧し、Unityと同様に実行した例を以下に示します。

ブラウザの実行エンジンで動作するためUnityの結果とは単純比較できませんが、実行時間の平均を以下に示します。

  • N = 120
    • 14.7ms
  • N = 1000
    • 216.0ms
  • N = 4000
    • 3140.7ms

Unityでの計測結果よりかなり遅いものの[35]、JIT無効のxLua (LuaJIT)よりは高速に動作していることが分かります。

課題・懸念点

さて、ここまではWASIの強みや動作例を示してきましたが、もちろん弱点も存在します。

まず、WASIには十分な実績がありません。WebAssembly自体はかなり広まりましたが、WASIは広がっていると言い難い状況です。
また、機能面での不足が多数あります。スレッド対応などの重要な機能すら仕様策定中であることは、WASIの大きな弱点でしょう[36]。ただし、例えばWasmerでは策定中の機能らを実装した標準ライブラリ (WASIX libc[37])が提案されていたりと、改善の兆しは見えます。

最後に、Luaとは異なり、 wasm へのコンパイルが最低一回は必要です。コンパイル時間は言語に依存しますが、規模が大きなコードをコンパイルする場合は時間がかかるかもしれません。
ただし、AoT/JITコンパイルはCraneliftを使う限り高速であり、さほど問題にはならなさそうに見えます。JIT Bomb[38]対策としてSinglepassコンパイラも提供されているので、 wasmu へのコンパイル時間が問題になることは少ないでしょう[39]

まとめ

本記事では、まず、ゲームのロジックの切り出しにLuaが用いられる背景を説明しました。
次に、Luaの弱点を示し、代替としてWASI (WebAssembly)の利用を提案しました。また、各プラットフォームにおけるUnity上での Lua (xLua) と WASI (Wasmer) の動作比較から、Luaの弱点をWASIが一定克服できることを示しました。
最後に、WASIは発展途上であり十分な実績がないこと、仕様として不足している機能があること、Luaと比較しコンパイルの手間があることを説明しました。

WASIは発展途上の技術ですが、仕様からランタイム、言語の対応まで、多くの関連技術が日々進化を続けています。今後のさらなる発展にも期待が持てるでしょう。

謝辞

本記事を執筆するにあたり、以下の方にご協力いただきました。この場をお借りして厚くお礼申し上げます。

  • @cllightz さん
    • iOSでの動作検証に際して、助言・確認をいただきました。ありがとうございました。
  • 🍊
    • 内容の添削をしていただきました。ありがとうございました。

ライセンス

本記事の内容は、特記なき限りCreative Commons Attribution 4.0 International Public License[40]のもとで自由に利用することができます。ただし、別のライセンスが示されている部分についてはそちらに従ってください。
また、著者ウェブサイトにて同一の内容[41]を掲載しています。



脚注
  1. https://github.com/flfymoss/202212-ac-wasi-sample ↩︎

  2. https://www.unrealengine.com/ ↩︎

  3. https://unity.com/ ↩︎

  4. https://learn.microsoft.com/ja-jp/dotnet/csharp/ ↩︎

  5. https://www.lua.org/ ↩︎

  6. https://www.rust-lang.org/ ↩︎

  7. MacBook Pro(16インチ、2021), macOS Monterey 12.6.1 ↩︎

  8. https://github.com/sharkdp/hyperfine ↩︎

  9. https://luajit.org/ ↩︎

  10. https://wasi.dev/ ↩︎

  11. https://webassembly.org/ ↩︎

  12. https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap01.html ↩︎

  13. https://wasmer.io/ ↩︎

  14. https://wasmtime.dev/ ↩︎

  15. https://github.com/wasmerio/wasmer ↩︎

  16. https://github.com/wasmerio/wasmer-js ↩︎

  17. 今回はCraneliftと呼ばれるWasmerデフォルトのコンパイラを用いましたが、WasmerはLLVMにも対応しており、より高速なコードを生成可能です。ただし、LLVMはコード生成にかなりの時間を要するため、Luaとの比較には相応しくないと判断し本記事からは除外しています。 ↩︎

  18. https://www1.econ.hit-u.ac.jp/kawahira/courses/mandel.pdf ↩︎

  19. x86_64での環境を今回は用意できませんでしたが、理論上は動作すると思われます(未検証)。 ↩︎

  20. https://benchmarksgame-team.pages.debian.net/benchmarksgame/index.html ↩︎

  21. https://benchmarksgame-team.pages.debian.net/benchmarksgame/program/mandelbrot-lua-3.html ↩︎

  22. https://benchmarksgame-team.pages.debian.net/benchmarksgame/license.html ↩︎ ↩︎

  23. https://benchmarksgame-team.pages.debian.net/benchmarksgame/program/mandelbrot-rust-6.html ↩︎

  24. 厳密には、巨大な領域を確保しようとしてしまい、iOSにおいて Cannot allocate memory となります。 ↩︎

  25. https://github.com/wasmerio/wasmer/blob/5287c4f1253f66f428066d35eef0825fe827cff3/lib/api/src/sys/tunables.rs#L45 ↩︎

  26. https://github.com/wasmerio/wasmer/blob/5287c4f1253f66f428066d35eef0825fe827cff3/lib/vm/src/memory.rs#L222-L228 ↩︎

  27. https://github.com/flfymoss/202212-ac-wasi-sample/tree/release/compiler ↩︎

  28. https://github.com/flfymoss/202212-ac-wasi-sample/blob/release/loader ↩︎

  29. https://github.com/flfymoss/202212-ac-wasi-sample/tree/release/unity/mandelbrot ↩︎

  30. https://github.com/Tencent/xLua/releases ↩︎

  31. JITコンパイルが認められていないiOSでは除外しています。 ↩︎

  32. https://github.com/flfymoss/202212-ac-wasi-sample/tree/release/mandelbrot/browser ↩︎

  33. 今回はプレビュー用にローカルサーバーを利用しましたが、ビルドして静的ファイルにしてしまえば、ローカルでのサーバー実行は不要になります。 ↩︎

  34. https://www.google.com/intl/ja_jp/chrome/ 。閲覧時のバージョンは 108.0.5359.98 。 ↩︎

  35. wasm の実行エンジンや描画コード等が異なるため、Unityでの結果との比較を単純に行うことはできません。高速化する余地が残されている可能性があります。 ↩︎

  36. https://github.com/WebAssembly/WASI/blob/main/Proposals.md ↩︎

  37. https://github.com/wasmerio/wasix-libc ↩︎

  38. https://logmi.jp/tech/articles/325635 ↩︎

  39. https://github.com/wasmerio/wasmer/tree/master/lib/compiler-singlepass ↩︎

  40. https://creativecommons.org/licenses/by/4.0/legalcode.ja ↩︎

  41. https://flfymoss.com/posts/2022-12-unity-meets-wasi/ ↩︎

Discussion