前編 に続き後編では Udon 2 がどのようなものになるのかの技術詳細の予想と解説をします。
後編の内容は VRChat が動いている仕組みの詳細が気になる人向けのものです。 ワールドやギミックを作製するのにはこれらを理解する必要はありません。
目次:
Udon 2 実行時構成予想図
最初に Udon 2 の実行時の構成の予想図を示します。 この内容は Developer Update 記事の後の質疑などの情報を総合して独自に書き出してみたものです。 それでも、Merlin さんに下書きを見てもらったところ thumbs-up の絵文字をもらったので、少なくとも大筋では的を射ていると思います。
図の全体は階層構造をなしています。 依存するモジュールが上になるようにして OS が一番下に、ユーザ(つまりワールド作成者)が書いたスクリプトが一番上になっています。 下から「VRChat Client classes」「Scene objects, components, etc.」までの階層は現状と同じです。
時間が無い人向けの要点説明
解説は不要という人向けに、まずはコンパクトに上記の図の要点を示します。
- ユーザーが書いたスクリプトはビルド時に概ね普通の C# としてコンパイルされて IL になって world asset bundle に入る。
- ユーザスクリプトの実行時環境は Wasm 環境上で動く Mono である
- ユーザスクリプトは IL のままロードされて interpret される。この IL インタプリタ自身も前項の環境の一部として Wasm の上で動く
- ユーザスクリプトに対しての .NET API は(Wasm 上のものとして Mono が提供する形で)おそらくほぼそのまま提供される
- ただし例えばファイル IO などは使えない。API がアクセス可能だったとしても実行時エラーになる。
- Unity と VRChat の API については独自のラッパーが用意される。
- ユーザスクリプトが動いている Wasm の内側にある .NET ランタイムと、VRChat プログラム本体が動いている .NET ランタイムとはまったく個別である。
- Wasm の実行環境は Wasmer をベースにしたものを使う予定
要素解説
WebAssembly (Wasm) の採用
Udon 2 は WebAssemblyというものを利用してユーザスクリプトを実行します。 以下では略称の Wasm(ワズム)と表記します。
Wasm の概要を知るには MDN の説明 (日本語) が分かりやすいと思います。手短に抜粋すると、 Wasm は最近のウェブブラウザーで実行できる新しい種類のコードで、 C/C++, C#, Rust など様々なプログラミング言語で記述されたものをウェブ上で、 ネイティブに近い速度で実行出来るようにすることで、 以前では実現できなかったウェブ上で動作するクライアントアプリケーションを実現する方法を提供するものです。
Wasm は元々は Web ブラウザ上の技術として始まったものですが、現在は他の応用も広がってきています。 Udon 2 でもそのような使い方になります(つまり名前 WebAssembly に「Web」という語がありますが Web は実質的に関係はありません)。
Udon 2 で Wasm 技術を採用する理由は
とのことです。 (参考: https://ask.vrchat.com/t/developer-update-15-december-2022/15518/19 および https://ask.vrchat.com/t/developer-update-15-december-2022/15518/73 )
補足1:「サンドボックス」は、プログラムを隔離された環境の中に置いて、その外に害を及ぼすことがないようにしながら安全に実行するメカニズムのことです。 VRChat では悪意を持った人がワールドを作るかもしれないことと、 (要は同じことですが)ユーザスクリプトによって VRChat クライアントそのものを破壊・改変されないようにするために、 サンドボックスの機構が Udon には必要になります。
補足2:「ツールチェイン」は、プログラムを作成する際のツール一揃いです。 プログラム作成では「あるツールの出力を次のツールの入力にして順々にプログラムを加工する」ということがよくあり、この様子をチェーンに例えてこう呼んでいます。 Wasm 技術を採用することで、Wasm のために作られた巷のツールを VRChat コンテンツ作成に使える道が開けます。 VRChat 社やツール類を作成をするユーザが自前でツールを作る必要が無く、その分の労力を減らせると期待できます。
ユーザスクリプトの .NET 実行環境
ユーザが書いた C# スクリプトを動かす方法はいくつか考えられますが Udon 2 の初期のリリースでは、 IL のままインタプリタで実行する形を予定しているそうです。
補足: ここでの IL (Intermediate Language) は C# など .NET 環境に対応した言語をコンパイルした結果の形式のことです。 CIL(Common Intermediate Language)と呼ばれることもあります。 参考ページ: https://learn.microsoft.com/ja-jp/dotnet/standard/managed-code
ここでのインタプリタは Wasm 実行環境の中で動作するものです。 これは Mono が実装しているものを使うようです。 (参考記事 https://www.mono-project.com/news/2017/08/09/hello-webassembly/ これは Mono が Wasm 環境で実行出来るようになったことを伝える記事です。 説明されている二つの実行方式の二番目の方 「The second prototype compiles the Mono C runtime into web assembly, and then uses Mono’s IL interpreter to run managed code.」 を使うようです)
Developer Update 記事本文中の「run within VRChat using the same runtime as Blazor」 および図中の「Mono-wasm Runtime used by Blazor」と書いてある部分がこれを指しているようです。
「Blazor」は Microsoft が開発を進めている、C# で Web アプリケーションを書けるようにするオープンソースのフレームワークです。 Web アプリケーションを作るためのものなので Udon 2 には直接には関係はありません。 ここで名前の言及があるのは単に「Blazor がやっているのと同じように IL を Wasm 上の Mono で動かす」 ということが言いたいだけのようです。
(蛇足: この Mono の .NET 環境は、 元々 C 言語で実装されている .NET 実行処理系をまるっと Wasm バイナリにコンパイルしたもののようです。 始めて見た時は処理系丸々なのかと驚きました。とはいえブラウザで実行するなら、そうするしかないので必然ではあります。 使うソースは dotnet に取り込まれた Mono の Wasm 版ランタイムらしいこの辺でしょうか(?) https://github.com/dotnet/runtime/tree/main/src/mono/wasm )
VRChat と Unity の API
.NET の API については前述の Mono が提供する実行環境に含まれます。 一方 VRChat と Unity の API については Udon 2 の機構が提供します。
これら API で操作したい物は実際には Wasm VM の外側にあります。 したがってユーザスクリプトから見えるクラスやオブジェクトは本物のそれらではなく、 本物へ接続するように作られていて本物のように動作する代わりのものです。 Developer Update 記事中では構成を示す図の中で「VRChat/Unity Wrapper」と表現されています (本文中に言及はありません)。
補足: wrapper(ラッパー)は、 何かの機能を何らかの理由で直接は使わずにそれを使うもう一つ別のものを介して使うようにするプログラミング手法およびそのように作られたものを指します。 元のものを包んでいて直接は触られないようにしているように見えるのでこう呼ばれます。 (参考 Wrapper function (Wikipedia en) )
この「VRChat/Unity Wrapper」は、現在の Udon が行っているのと同様のアクセス制限を実装します。 ワールド作成者には触らせたくないオブジェクトやコンポーネントを取得すると null になってしまうとか、 使わせたくないメソッドを使おうとするとエラーになる、といったことを行います。
ラッパーがどのように実装されるのかは明らかではありませんが、 Wasm VM の内側と外側を繋ぐので内側と外側にそれぞれ何らかの機構が置かれると思われます。 この記事冒頭の構成予想図では、内側を「Wrapper objects」外側を「Wrapper implementations」の箱で示しました。
Marshaling の必要性
例えばユーザスクリプトが Unity コンポーネントのアトリビュートを書き換えた場合、 ラッパーはその書き込み動作として Wasm VM の外側にある本物のコンポーネントに対してその値を書き込む動作を行います。 また逆にコンポーネントから値を読みだしたならば、ラッパーは本物のコンポーネントから値を取り出してきます。 ヒエラルキーをたどって GameObject を取得したら、Wasm VM の中にその GameObject であるかのように見えるラッパーのオブジェクトを生成します。
このようにラッパーは必要に応じて Wasm VM の内側と外側の間でデータのコピーを行います。 これは従来の Udon VM では必要のなかったものです。この部分は動作を遅くさせる要因で、 これを頻繁に行う必要のあるプログラムはあまり速度が上がらないかもしれないとのコメントがありました (https://ask.vrchat.com/t/developer-update-15-december-2022/15518/31)。
参考:このような、システム間でオブジェクトをシステム間で透過的に扱えるようにする過程を、 プログラミング用語で マーシャリング ) と言います。
分離された .NET 環境によるサンドボックス
以上で冒頭の構成予想図中の要素は説明出来たかと思います。 Udon 2 ではユーザスクリプトが動く環境は Wasm ランタイムの中にある .NET 環境であり、 VRChat クライアント本体が動いている .NET ランタイムとは完全に切り離されているというのが重要です。
悪意のある(あるいは不具合のある)ユーザスクリプトが何かを壊そうとしても、 せいぜい壊せるのは内側の .NET 環境だけであり、外側で動いている VRChat プログラム本体には害を及ぼせません (もちろん Wasm ランタイムとラッパーが正しく実装されていれば、という前提がありますが)。
これにより「サンドボックスによる障壁を設けつつ、C# の言語機能は可能な限りすべて提供する」という目的が実現されることになります。
構成図の読み解き
Developer Update 記事に掲載されていた構成図をあらためて見てみます。
右側の実行時環境「Udon 2 Client Runtime」の要素ははっきりしました (なお矢印はミスリーディングなので無視してよいと思います。 書くなら逆にして、情報の流れの向きの意味にするのが妥当かと思います)。 改めて右下から見てみます:
- 「User C# Script Assemblies」はワールド作成者が作るプログラムで CLI で言う所のアセンブリ) でプログラムは IL 形式になっています。
- 「C# Core Libraries」は C# のシステムのライブラリ(正確には .NET Standard?)でしょう。
- 「VRChat/Unity Wrapper」は「VRChat と Unity API」の所で述べたラッパーです。
- 「Mono-wasm Runtime used by Blazor」は Wasm バイナリになっている Mono プロジェクト作成の .NET ランタイムです。 「used by Blazor」は「ユーザスクリプトの .NET 実行環境」の所で述べたように Blazor が使っているのと同じものだよ、ぐらいの意味でしょう。
- 「Wasm VM」は Wasm の実行環境です。具体的には Wasmer を今は使うつもりであるとのコメントがありました。 https://ask.vrchat.com/t/developer-update-15-december-2022/15518/73
図の左側「Udon 2 SDK」はワールドのビルド過程を示しています (ユーザスクリプトは SDK に含まれないので不正確な表記になっていますが)。 矢印はビルドにおける情報の流れる方向を示しています。
- 「Graph Scripts」は Udon Graph として作られたユーザのスクリプトを意味しています。
- 「Graph Transpiler」は Udon Graph を C# スクリプトに変換する、SDK に内蔵されるツールを表しています。
- 「U# Scripts」は UdonSharp を使って作られたユーザのスクリプトです。
- 「C# Scripts」は Graph Transpiler が出力した C# スクリプトと U# Scripts です。U# Scripts は C# のサブセットになっていて変換の必要は無いはずなので、単に「これで両方とも C# です」と言いたいだけなのだと思います。
- 「Roslyn」は C# コンパイラを含む .NET 向け言語処理系です。これは現状の U# にも使われています。 https://github.com/dotnet/roslyn
図の左側全体としては、ビルドした結果ユーザスクリプトは IL の形になってワールドのアセットに含まれることを示しています。
移行図の読み解き
次に移行計画を示す図を見ていきます。
「Phase 1」は現在の状態です。 「Udon Graph」として書かれているユーザスクリプトはワールド SDK に含まれている「Graph Compiler」で処理して Udon Assembly になります。 UdonSharp (U#) を利用して書かれているものは UdonSharp のコンパイラが処理して Udon Assembly になります (「SharpCompiler」という語はこれまで使われてなかったように思いますが、素直に解釈して現状の UdonSharp のコンパイラでしょう)。 実行時には「Udon Assembly VM」が Udon Assembly を読み込んで実行します。
「Phase 2」は Udon Graph を C# に変換するものを導入した段階で一旦リリースするという計画を示しています。 Phase 2 で「Udon Graph」は「Graph Compiler」で C# (正確に言えば U# が扱える範囲の C# での書き方)に変換され、 その後は「Phase 1」での U# と同じように扱われます。 (図中の「Graph Compiler」は前節の図中「Graph Transpiler」と同じものでしょう。 この図を書いた人は細かいことは気にしていないと思われます。) この時点で「Custom Graph Nodes」(Udon Graph で使うノードを U# スクリプトで増やすことのできる新機能)が実現されます。 ここで一旦リリースするのは、まずは「Udon Graph」の扱いが移行に伴って壊れないことを確保するためなのでしょう。
「Phase 3」は Wasm 技術ベースの実行環境へ移行した形を示しています。 ここでまた「SharpCompiler」という名前が出てきていますが、ここでは IL を出力するので Phase 1 と 2 のものとは別物のはずです。 U# であった制約は取り払われて可能な限り“普通の C# コンパイラ”になっているはずです。 その次の「Udon Wasm VM」は、「構成図の読み解き」で見た右側の実行時環境をざっくりと一つの箱にしたものです。
「Phase 3」になると既にアップロードされているワールドに入っている Udon Assembly をどう実行するかが課題になります。 文章では説明されていませんが、 現状の Udon Assembly VM を Wasm 上で動くようにして、それに Udon Assembly を読み込ませて実行することを計画しているそうです。 従来のアクセス制限機構と Udon 2 でアクセス制限を実現するラッパーは機能が重複するので、そのあたりは改変するのでしょう。 (詳しくは後ほど)
質疑など
Developer Update 記事の下のコメント欄で質疑応答が起きてたので要点を書き出してみます。
(概要理解を優先して適宜補足や要約をしているので、翻訳として正確なものではありません。 また読み易い様に順番も入れ替えています。)
UdonSharpBehaviour を継承しないクラスは作れるか?
質問意図: 従来の U# ではクラスは UdonSharpBehaviour を継承する必要があったが、そうでないクラスも作れるようになるのか?
回答: https://ask.vrchat.com/t/developer-update-15-december-2022/15518/14
作れる。
C# で書けるということは、Udon のことは気にしなくてよくなるってこと?
回答: https://ask.vrchat.com/t/developer-update-15-december-2022/15518/74
そうだ。Udon 特有の事は考えずに書けるようになるし、既存の C# ライブラリを使えるようになる。 ただし Udon は変数同期や他の相互作用(補足: OnPlayerJoined 等々の UdonBehavior で受けることのできるイベントを指していると思われ)を VRChat API を通して提供するので、それらを使いたい時は何らかの形で Udon を意識する必要がある。
補足: 別所で私も質問したところ、変数同期についてはまだ設計検討中ではあるが 今のところ現状の UdonBehavior に関連付けた方式を続ける方針とのことでした。
Udon 2 による速度向上はどのくらい?
質問: 素の C# と同じような速さで動作するの?
回答: https://ask.vrchat.com/t/developer-update-15-december-2022/15518/5
速度の目算はある?
回答: https://ask.vrchat.com/t/developer-update-15-december-2022/15518/31
状況に強く依存する。 速くなると期待するが、どのくらいなのかは言えない。 一般的には数倍、あるいはそれ以上は速くなるだろう。 一方遅くなるケースも見つかるだろう。 ラッパーでデータの変換(marshal)を頻繁に繰り返す必要のある場合にはそうなる。
補足:本文では marshaling の話は出てきませんでしたが、ここで言及されていました。
Wasm 導入の理由は?
回答: https://ask.vrchat.com/t/developer-update-15-december-2022/15518/19
今より速い実行速度を実現しつつ、現状と同じようなサンドボックスを構成できるから。 あと既存のツールチェインが使えて、作成するものを減らせるから。
従来 Udon と Udon 2 のスクリプトは連携できるの?
回答:https://ask.vrchat.com/t/developer-update-15-december-2022/15518/51
補足:実行時には Udon 2 しか無いので、 質問者が気にしていたような現行の Udon の実行環境と Udon 2 の実行環境としての連携は発生しない、という話。
従来 Udon はどう動くのか、もう少し詳しく
回答:https://ask.vrchat.com/t/developer-update-15-december-2022/15518/71
Udonアセンブリの縮小されたインタプリタが Udon 2 ランタイムの中で動く(補足:それが従来の Udon アセンブリを実行する)。 一部のコードは Udon Assembly から IL に変換されるかもしれない。
補足:「縮小された(reduced)」の意味合いが説明されていませんが私は、 「Udon 2 ランタイムの中で動くとなるとアクセス制限については機能が重複するので、 そこの改造などをするのだろう」と解釈しました。
JIT で動く可能性はある?
回答: https://ask.vrchat.com/t/developer-update-15-december-2022/15518/53
Mono ランタイムは JIT されて動く。 ユーザーコードに関しては AOT されて Wasm バイナリになれば JIT で動く。 そのような動作を組み込むのを検討したい。
解説: JIT は just-in-time compilation の事で、 Wasm のような中間コードを利用するシステムで、実行時に CPU が直接実行できる命令に変換することで高速にプログラムを動かす手法です。 (実行時コンパイラ(Wikipedia Ja))
AOT は ahead-of-time compilation の事で、 実行時ではなく事前に変換する手法、つまりは JIT ではない従来のコンパイルのことです。 (事前コンパイラ(Wikipedia Ja))
Wasmer は JIT 機能を備えるので Mono の Wasm バイナリになっている部分は JIT が効きます。 Mono が IL としてインタプリタで実行されるところは効きません。 ユーザーコードを IL から Wasm に事前にコンパイルすれば(AOT の処理をかければ)、 ユーザーコードにも JIT を効かせられるようになります。
Mono には IL を interpret するのでなく AOT 処理をして Wasm バイナリにする実行方法もあります ( https://www.mono-project.com/news/2018/01/16/mono-static-webassembly-compilation/ )。 ですが、 「Udon 2 の初期リリースでは AOT にかけるのは見送る。この方式を使うには開発する要素がインタプリタによる実行方式に比べて多くなるので」 という旨を Merlin さんから聞きました。
(回答文中の「partial AOT」というのは Mono の full AOT ではない AOT をさしているように思いますが、 詳しいことは分かりませんでした。 https://www.mono-project.com/docs/advanced/aot/#full-aot )
直接に IL を動かさないのはなぜ?
質問意図: Mono や .NET のサンドボックスやサーバサイドでのコード検証をすれば、 Wasm を採用するといった構成よりもっとシンプルに高速に動かせるのでは?
回答: https://ask.vrchat.com/t/developer-update-15-december-2022/15518/73
Mono と .NET の サンドボックスは廃止されている。 .NET は サンドボックスの改良を行っているが、いつ使えるようになるのか、また Udon にとってそれが十分なものかは不明だ。 また、コード検証はこれらでも使われていない。
(補足:端的な回答はここまでで済んでいますが、Wasm によるサンドボックスという設計を選んだ背景を引き続き説明してくれています。)
Mono (の実行環境)に直接依存するのも問題になる(ので避けたい)。 Mono がサポートしていない環境を VRChat がサポートしたくなるかもしれないので。
Wasm ランタイムには今の所 Wasmer を使うつもりだ。だが状況によっては将来変えるかもしれない。
この Udon 2 システムには IL のバリデーションは必要ないから無い。 ホワイトリストに載ってない Unity の機能は接続が生成されないから呼べない。 “安全でない”プログラムは書けるがそれでも Wasm VM のサンドボックスの中であるので、 悪意のある事をしようとしても最悪でも Wasm ランタイムの内側を壊す事しかできない。 IL のコード検証をしようとした場合“安全でない”コードは使えず、(それに伴って)多くの C# ライブラリが使えなくなってしまう。
解説: 質問者は 「ユーザスクリプトが害のある内容であるかを、ワールドがアップロードされた後にサーバで検証出来れば、 実行時にはそのまま IL を走らせることができるのではないだろうか。 サーバ処理ならば悪意のある者が回避することは出来ないし。 このようにすれば VM による速度ペナルティがかからないのでは。なぜそうしないのか。」 という事が聞きたかったようです。
Udon では、たとえばアバターの GameObject のような、VRChat のシステムが制御している要素はユーザスクリプトにはアクセスできないようにしています。 これには実行時のスクリプトの動作の確認が必要で、事前検証で安全性を確保することは出来ません。 何らかの実行時に介入する階層が必要になります。 例えばシーンのヒエラルキーをたどるプログラムがアクセス不可のところに到達するかどうかを事前検査では事実上判定できません。 判断を安全側に倒してそうなってしまうプログラムを事前検証で排除することは出来るかもしれませんが、 そうすると検査をパスできるプログラムが出来ることが狭くなりすぎてしまう、というお話です。
(この項の質問への答えは端的に言えば「無理です」だったわけですが、テクニカルなことを聞いてくれたので、 副産物的に Udon 2 の設計におけるバランス感覚のようなものが見えてきて興味深い質疑でした。)
その他(話題の落穂ひろい)
リフレクション使える?
「使える」だそうです(とある Discord での会話より)。
マルチスレッドサポート
スレッド関連には課題があるとのことで、マルチスレッドは扱えなさそうです。
(Wasm の仕様としてマルチスレッドがまだ定まっていないという段階にあり、 環境側の課題があるようです。Wasmer でのマルチスレッドサポート試作版はあるようですが。)
Phase2 の開発状況
前編でも触れましたが、 Udon Graph を C# に変換する機能はすでに実装し終わっているようです。 記事中の Custom Graph Nodes に掲載されている画像は実際に変換されたもののようにみえます。
Phase2 の範囲については SDK を使いやすいように調整するといった作業が残っているようです。
Wasm バイナリ
Udon 2 ではまずは IL をインタプリタ実行する構成が目指されています。 Wasm 技術を採用はするのですが Wasm バイナリはビルド時には現れないので、 SDK を使う側としては Wasm に関連して何か活用する(ないしは遊ぶ)ことは出来なさそうです。
しかし、ユーザスクリプトに AOT をかけて IL ではなく Wasm バイナリにして JIT を効かせるようにする、 というのは将来目標としては考えているようです。 その時になればユーザが Wasm バイナリを C# プログラミングではない他の方法で作って流し込むというのは可能になるのかもしれません。 その意味では記事中の「you will not be able to upload WebAssembly directly at launch」は 「初期リリースでは出来ないけど」と肯定的に捉えられるかもしれません。
感想など
雑談 U# の行方
U# はこれまであくまでもユーザコミュニティが作った付加的なものという扱いでした。 それが Phase 2 では公式に SDK に取り込まれ、Udon Graph に由来するユーザスクリプトをコンパイルするようになります。 そして Phase 3 では Udon Assembly を出力するコンパイラとしては役目を終えます。 同期周りのアトリビュートと UdonBehavior のクラスの仕様としては残りそうですが、 U# の成果物であるプログラムは実質的には破棄されるのでしょう。
U# は文法としては C# のサブセットであることを守り独自の文法拡張を行わない設計だったので、 ユーザが U# スクリプトとして書いたものは互換を保って残ることができるわけですが、 U# 自体は公式に取り込まれた後にある意味で消えてなくなるという道筋を選んだわけです。 自分が居なくなっても自分が育てた資産は残るわけで、ちょっとエモいなと思ったり。
ところで U# v1 に移行する時に、v0 からの互換を捨ててまでも C# の文法に忠実に従おうとしたわけですが、 今思えば Udon 2 のための準備だったのでしょうかね。
私的なまとめ
これで Udon 2 がどのようなものになるのかの全体像は見えてきたかと思います。
Udon 2 が目指すこと「サンドボックスを維持しつつ、普通の C# で書けるように。速度も出来るだけ向上させたい。」 設計上の制約「IL を直接実行は出来ない。実行時検証が必要で何らかの中間層が必要」 設計上の選択「既存のソフトを活用したい。だが特定の実装にロックインはしたくない」 といった事が把握できました。