CD Code Digest Hub /GTA V

Native FFI、宿主能力与游戏集成

6 分钟阅读 8 个章节

1. 一门小语言连接一个很大的游戏 API#

语言内核提供流程、数据和调用机制;车辆、角色、动画、摄像机、HUD、网络、存档等能力主要来自 game/script_headers/commands_*.sch 的 Native 声明及 game/script/commands_*.cpp 的实现。

所谓“标准库”需要分三层理解:VM/编译器内建运算,rage_builtins.sch 声明的计时/数学/线程等基础命令,以及项目封装在 .sch 中的玩法库。Native 并不自动等于语言内建 opcode;IS_BIT_SET 这样的特判才会直接降成专用指令。[S1][S2]

2. CREATE_VEHICLE 的端到端证据链#

脚本声明为:

text
NATIVE FUNC VEHICLE_INDEX CREATE_VEHICLE(
    MODEL_NAMES ModelHashKey,
    VECTOR VecCoors,
    FLOAT fVehicleHeading = 0.0,
    BOOL RegisterAsNetworkObject = TRUE,
    BOOL ScriptHostObject = TRUE,
    BOOL bIgnoreGroundCheck = FALSE
) = "0xba715a7beba5a1f9"

这是对实际声明的换行整理。[S3] 对应 C++ 注册:

text
SCR_REGISTER_SECURE(CREATE_VEHICLE, 0xba715a7beba5a1f9, CommandCreateVehicle)

调用链为:源码类型检查及默认参数展开 → NATIVE 指令与标识表 → 装载时解析标识 → scrWrapper 参数转换 → CommandCreateVehicle → 模型/网络/实体系统。

C++ 实现检查任务车辆容量、网络脚本资格、坐标范围、脚本主机权限、模型存在/已加载/为车辆等条件,再尝试创建车辆。[S4] 因此声明上“接受 MODEL_NAMES”并不能证明模型已请求成功,返回句柄也必须允许创建失败。

3. 参数和返回 ABI#

VM 的 Info 提供参数槽地址、参数槽数、返回区及向量临时缓冲。scrWrapper::Arg<T> 以 C++ 模板读出 int/float/bool/string 等类型,返回值模板写回槽位。[S5]

VECTOR 在脚本侧为三个槽。引擎 Vector3 对齐、SIMD 表示和 64 位槽填充不同,引用向量通过临时缓冲 copy-in/copy-out 对接;源码还注释某些按值 scrVector 包装会生成较差代码。这说明 FFI 成本不仅是一次 C++ 函数调用。

Native 参数上限按槽数而非源形参数计算:VECTOR 占三槽,大结构和变参会增加槽数。开放 STRUCT& 接口一般还接收 SIZE_OF,由具体命令验证边界和布局;它并非自动生成结构序列化。

VARARGS / VARARGS1/2/3 是另一套受限协议:生成器最多接收 16 个实参,用每项两位的类型描述编码整数、浮点、字符串引用或向量;TEXT_LABEL/向量按相应引用形式传递,带数字的变体规定最少实参数量。它不是任意对象动态调用,也不是 NATIVE 指令六位参数槽编码的同一个限制。[S9]

4. 稳定标识与装载绑定#

声明的十六进制 ID 与 C++ 注册 ID 要一致。装载 Validate 查命令哈希表并把程序 Native 表改成可调用地址;缺失命令会记录错误并使用无效命令处理路径。[S6]

这种做法避免每次调用通过字符串查找,也使脚本资源与 C++ 符号名解耦。但 ID 一致不自动保证参数数目、参数含义、返回 ABI 或副作用契约一致,必须由 API 维护流程保证。目录有 scannative、randomizenatives 和头文件生成相关工具,说明 ID/声明维护是单独的工程问题。

随机或不直观的 ID 不能提供访问权限隔离。“脚本能否创建网络实体、是否拥有控制权”仍然由 Native 内部和上下文决定。

5. 阻塞式 Native 的特殊契约#

WAIT 首次进入时置 BLOCKED,VM 保存同一 Native 的 PC 和参数;恢复时重进同一函数。只有完成后才清理参数并进行引用写回。这要求阻塞 Native 区分“第一次启动”与“后续轮询”,避免每帧重复创建资源或重复提交操作。[S7]

新 Native 若在返回前保留局部裸指针、修改不完整的输出结构、或在重入时重复副作用,可能破坏调用者状态。语言签名目前没有效果系统表达 may-yield、必须同帧完成、是否保留引用等条件。

6. 资源归属是跨边界的核心语义#

CTheScripts::RegisterEntity 把实体关联到当前 script handler,并区分 script-host object、networked 等属性;框架 handler 保存对象和资源列表,清理时按 Detach/Destroy 等策略处理。[S8]

所以“创建车辆”至少同时发生三件事:创建游戏对象、建立脚本归属、可能参与网络注册。仅看返回句柄无法掌握副作用范围。性能和清理分析应沿这三条链分别追踪。

7. 新 FFI 设计建议#

建议统一生成下列内容:脚本声明、C++ 包装、参数布局、错误枚举、资源归属、线程/帧阶段限制、挂起效果、网络权限和性能分类。

例如模型请求可返回一个受控异步结果,创建车辆接收“已加载模型”或显式报告未加载错误;实体引用区分本地有效性与网络控制权。高频批量查询应有批处理接口,但应根据剖析数据选择,不能只为减少函数数量而做巨型命令。

对不可信脚本,Native 应成为权限边界:验证每个句柄、范围、资源配额和调用频率,按模块授予能力。编译期类型检查可以减少作者错误,却不能承担宿主安全的全部职责。

源码证据#

  • S1 — 基础命令声明。
  • S2 — IS_BIT_SET 内建化。
  • S3 — CREATE_VEHICLE 声明。
  • S4 — CommandCreateVehicle;注册。
  • S5 — wrapper.h;Info 的向量缓冲。
  • S6 — Validate Native 绑定。
  • S7 — NATIVE 阻塞和写回。
  • S8 — RegisterEntity;handler 资源清理。
  • S9 — CollectParameters 的 VARARGS 协议。