Udon VM 和 Udon Assembly
此页面由 VRChat 社区成员编写。感谢您的贡献! VRChat 团队无法保证此页面信息的有效性。如果您想提出更改建议,请转到此页面底部并点击"编辑此页面"。
Udon VM 概述
Udon VM 是一个字节码解释器,设计用于运行编译后的 Udon Graph 程序。
因此,需要记住它具有和不具备的功能:
- Udon VM 预期在 .NET 环境中运行。虽然不使用反射来访问函数,但 Udon VM 的风格类似。
- 它不直接实现调用/返回或子程序(尽管存在
JUMP_INDIRECT并可用于此目的)。 - 它通过
JUMP、JUMP_INDIRECT和JUMP_IF_FALSE实现流程控制。 - 它可以调用 C#(允许的地方)的函数。
- 它没有局部变量,只有对象上的字段。
- 它有一个整数堆栈,但此整数堆栈在大多数情况下应视为操作码的"额外参数"。可以将其用作调用/返回机制的一部分,但请记住没有局部变量,因此递归函数必须非常小心地实现。
你可以从 Udon Graph 和 UdonSharp 程序导出 Udon Assembly。
这有助于了解你编写的代码是如何编译的,并帮助你找到 extern 名称!
Udon 类型
"Udon 类型"是 Udon 引用 C# 类型的方式。
理解这些类型名称的最简单方法是从对应的 .NET 名称开始,例如 System.Int32[]。
Udon 类型的构造有以下规则:
- 所有
.和+都被移除,即VRC.SDKBase.VRCPlayerApi+TrackingData变为VRCSDKBaseVRCPlayerApiTrackingData。 - 类型可能附加
Array,表示[]。
因此,上面的示例是 SystemInt32Array。
Udon Assembly
Udon Assembly 程序由两个部分组成:数据部分和代码部分。
这些部分用开始/结束指令标记,如下所示:
.data_start
# Data goes here!
.data_end
.code_start
# Code goes here, instead!
.code_end
数据部分
数据部分标记了行为(behaviour)的变量,以及哪些是导出的(公共)。
这些部分中的数据存储在"Udon 堆"中,尽管名称如此,它实际上是一个带有类型信息的扁平值数组。"堆索引"是该数组中的索引。
在 Udon Assembly 中定义变量的示例:
message: %SystemString, "Hello, world!"
这里,变量的符号是 message,类型是 SystemString,内容是 "Hello, world!"。
注意,类型可能更适合描述为_初始_类型,因为变量可以在运行时更改类型(但如果变量是公共的,则可能不应该这样做)。
值可以是 null、this、true、false、字符串、字符常量、整数、无符号整数(以 u 结尾的整数)和浮点数,但汇编器对何时可以指定这些值有严格限制。
具体来说:
SystemSingle和SystemDouble必须是数字或null。SystemInt32和SystemUInt32必须是任何一种整数或null。SystemString必须是字符串字面量或null。- 所有其他类型,包括
SystemObject,只能是this或null。
this 本身并不具有其"传统"含义。取决于变量的类型,它可以是:
GameObject:UdonBehaviour的GameObject。Transform:GameObject.transform。UdonBehaviour、IUdonBehaviour或Object:UdonBehaviour本身。
如果以上都不是,则会发生错误。
目前无法在 Udon Assembly 中为 SystemType 指定非 null 值,但在 Udon Graph 和 UdonSharp 中可以。类似的问题也存在于 SystemInt64、SystemUInt64、SystemSByte、SystemByte、SystemInt16、SystemUInt16 和 SystemBoolean(是的,实际上不可能成功指定 true 或 false)。
这些是 Udon Assembly 的限制。只能通过不使用 Udon Assembly 来规避。
浮点数始终被读取为 floats,即使预期类型是 double。
这些变量也可以标记为公共,例如使用 .export message,并且可以标记同步元数据,例如 .sync message, none。
将变量标记为同步元数据等同于 synced 复选框;详情请参见网络。
这里的 none 是插值模式。插值模式有 none、linear 和 smooth,但并非所有插值模式都适用于所有类型。
代码部分
代码部分是一个带标签和可能导出的操作码列表。
.export _start
_start:
PUSH, message
EXTERN, "UnityEngineDebug.__Log__SystemObject__SystemVoid"
JUMP, 0xFFFFFFFC
.export _start(示例;将 _start 替换为你导出的任何符号)用于导出事件处理程序的代码符号。
标准事件以 _ 开头,其参数通过变量传递(非公共),你需要创建这些变量;这是一个很长的列表,最好通过 Udon Graph 探索。
然而,重要的是,最先运行的两个事件是 _onEnable 和 _start,按此顺序。在初始运行中它们之间没有间隙,并且它们总是在任何其他事件之前运行;如果有任何尝试绕过这一点,调用将被忽略。更多详情请参见事件执行顺序。
自定义事件从不接受参数(除了你定义的任何机制之外),并且不以 _ 开头。
实际的操作码相当简单。有操作码名称,然后对于某些操作码,有一个参数。该参数可以是整数、符号(该符号的整数值,即堆索引或代码地址)或字符串。当是字符串时,汇编器会为该字符串创建一个隐藏的未命名变量,实际值将是堆索引。
出于某种原因,不允许定义指向同一位置的两个代码符号,会导致 Address aliasing detected 错误。
Udon Opcodes
NOP
- 操作码:0
- 参数:0
此操作码不执行任何操作。通常没有理由使用它,除非遇到 Address aliasing detected: 错误。
PUSH, parameter
- 操作码:1
- 参数:1
此操作码将一个整数推送到堆栈顶部。
Udon Assembly 可能给人以推送值的印象;但情况并非如此。
在这些情况下,推送的是堆地址。
除非你非常致力于优化 Udon 程序的大小(即使以牺牲运行时速度为代价),或者试图混淆代码,否则没有理由以条件方式使用此操作码。只需在 EXTERN、COPY 或 JUMP_IF_FALSE 之前立即推送所有内容。
POP
- 操作码:2
- 参数:0
此操作码从堆栈中移除顶部整数,没有进一步的效果。
JUMP_IF_FALSE, parameter
- 操作码:4
- 参数:1
从堆栈中弹出一个堆索引,并从中读取 SystemBoolean。
如果该值为 false,则跳转到参数指定的字节码位置。否则,继续执行下一条指令。
JUMP, parameter
- 操作码:5
- 参数:1
跳转到参数指定的字节码位置。
JUMP, 0xFFFFFFFC 也用于结束执行(即从 Udon 代码返回)。
EXTERN, parameter
- 操作码:6
- 参数:1
此操作码是 Udon 执行任何有用操作的方式。
首先要注意的是,参数是一个堆索引,最初包含 extern 名称(作为字符串),但这也被写入。
作为优化,Udon 在给定堆索引中首次运行 extern 后会缓存有关 extern 的信息。这些值仍然是堆值,可以复制。
extern 的参数按 PUSH 顺序给出;即第一个推送的值是第一个参数。
这些堆值对于普通(即 in)参数是读取,对于 ref 参数是读写,对于 out 参数是写入。
如果 extern 不是静态的(即如果有 this 参数),则 this 参数会添加到开头。如果有返回值(即返回类型不是 SystemVoid),则将其视为末尾的 out 参数。
ANNOTATION, parameter
- 操作码:7
- 参数:1
这实际上是一个"长 NOP"。参数被忽略。
JUMP_INDIRECT, parameter
- 操作码:8
- 参数:1
从参数中获取堆索引,并从中读取 SystemUInt32。
将其解释为字节码位置并跳转到该位置。
COPY
- 操作码:9
- 参数:0
弹出两个堆索引。第二个弹出的堆索引(即第一个推送的堆索引)中的值被复制到第一个弹出的堆索引(即第二个推送的堆索引)。
Externs 参考
依赖 extern 签名的确切格式是不可取的,除非依赖已知存在的特定 extern。
格式在最好的情况下也可能会很奇怪,并且从签名推断诸如'这是静态方法吗'之类的属性是不可能的,更不用说泛型的细节了。
如果你正在尝试构建依赖于完全了解 API 的内容,你可能需要编写 C# 代码来抓取 Udon Graph 节点列表。
Externs 的形式为 SomeUdonTypeName.SomeSignature。(有一个情况下 Udon 类型名称是"伪造的",即 VRCInstantiate。)
这里,示例将是 SystemDateTimeOffset.__TryParseExact__SystemString_SystemStringArray_SystemIFormatProvider_SystemGlobalizationDateTimeStyles_SystemDateTimeOffsetRef__SystemBoolean。
这是 System.DateTimeOffset.TryParseExact(string, string[], System.IFormatProvider, System.Globalization.DateTimeStyles, out System.DateTimeOffset)。这是一个静态方法。值得注意的是,如果它不是静态方法,'this'参数仍然不会在签名中标记。
签名本身总是以 __ 开头,后跟函数名称,再后跟 __。构造函数的函数名称是 ctor;这里它是 TryParseExact。
然后,每个非 this 参数以其 Udon 类型名称跟随,用 _ 分隔。在 ref 和 out 参数的情况下,Udon 类型名称有一个特殊修饰符 Ref 后缀。
最后,签名以 __ 结尾,后跟返回类型的 Udon 类型名称。
有一些特别奇怪的情况:
- 泛型在其签名中将类型参数列为"Udon 类型",例如
T,并具有不可见的SystemType参数。 VRCUdonUdonBehaviour变为VRCUdonCommonInterfacesIUdonEventReceiver。(如果相关,仍然会附加Array等。)
目前没有完整的 extern 参考。
但:
- UdonSharp 文档有 VRChat 方法的 API 参考。
- UdonSharp 提供了一种使用类暴露树探索可用内容的方法。
在 UdonSharp 类暴露树中复制成员名称的功能目前存在问题。这是快速浏览可用内容的好方法,但你仍然需要使用 Udon Graph 获取实际的 extern 名称。