CD Code Digest Hub /GTA V

类型系统、数据表示与 ABI

9 分钟阅读 9 个章节

1. 编译期有类型,VM 槽位没有逐值标签#

编译器以 scrType 及其子类表示 BOOL、INT、FLOAT、字符串、向量、枚举、数组、结构体、引用、Native 句柄和函数类型。一般兼容性比较类型对象身份,特殊类型覆写规则。生成后,VM 使用 union scrValue:同一槽位可按 Int、Float、String、Reference 或 Any 解释。操作码及编译结果负责告知 VM 应采用哪种解释。[S1][S2]

因此这不是动态类型语言。也不能因 scrValue 的辅助 ValueType 枚举存在,就认为每个运行时值都携带类型标签。调试元数据、Native 模板、可变参数类型描述与槽位本体是不同层。

2. 主要类型#

类型编译/执行特性设计含义
INT使用 C++ int/32 位操作,通常一槽64 位槽不自动提供 64 位算术
FLOAT单精度 float,一槽不应推定双精度或跨平台确定性
BOOL独立静态类型,运行以整数槽表示条件类型检查的 warning 路径也会阻止正常生成产物
VECTOR三个 FLOAT 槽,字段 X/Y/Z与引擎 SIMD Vector3 不一定同布局
STRING字符串指针/编码引用,一槽不拥有通用可增长字符串对象
TEXT_LABEL_N固定可写缓冲区;编译大小 (N+1)/4 槽容量、槽位和字节不能混用
ENUM / STRICT_ENUM命名枚举;严格变体抑制若干整数提升位标志与普通状态枚举应区分
HASH_ENUM / STRICT_HASH_ENUM未显式赋值时取枚举名哈希哈希不是序号;COUNT_OF 不等于项数
STRINGHASH整数表示的语义类型字面量可编译期哈希,变量可能运行期哈希
NATIVE 类型引擎句柄,可声明父类型静态 API 类型安全,不等于脚本类继承
POOLINDEX特殊池索引,默认 -1“全部默认零”也有例外
STRUCT、数组内联聚合值、固定布局复制与栈消耗可预测,但大对象代价直接
TYPEDEF FUNC/PROC带签名的函数/过程指针不等同捕获环境的闭包

类型与默认初始化见 type.h/type.cpp。[S1] 局部槽通常先清零,再补数组头和非零默认字段;Native 句柄一般以 NULL 为零值。ENUM 的 COUNT_OF 按实现维护的枚举计数规则理解,手册说明为最后枚举值加一,不宜用于稀疏或 HASH_ENUM 的成员数量统计。

3. 数值转换:强类型不是绝对禁止转换#

INT/FLOAT 变量直接赋值不应按 C 的自动转换来写,通常使用 TO_FLOAT、ROUND、FLOOR、CEIL。但生成器对常量和算术存在提升特例。VECTOR 与 FLOAT 算术可广播 scalar。[S3]

ENUM_TO_INT、INT_TO_ENUM、NATIVE_TO_INT、INT_TO_NATIVE 提供显式通道,若干转换仅产生 OP_NOP 或改变编译期类型。显式写出转换不代表运行时验证了枚举成员或句柄存活。

scrTypeDefaultValue::IsCompatibleWith 比较基本类型,宽于精确枚举类型身份;引用参数又有 -strictreferencetypechecking 选项。报告将其描述为“有静态类型检查”,而不声称它已经形式化证明健全或拒绝一切不安全转换。[S1][S3] 诊断名字中的 warning 不代表能带警告正常生成产物,具体失败机制见前端报告。

数值边界也要单独定义。当前整数加减乘及除法多直接使用宿主 C++ int 运算,不能仅凭常见机器结果认定语言已规范“有符号溢出总是回绕”;INT_MIN / -1 也需要单独验证。FLOAT 采用单精度,但 NaN、负零、舍入及不同平台优化不由“使用 float”自动保证一致。已经能静态确认的零除数路径差异见虚拟机报告,其余边界本次没有运行验证。

4. 内存布局:用槽位计算#

当前模型可用下列公式理解:

text
SIZE_OF(INT) = SIZE_OF(FLOAT) = 1
SIZE_OF(VECTOR) = 3
SIZE_OF(STRUCT) = 所有字段槽位数之和
SIZE_OF(T[N]) = 1 + N × SIZE_OF(T)
运行时占用字节 = 槽位数 × sizeof(scrValue)

数组多出的一个槽保存元素数量,嵌套数组的每一层都有自己的长度头。例如:

text
STRUCT AgentState
    VECTOR position       // 3 槽
    INT health            // 1 槽
    BOOL active           // 1 槽
ENDSTRUCT                 // 总计 5 槽

AgentState agents[10]      // 1 + 10×5 = 51 槽

该例在四字节槽目标为 204 字节,在八字节槽目标为 408 字节。这里是布局推导,不是编译器实测;不包含线程、资源对象及分配器额外开销。

SIZE_OF 返回槽数,不是 C++ sizeof 的字节数。跨语言 API 在边界转换,例如网络广播将 Size 乘 sizeof(scrValue);漏乘、重复乘或按固定四字节写接口都会产生兼容问题。[S4]

5. 字符串的三种不同成本#

  1. 字符串字面量放进程序字符串池,可以复用;不是每次执行都创建字符串对象。
  2. STRING 是借用式访问形态,不能从类型本身推导其指向数据的寿命。
  3. TEXT_LABEL 是可写的定长缓冲区,赋值、拼接和整数格式化按容量处理,调试路径可诊断过长,快速路径有截断行为。

TEXT_LABEL_15 在编译类型系统里是四槽;64 位目标的物理存储可能比命名暗示的 16 字节更大。C++ 桥接定义也按 sizeof(scrValue) 扩展。不能简单把这个历史名字视为所有平台一致的物理容量规范;应同时核对生成的指令长度、平台转换和调用端缓冲区。[S2][S5]

字符串处理以 char 字节为中心。当前证据没有提供完整 Unicode 字符索引、规范化或按字素截断规则。中文等多字节内容不应依照 TEXT_LABEL 后缀直接推定可存“字符数”。

6. 数组和引用不是动态堆#

普通数组大小参与编译期类型与布局。PROC F(INT &values[]) 的空维度表示参数可以借用不同长度的数组,实际长度由数组头保存;这不等于支持 new、resize、自动增长列表。固定长度数组实参通常要求匹配,开放长度引用在兼容性检查中有特殊规则。[S1]

数组访问有编译期常量下标检查和 VM 运行期边界检查。然而边界检查依赖数组长度头可信,Native 写入与裸引用仍是可信边界;这不能单独组成对恶意字节码的安全沙箱。

结构体按值传参会复制多个槽,引用传参则借用原区域。编译器没有展示通用所有权/借用寿命系统;将局部地址注册给长期使用的原生系统,必须遵守具体接口的生命周期契约。

7. 句柄子类型与抽象边界#

游戏定义 ENTITY_INDEX : ITEM_INDEX,VEHICLE_INDEX、PED_INDEX、OBJECT_INDEX : ENTITY_INDEX。[S6] 这允许实体 API 接受车辆或角色,防止在编译期明显混用类型。它没有赋予脚本对象方法、虚函数、用户类构造或内存继承布局。

“类型正确”和“对象仍然存在”是两个条件。对象销毁、流式卸载、网络所有权变化后,类型仍是 VEHICLE_INDEX,但对应引擎实体可能不能操作。新语言应考虑带世代编号的句柄、可空返回值和查询结果类型。

8. 对新语言的建议#

建议分离三种布局:VM 执行布局、Native ABI、网络/存档 schema;不要令平台指针宽度自动改变序列化协议。标量采用明确宽度,字符串采用明确编码,数组提供长度和容量语义,避免把可变参数签名当成动态容器。

对引用,引入只读借用、可写借用与是否跨帧保留的契约;对句柄,表达失效和所有权检查。保留编译期强类型与运行期紧凑表示的优点,同时建立加载验证器和 Native 参数验证。

源码证据#

  • S1 — type.h:类型、默认值、数组兼容及大小;type.cpp:结构体、向量与 TEXT_LABEL 大小。
  • S2 — value.h:槽位与值表示;thread.h:scrVector / scrTextLabel 桥接。
  • S3 — node.cpp:数值提升与参数检查。
  • S4 — 广播槽数转字节。
  • S5 — TEXT_LABEL 快速执行路径。
  • S6 — types.sch:游戏句柄层级。