Building an LLM Inference Engine from ScratchPart VI / Module 18
Part VI · Portability & Deployment

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++ 工程中。

C++ GDK Host main / WinMain D3D12XboxCreateDevice command queue calls C ABI Rust Core xinfer-ffi staticlib DirectML on injected D3D12 device Qwen2 model + KV cache token callback xinfer_create_with_device xinfer_generate callback
图 18-1:Xbox 路径的核心:C++ host 负责平台,Rust core 负责推理。

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:

ID3D12DevicehostIDMLDevicerustQwen2 inference\text{ID3D12Device}_{host} \longrightarrow \text{IDMLDevice}_{rust} \longrightarrow \text{Qwen2 inference}
C++ GDK Host D3D12XboxCreateDevice Raw COM pointers device + queue Rust DmlDevice DirectML on top 同一 Rust core:PC 可自己创建设备;Xbox 由 host 注入设备。
图 18-2:Device injection 避免 Rust core 依赖 DXGI,从而适配 Xbox GDK。

18.5C++ GDK host:链接 Rust lib

C++ host 的职责集中在平台边界:

  1. 创建 D3D12 device 和 command queue;
  2. 调用xinfer_create_with_device创建推理 context;
  3. 调用xinfer_generate,用 callback 接收 token 文本;
  4. 退出时调用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,并链接bcryptntdlluserenvws2_32advapi32dbghelp以及 windows-rs import library。

GDK 现实

已验证的是 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.
--------------
图 18-3:Xbox 路径的验证层级:从 cdylib 到 C++ host,再到真正 console。

Lab 18从 C 调用引擎,再从 C++ host 调用

本实验检查同一 C ABI 的两个入口:

  1. 用 Pythonctypes加载xinfer_ffi.dll,调用xinfer_createxinfer_generatexinfer_free
  2. 构建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_createxinfer_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 安全。