Module 18:面向 Xbox GDK
在没有官方 Rust Xbox console target 的条件下,将 Rust 推理核心封装为 C ABI staticlib/cdylib,并由 C++ GDK host 完成进程入口、D3D12 device 创建与注入。
学习目标
- 理解为什么 Xbox console 不能简单地作为普通 Rust target 处理。
- 掌握 C ABI 边界设计:opaque handle、错误码、no unwinding、回调。
- 理解 device injection:host 创建 D3D12 device,Rust core 在其上创建 DirectML device。
- 了解 C++ GDK host 如何链接 Rust staticlib,以及 CRT / system libs 的注意事项。
- 使用 GDK desktop toolchain 验证同一 FFI/device-injection 路径。
18.1问题:没有官方 Rust Xbox console target
Windows PC 上,Rust 的x86_64-pc-windows-msvctarget 可以直接构建可执行程序。Xbox console 属于 GDK 平台:process entry、应用打包、设备创建和运行时库由 GDK 管理;Rust 官方没有提供可直接使用的 Xbox console target。
xinfer 采用清晰的宿主划分:Rust core 编译为 C ABI library,C++ GDK host 负责平台入口和 device creation。这样既保留 Rust 侧的模型、DirectML 与采样实现,又让平台相关代码留在 GDK 支持最完整的 C++ 工程中。
18.2FFI 边界:Rust core 作为 C ABI library
xinfer-ffi同时构建为:
staticlib:供 C++ GDK host 链接;cdylib:供 PC 工具 / Python ctypes 测试加载。
C ABI 中暴露的核心函数以crates\xinfer-ffi\include\xinfer.h为准:
const char* xinfer_version(void);
int xinfer_create(const char* model_dir,
int stream_layers,
XinferContext** out_ctx);
int xinfer_create_with_device(void* d3d12_device,
void* command_queue,
const char* model_dir,
int stream_layers,
XinferContext** out_ctx);
int xinfer_generate(XinferContext* ctx,
const char* prompt_utf8,
const char* system_utf8,
int use_chat,
int max_tokens,
float temperature,
int top_k,
float top_p,
uint64_t seed,
XinferTokenCallback callback,
void* user_data);
const char* xinfer_last_error(const XinferContext* ctx);
void xinfer_free(XinferContext* ctx);
XinferContext是 opaque handle:C++ 只持有指针,不依赖 Rust 内部结构布局。由 Rust 分配的 context 必须用xinfer_free释放;xinfer_version返回静态字符串,不需要释放;callback 收到的 UTF-8 片段只在本次调用期间有效。
18.3安全 C ABI:错误码、opaque handle、no unwinding
跨语言边界的首要规则是:Rust panic 不能穿过 C ABI。xinfer-ffi在导出函数内部使用catch_unwind,把 panic 转换为XINFER_PANIC,普通错误则返回负数状态码。生成阶段的错误消息保存在 context 中,可通过xinfer_last_error查询;创建失败时 context 尚未建立,调用者应先检查返回码。
| 设计点 | 原因 |
|---|---|
Opaque XinferContext* | 隐藏 Rust 内部布局,C++ 只负责持有和释放 |
| 负数错误码 | C ABI 简单稳定,不抛异常 |
xinfer_last_error | 提供可读错误消息 |
| Token callback | 流式输出,不必等完整生成结束 |
xinfer_free | 由 Rust 释放 Rust 分配的对象 |
18.4Device injection:为什么不能用 DXGI?
Windows PC 上可以通过 DXGI 选择 adapter 并创建 D3D12 device;Xbox console 上不提供桌面 DXGI adapter 枚举。GDK host 使用D3D12XboxCreateDevice创建设备,再把ID3D12Device*和ID3D12CommandQueue*交给 Rust core。
DmlDevice::from_raw_pointers接收两个 raw COM pointer,并为 context 生命周期持有相应引用:
ID3D12Device*ID3D12CommandQueue*
然后在这个 device 上创建 DirectML device:
18.5C++ GDK host:链接 Rust lib
C++ host 的职责集中在平台边界:
- 创建 D3D12 device 和 command queue;
- 调用
xinfer_create_with_device创建推理 context; - 调用
xinfer_generate,用 callback 接收 token 文本; - 退出时调用
xinfer_free。
XinferContext* ctx = nullptr;
int rc = xinfer_create_with_device(device.Get(), queue.Get(),
model_dir, stream_layers, &ctx);
rc = xinfer_generate(ctx, prompt, nullptr, 1, 64,
0.0f, 0, 1.0f, 42,
&OnToken, nullptr);
xinfer_free(ctx);
链接时需要处理 CRT 与 Rust staticlib 的系统依赖。当前 GDK desktop 验证路径使用/MD匹配 Rust 的动态 CRT,并链接bcrypt、ntdll、userenv、ws2_32、advapi32、dbghelp以及 windows-rs import library。
已验证的是 GRDK / Gaming Desktop toolchain 路径;真正的 Xbox console 分支仍需要 GXDK console SDK 与 dev kit。二者使用同一套 C ABI 与 device injection 设计。
18.6用 GDK desktop toolchain 验证
没有 console dev kit 时,仍可用 GDK desktop toolchain 验证从 C++ host 到 Rust staticlib、device injection、真实推理的完整链路:
# 构建 Rust staticlib + C++ host
.\xbox\build_host.ps1
# 运行 host,创建 D3D12 device 并注入 Rust core
.\xbox\xinfer_host.exe models\Qwen2.5-0.5B-Instruct "What is 2 plus 2?"
验证输出示例:
xinfer (Xbox host) version 0.1.0
--- output ---
2 plus 2 is 4.
--------------
Lab 18从 C 调用引擎,再从 C++ host 调用
本实验检查同一 C ABI 的两个入口:
- 用 Python
ctypes加载xinfer_ffi.dll,调用xinfer_create、xinfer_generate和xinfer_free。 - 构建
xbox\xinfer_host.exe,验证 C++ host 调用xinfer_create_with_device。
python crates\xinfer-ffi\tests\ctypes_smoke.py `
target\release\xinfer_ffi.dll `
models\Qwen2.5-0.5B-Instruct `
"What is the capital of France?"
.\xbox\build_host.ps1
.\xbox\xinfer_host.exe models\Qwen2.5-0.5B-Instruct "Name three primary colors."
小结
本章说明了 xinfer 的 Xbox GDK 部署路径。Rust core 通过xinfer-ffi导出 C ABI,并同时构建为 staticlib 与 cdylib;C++ GDK host 负责 process entry、D3D12 device 与 command queue 创建,并通过xinfer_create_with_device把 raw COM pointer 注入 Rust。这个设计把平台职责和推理职责分离,也把 FFI 约束明确化:opaque handle、负数状态码、xinfer_last_error、panic 不跨边界、由分配方负责释放。当前已验证的路径包括 Python ctypes smoke test 和 GDK desktop host 的 end-to-end 运行;真正 console 分支还需要 GXDK 与 dev kit。
思考与练习
基础为什么 Rust panic 不能跨 C ABI 边界?
panic 是 Rust 特有的栈展开(或 abort)机制,C/C++ 不认识它。让 panic 展开穿过extern "C"边界进入 C++ 栈是未定义行为(可能破坏栈、跳过析构、直接崩溃)。所以 FFI 函数必须在边界内用catch_unwind捕获 panic,转换成错误码/错误消息返回。xinfer-ffi 正是在每个导出函数里 catch panic,再通过xinfer_last_error暴露原因。
基础opaque handle 有什么好处?
opaque handle(如void*/ 不透明指针)把 Rust 内部结构对 C 隐藏:C 端不需要知道也不能依赖其内存布局,只把它当令牌传回 Rust。好处:①ABI 稳定,Rust 侧结构可自由演进而不破坏 C 头;②封装/安全,C 不能误碰内部字段;③生命周期清晰,由 create/free 配对管理。xinfer 用它表示引擎实例(xinfer_create或xinfer_create_with_device返回,xinfer_free释放)。
进阶解释为什么 Xbox console 上不能依赖 DXGI 枚举 adapter。
因为 Xbox 主机不提供桌面 DXGI 的 adapter 枚举模型——它是固定单一 GPU 的封闭平台,用专用入口D3D12XboxCreateDevice(带主机特定参数)直接创建设备,而非CreateDXGIFactory+ 枚举 +D3D12CreateDevice。所以可移植设计要把设备创建外置:让宿主按平台各自创建 device/queue,再注入 Rust 核心(DmlDevice::from_existing/from_raw_pointers),核心不直接调 DXGI。
进阶如果 C++ host 提前释放 device,会对 Rust core 造成什么风险?为什么需要 COM ref-count?
风险:Rust core 仍持有指向该ID3D12Device(及 queue/资源)的指针,host 提前释放会造成悬垂指针——后续 GPU 调用访问已释放对象,导致崩溃或未定义行为(use-after-free)。COM 的引用计数解决此问题:当 Rust 适配既有 device(from_existing)时应AddRef,使对象的存活与所有持有者绑定;各方释放时Release,引用归零才真正销毁。这样无论 host 还是 core 谁先“放手”,只要还有人引用,device 就不会被过早销毁。
挑战设计一个 C ABI 函数,让 host 查询模型的 resident weight bytes 和 decode tok/s。
用输出参数 + 错误码返回,避免跨边界返回复杂类型:
int xinfer_get_stats(XinferContext* ctx, uint64_t* resident_weight_bytes, double* decode_tok_s);
约定:返回 0 表示成功,非 0 表示出错(细节经xinfer_last_error取);handle 为空或无最近一次生成统计时返回错误。Rust 侧在catch_unwind内把QwenModel::resident_weight_bytes()与最近一次GenerateStats::decode_tok_per_s()写入两个出参指针(先判空)。也可定义一个XinferStatsPOD struct 一次性填充。要点:所有数值用固定宽度类型(u64/f64),指针出参,错误用返回码——保证 ABI 稳定且 panic 安全。