Skip to content

ShirasUdonAssembly JSON v1 仕様書 ​

生成AIでテストJSONを作成する場合は、実践的な生成手順、検証項目、プロンプト雛形をまとめた AI向け作成ガイドも参照すること。

1. 概要 ​

ShirasUdonAssembly JSON は、ShirasUdonVM が実行するための中間表現である。

ShirasUdonAssembly JSON は UdonAssembly そのものを表現するものではない。

特に以下の実行時・生成時の詳細は JSON から排除する。

  • Udon の Heap アドレス
  • Udon の命令アドレス
  • UdonAssembly のラベル解決後のアドレス
  • ExternBridge の内部配置

ShirasUdonAssembly ではコードブロックにラベルを付け、ジャンプはラベルを使用する。

現在のランタイムでは、EXTERN の実行を次のコンポーネントに分離する。

text
ShirasUdonVM
    ↓ EXTERN要求
ShirasUdonExternBridge
    ↓ 引数・Signature・実行先を設定
ShirasUdonExternExecutor
    ↓
Udon EXTERN命令
  • ShirasUdonVM は ShirasUdonAssembly の命令、Stack、Heapを管理する。
  • ShirasUdonExternBridge は論理Extern Index、Executor bank、キャッシュおよび障害復旧を管理する。
  • ShirasUdonExternExecutor は実際のUdon EXTERN命令だけを実行する。

この分離はランタイムの実装詳細であり、ShirasUdonAssembly JSONの形式には含めない。

text
ShirasUdonAssembly JSON
        ↓
    ShirasUdonVM
        ↓
      実行

または

ShirasUdonAssembly JSON
        ↓
UdonAssembly Compiler
        ↓
   UdonAssembly

2. トップレベル構造 ​

json
{
  "version": 1,
  "data": [],
  "code": []
}
プロパティ型必須内容
versionNumber○JSON仕様のバージョン
dataArray○Heap初期値
codeArray○コードブロック

3. data ​

data は VM の Heap を初期化するためのデータを定義する。

json
{
  "name": "__value_0",
  "type": "SystemInt32",
  "value": 123
}

3.1 プロパティ ​

プロパティ型必須内容
nameString○Heap Symbol
typeString○実際に保持する型
valueJSON Value○初期値

4. JSON値とHeap値 ​

VRCJson では JSON の数値は TokenType.Double として読み込まれる。

したがって、例えば、

json
{
  "type": "SystemInt32",
  "value": 123
}

はロード時に、

text
JSON Double
    ↓
data.type == SystemInt32
    ↓
(int)123
    ↓
new DataToken((object)123)

と変換する。

現在想定する数値型:

text
SystemByte
SystemSByte
SystemInt16
SystemUInt16
SystemInt32
SystemUInt32
SystemInt64
SystemUInt64
SystemSingle
SystemDouble

Boolean、String についても JSON から読み込んだ値を new DataToken((object)value) として再ラップする。

これにより Heap 上の実値は Reference として扱える。


4.1 enum の表現 ​

ShirasUdonAssembly JSON の data で enum 型の初期値を定義する場合、value には enum の要素名ではなく、その基になる整数値を JSON Number として記述する。

json
{
  "name": "__ease_0",
  "type": "VRCSDK3ComponentsVRCTweenEase",
  "value": 1
}

上記の 1 は VRCSDK3ComponentsVRCTweenEase.Linear を表す。type には Udon の型名を記述し、value に "Linear" のような文字列を記述してはならない。

これは ShirasUdonAssembly JSON を生成する外部コンパイラの責務である。外部コンパイラは コンパイル元の enum 定数を、その enum の基になる数値へ正規化して value に出力する 必要がある。Flags enum の組み合わせも、要素名の配列ではなく合成後の整数値を出力する。

ShirasUdonVM はロード時に type と数値 value を使って、実際のUdon enum値へ変換する。 対応するenum型または数値がランタイムのenumテーブルに存在しない場合、ロードは失敗する。

この表現は標準のUdonAssemblyと異なる。標準のUdonAssemblyでは型付きHeap変数として enumを保持できるが、ShirasUdonAssembly JSONではenumオブジェクトをJSONへ直接格納せず、 型名と数値の組として表現する。したがって、標準UdonAssemblyからShirasUdonAssembly JSONを 生成するコンパイラも、enum初期値を数値へ変換しなければならない。

COPY では、コピー元とコピー先の宣言型に応じて次の変換を行う。

text
enum -> SystemInt32
SystemInt32 -> enum

これにより、次のような処理ではenumを一度 SystemInt32 として計算し、結果をenumへ 書き戻せる。

csharp
e = (EnumType)((int)e + 1);

ただし、外部コンパイラが生成する data のenum初期値については、常に前述の数値表現を 使用しなければならない。

4.2 SystemType と typeof(T) の表現 ​

SystemType の定数を定義する場合、value には CLR の完全修飾型名ではなく、 type や Extern シグネチャで使用する Udon型名を JSON String として記述する。

json
{
  "name": "__int_type",
  "type": "SystemType",
  "value": "SystemInt32"
}

上記は C# の typeof(int) に相当する。ShirasUdonVM はロード時に共有型テーブルを参照し、 "SystemInt32" を実際の System.Type オブジェクトへ変換して論理Heapへ格納する。

外部コンパイラは typeof(T) を、type: "SystemType" と、対象型のUdon型名を記述した 文字列 value の組として出力する。"System.Int32" や Assembly Qualified Name は使用しない。

SystemType のAPI戻り値スロットには、通常の参照型と同様に value: null を使用できる。 型テーブルに存在しないUdon型名が指定された場合、ロードは失敗する。標準Udonと同様に、 Udonへ公開されていないユーザー定義型の typeof はv1の対象外とする。

4.3 null と値型のデフォルト値 ​

data.value が null の場合、宣言された type に応じて次のように扱う。

text
参照型   -> null
値型     -> default(T)

値型の null は、値そのものをShirasUdonAssembly JSONへシリアライズするための表現では なく、APIの戻り値を後から格納するHeap Symbolの安全な初期値として使用する。

json
{
  "name": "__hit_0",
  "type": "UnityEngineRaycastHit",
  "value": null
}

ShirasUdonVMはロード時に、UdonでHeap変数として使用可能な各値型について事前生成された default(T) を取得し、boxed objectとして論理Heapへ格納する。これにより、APIの戻り値が 書き込まれる前にHeap Symbolが参照されても、値型引数へnullが渡されることを防ぐ。

コンストラクターが公開されていない値型について、任意の非デフォルト値をJSONから 復元する仕様はv1に含めない。そのような値はAPIの戻り値として取得して使用する。

enum初期値は4.1の数値表現を使用する。enumのAPI戻り値スロットを初期化する目的に限り、 value: null を default(EnumType) として使用できる。


5. Stack ​

ShirasUdonVM の Stack には、実値そのものではなく Heap Symbol を格納する。

例えば、

text
PUSH __value_0

を実行すると、

text
Stack
┌──────────────┐
│ "__value_0"  │
└──────────────┘

となる。

実際の値は Heap から取得する。

text
Stack
    │
    │ "__value_0"
    ▼
Heap
    │
    │ Reference → int 123
    ▼
123

したがって Stack の基本的な型は String である。


6. code ​

code はコードブロックの配列。

json
{
  "name": "_start",
  "export": true,
  "instructions": []
}

6.1 コードブロック ​

プロパティ型必須内容
nameString○コードブロックのラベル
exportBoolean○外部から呼び出し可能か
instructionsArray○命令列

すべてのコードブロックはラベルを持つ。

ShirasUdonAssembly JSON ではジャンプ先をアドレスではなくラベルで指定する。


7. export ​

export は、そのコードブロックを外部からエントリポイントとして実行できるかを表す。

json
{
  "name": "_start",
  "export": true,
  "instructions": []
}

Run("_start") のような外部実行では export: true が必要。

一方、

text
JUMP "_loop"

では export の値に関係なくジャンプできる。

_start を特別扱いする仕様ではない。


8. 命令 ​

v1 で現在使用している命令は以下。

text
PUSH
POP
COPY
EXTERN
JUMP
JUMP_INDIRECT
JUMP_IF_FALSE

8.1 PUSH ​

Heap Symbol を Stack へ積む。

json
{
  "op": "PUSH",
  "operand": "__value_0"
}

9. POP ​

Stack の最上位を破棄する。

json
{
  "op": "POP"
}

主に、不要な Extern 戻り値を破棄するために使用する。


10. COPY ​

Stack 上の 2 つの Heap Symbol 間で値をコピーする。

json
{
  "op": "COPY"
}

Stack:

text
[
  "__source",
  "__destination"
]

実行すると、

text
Heap["__destination"]
    =
Heap["__source"]

となり、Stack から 2 つの Symbol が取り除かれる。


11. EXTERN ​

Udon の Extern を実行する。

json
{
  "op": "EXTERN",
  "signature": "SystemInt32.__op_Addition__SystemInt32_SystemInt32__SystemInt32",
  "argumentCount": 2,
  "return": "__sum_0"
}

プロパティ ​

プロパティ型必須内容
opString○"EXTERN"
signatureString○Udon Extern Signature
argumentCountNumber○Udon EXTERN へPUSHする引数数。戻り値格納先は含まない
refArgumentsArray×実行後に書き戻す引数の0始まりインデックス。省略時は空配列
returnString条件付き非Void Signatureでは必須。Void Signatureでは指定不可

hasReturn と pushCount は存在しない ​

戻り値の有無はVMがロード時にSignatureから判断する。原則として末尾が __SystemVoidなら戻り値なし、それ以外なら戻り値ありとして扱う。

ただし、Udonの一部のプロパティsetter Signatureは末尾の__SystemVoidを省略する。 .__set_を含むSignatureは、末尾にかかわらず戻り値なしとして扱う。

json
{
  "signature": "UnityEngineDebug.__Log__SystemObject__SystemVoid",
  "argumentCount": 1
}

→ 戻り値なし。

json
{
  "signature": "SystemInt32.__TryParse__SystemString_SystemInt32Ref__SystemBoolean",
  "argumentCount": 2,
  "refArguments": [1],
  "return": "__result_0"
}

→ 戻り値あり。

非Void Signatureで return が省略されている場合、またはVoid Signatureで return が指定されている場合、ロードは失敗する。戻り値を破棄する場合も 上位コンパイラが一時Heap Symbolを return に指定し、直後に POP を生成する。


12. refArguments ​

Extern の引数について、実行後にHeapへ値を書き戻す必要がある引数の 0始まりインデックスを表す。C#の ref と out の両方を含む。

例えば、

text
SystemInt32.TryParse(
    string,
    ref int
)

なら、

json
"refArguments": [1]

となる。

インデックスは argumentCount 未満でなければならず、重複してはならない。 refArguments を省略した場合は空配列として扱う。


13. argumentCount と内部Push数 ​

argumentCount は上位コンパイラが確定した、Udon EXTERNへPUSHする 引数数である。インスタンスメソッドのreceiverを含み、戻り値格納先は含まない。

VMはロード時に次を計算し、内部情報として命令へキャッシュする。

text
hasReturn = Signatureが値を返すか(プロパティsetterは常にfalse)
pushCount = argumentCount + (hasReturn ? 1 : 0)

例えば、

text
TryParse(
    string,
    ref int
)
    ↓
引数 2
戻り値 1
    ↓
argumentCount = 2
pushCount = 3(VM内部)

戻り値なしの Debug.Log(object) なら、

text
引数 1
戻り値 0
    ↓
argumentCount = 1
pushCount = 1(VM内部)

となる。

argumentCount は0以上の整数でなければならない。計算された pushCount が ABI上限を超える場合、ロードは失敗する。実行時はロード時にキャッシュした値を使う。


14. EXTERN の Stack 動作 ​

例えば、

json
{
  "op": "EXTERN",
  "signature": "SystemInt32.__op_Addition__SystemInt32_SystemInt32__SystemInt32",
  "argumentCount": 2,
  "return": "__sum_0"
}

実行前:

text
Stack:
[
    "__value_0",
    "__constant_10"
]

結果:

text
Heap["__sum_0"] → -113

Stack:

text
[
    "__sum_0"
]

戻り値がある Extern は、戻り値の Heap Symbol を Stack へ残す。


14.1 Extern BridgeとExecutor ​

ShirasUdonExternBridge と ShirasUdonExternExecutor の責務は分離する。

コンポーネント責務
ShirasUdonExternBridgeSignatureから論理Extern Indexへの割り当て、Executor bankの選択・生成・交換、引数と戻り値の橋渡し
ShirasUdonExternExecutor生成済みUdonAssemblyを使用した、実際のUdon EXTERN命令の実行

ShirasUdonExternBridge は常駐する。

ShirasUdonExternExecutor はPrefabから必要に応じてInstantiateし、Bridgeの子GameObjectとして保持する。

Executorの1インスタンスは256個の物理Extern slotを持つ。

Signatureへ割り当てる論理Extern Indexには256の上限を設けない。Bridgeは次の式で実行先を決定する。

text
executorBankIndex = logicalExternIndex / 256
physicalExternIndex = logicalExternIndex % 256

例えば、論理Extern Index 1500 は次のように配置する。

text
Executor bank = 5
Physical slot = 220

必要なExecutor bankだけを遅延生成する。

論理Extern Indexはランタイム内部のキャッシュであり、ShirasUdonAssembly JSONのEXTERN命令には保存しない。JSON上の識別情報は引き続きsignatureとする。


14.2 Externキャッシュ ​

BridgeはExtern Signatureごとに安定した論理Extern Indexを割り当てる。

text
Extern Signature
    ↓ cache
Logical Extern Index
    ↓ / 256, % 256
Executor bank + Physical slot

同じSignatureを再度実行するときは、同じ論理Extern Indexと物理slotを使用する。

各実行時にSignatureを対象の物理slotへ設定する。このため、Executorが交換された場合でも、使用されたExternから順にslotの内容を遅延再構築できる。交換時にbank内の全Signatureを一括登録し直す必要はない。


14.3 Executorの例外と交換 ​

Udonは実行中に例外が発生すると、そのUdonBehaviourのenabledをfalseにして停止する。停止したShirasUdonExternExecutorは、その後の命令を実行できない。

BridgeはExtern呼び出しの直前と直後にExecutorのenabledを確認する。

Extern実行後にExecutorが無効になっていた場合は、次のように処理する。

  1. 失敗したExecutorから戻り値およびref/out値を読み取らない。
  2. 失敗したExecutor bankのGameObjectを破棄する。
  3. 同じbank位置へ新しいShirasUdonExternExecutorをInstantiateする。
  4. Signatureから論理Extern Indexへのキャッシュは維持する。
  5. 現在のEXTERN命令を失敗としてVMへ返し、VMの現在の実行を停止する。

例外を発生させたExtern命令は、副作用が途中まで実行されている可能性がある。そのため自動的には再実行しない。次回のVM実行では、交換済みExecutorを使用する。

他のExecutor bankは破棄せず、そのbankに属するキャッシュと実行状態を維持する。


15. JUMP ​

指定されたラベルへジャンプする。

json
{
  "op": "JUMP",
  "operand": "_loop"
}

_loop が存在すれば、その命令列の先頭から実行する。

export は確認しない。


16. JUMP_INDIRECT ​

Heap に格納されたラベルへ間接ジャンプする。

json
{
  "op": "JUMP_INDIRECT",
  "operand": "__jump_0"
}

Heap:

text
__jump_0
    ↓
Reference("_loop")

の場合、

text
JUMP_INDIRECT __jump_0
        ↓
"_loop"
        ↓
JUMP "_loop"

として実行する。

ShirasUdonAssembly JSON では、間接ジャンプにも Udon の生アドレスを使用しない。


17. JUMP_IF_FALSE ​

Stack から条件の Heap Symbol を取得し、その Heap 値が false の場合に指定ラベルへジャンプする。

json
{
  "op": "JUMP_IF_FALSE",
  "operand": "_end"
}

例えば、

text
Stack:
[
    "__condition_0"
]

Heap:
__condition_0 → true

ならジャンプしない。

text
__condition_0 → false

なら、

text
JUMP "_end"

を実行する。


18. 命令列の終了 ​

現在の仕様では、

現在のコードブロックの命令列末尾に到達した場合、正常終了する。

例えば _end の命令列を最後まで実行した場合、その後に次の命令が存在しないため VM は正常終了する。


19. v1 の設計思想 ​

ShirasUdonAssembly JSON v1 では、上位コンパイラだけが確定できる意味情報をJSONに持たせ、派生情報はロード時に一度だけ計算する。

例えば EXTERN では、

text
signature
argumentCount
refArguments(該当する場合)
return

を持つ。

一方、

text
hasReturn
pushCount
ref引数のbool配列への展開

などはVMがロード時に検証・計算し、実行時に再計算しない。

設計としては、

text
コンパイル時
    ↓
解析・計算
    ↓
ShirasUdonAssembly JSON
    ↓
実行時
    ↓
できるだけそのまま実行

とする。


20. ShirasUdonAssembly と UdonAssembly の境界 ​

ShirasUdonAssembly で扱う ​

text
ラベル
Heap Symbol
Extern Signature
Stack
Heap
制御フロー
型情報
WriteBack 情報

ShirasUdonAssembly では扱わない ​

text
Udon Heap Address
Udon Instruction Address
JUMP の実アドレス
ExternBridge の内部配置
UdonAssembly の stride / offset

これらは将来的な ShirasUdonAssembly → UdonAssembly Compiler の責務とする。


21. 設計上の基本原則 ​

ShirasUdonAssembly は、Udon VM の低レベルなアドレス表現を直接記述するのではなく、コンパイル可能な意味を表現する中間言語として設計する。

特に、

text
ラベル → 制御フロー
Heap Symbol → 値への参照
Extern Signature → Udon Extern
export → 外部公開

という責務を持たせる。

その後の UdonAssembly 生成時に、

text
Shiras Label
      ↓
Udon address

Heap Symbol
      ↓
Udon heap variable

Extern
      ↓
ShirasUdonExternBridge
      ↓
ShirasUdonExternExecutor / UdonAssembly

へ変換する。