Skip to main content

Udon VM 和 Udon Assembly

info

此页面由 VRChat 社区成员编写。感谢您的贡献! VRChat 团队无法保证此页面信息的有效性。如果您想提出更改建议,请转到此页面底部并点击"编辑此页面"。

Udon VM 概述​

Udon VM 是一个字节码解释器,设计用于运行编译后的 Udon Graph 程序。

因此,需要记住它具有和不具备的功能:

  • Udon VM 预期在 .NET 环境中运行。虽然不使用反射来访问函数,但 Udon VM 的风格类似。
  • 它不直接实现调用/返回或子程序(尽管存在 JUMP_INDIRECT 并可用于此目的)。
  • 它通过 JUMP、JUMP_INDIRECT 和 JUMP_IF_FALSE 实现流程控制。
  • 它可以调用 C#(允许的地方)的函数。
  • 它没有局部变量,只有对象上的字段。
  • 它有一个整数堆栈,但此整数堆栈在大多数情况下应视为操作码的"额外参数"。可以将其用作调用/返回机制的一部分,但请记住没有局部变量,因此递归函数必须非常小心地实现。
tip

你可以从 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 本身。

如果以上都不是,则会发生错误。

caution

目前无法在 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,按此顺序。在初始运行中它们之间没有间隙,并且它们总是在任何其他事件之前运行;如果有任何尝试绕过这一点,调用将被忽略。更多详情请参见事件执行顺序。

自定义事件从不接受参数(除了你定义的任何机制之外),并且不以 _ 开头。

实际的操作码相当简单。有操作码名称,然后对于某些操作码,有一个参数。该参数可以是整数、符号(该符号的整数值,即堆索引或代码地址)或字符串。当是字符串时,汇编器会为该字符串创建一个隐藏的未命名变量,实际值将是堆索引。

caution

出于某种原因,不允许定义指向同一位置的两个代码符号,会导致 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 参考​

caution

依赖 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 参考。

但:

caution

在 UdonSharp 类暴露树中复制成员名称的功能目前存在问题。这是快速浏览可用内容的好方法,但你仍然需要使用 Udon Graph 获取实际的 extern 名称。