Gdev 至 Rust 移植工程:方案论证、早期试错与分层实施报告

摘要

针对遗留 Gdev 组件向 Rust 语言体系迁移的工程需求,本项目通过前期方案对比与原型验证,确立了自底向上分层重构的核心策略。本文系统阐述了四种候选移植路径的优劣矩阵、方案 C 试错阶段暴露的三大深层矛盾、基于架构分层的 15 阶段细化计划,以及当前 Layer 1 Ioctl 封装层的实际推进状态。所有设计均以类型安全、内存安全与工程可验证性为第一性原则,力求实现从 C/C++ 到惯用 Rust 的范式转换。

一、项目背景与总体目标

Gdev 作为 GPU 虚拟化与运行时系统的关键组件,长期运行于内核态与用户态混合模式下,代码基约 35,000 行 C/C++。其迁移至 Rust 的核心诉求在于:

  • 消除 C 语言固有的内存安全与并发安全风险。
  • 重构臃肿的 C++ 继承体系,实现清晰的 trait 多态。
  • 以 RAII、Result/Option 等现代语义取代手动错误处理与资源管理。
  • 构建纯 Rust crate 生态,彻底摆脱对 C 构建系统与 ABI 的依赖。

在与 Opencode Agent 的联合推演中,我们归纳出四种可行路径,并结合对底层 Ioctl 接口核心地位的认识,最终选定方案 A 作为主策略。

二、四种候选方案综述

方案 A:自底向上逐层构建

  • 阶段划分:
    1. AIDEV Ioctl 封装 (1,200 行)
    2. Gdev 核心抽象 (4,000 行)
    3. SDAA Driver API (7,700 行)
    4. Ocelot Runtime (22,500 行)
  • 核心理念:从离硬件最近的 Ioctl 层向用户 API 方向推进。每完成一层即可获得独立可验证的 Rust 库,并行对接上层 C 测试。
  • 验证方式:阶段 1 后,以 Rust Ioctl 库替换 lib/user/aidev/,重新链接 libgdev.so,执行现有 gdev/test/ 全量用例。
  • 优点:步步可测、风险最低、最早接触 unsafe 边界,杜绝后期发现 Ioctl 封装错误的灾难性返工。
  • 缺点:前期对外不可见面向用户的 Rust API。

方案 B:自顶向下逐步剥离

  • 阶段划分:
    1. Bindgen 自动生成 libgdev.so 的 FFI 绑定
    2. SDAA Runtime 安全包装
    3. SDAA Driver API 替代 FFI 调用
    4. Gdev 核心与驱动替换
  • 核心理念:先通过 FFI 桥接现有 C 库,尽早暴露 Rust 用户接口,随后向下替换。
  • 优点:首周即可用 Rust API,上层生态友好。
  • 缺点:FFI 作为临时拐杖终将抛弃,底层 Ioctl 被遗留至最后,重构成本叠加。

方案 C:逐文件替换

  • 阶段划分:
    1. 选最小 .c 文件翻为 Rust (如 version.c,30 行)
    2. CMake 中将 Rust 编译的 .o 链接入现有构建
    3. 运行全量测试验证
    4. 循环直至全部替换
  • 核心理念:不改变现有构建系统,Rust 以目标文件形式嵌入,任何时刻可回退。
  • 优点:风险最低,无需一次性整体架构设计。
  • 缺点:推进最慢;最终产物仍为 C ABI 的 .so,无法享受纯 Rust crate 的生态红利。

方案 D:全量重写与 ABI 兼容

  • 阶段划分:
    1. 导出符号分析
    2. Rust 全量实现,编译为 libgdev_rs.so
    3. A/B 对比测试
    4. 无缝替换
  • 核心理念:一次性用 Rust 重写整个 Gdev,严格保持与原始 libgdev.so 相同的公开符号,实现即插即用替代。
  • 优点:结果最干净,无 C 遗留。
  • 缺点:风险最高,前期无中间可验证产物,ABI 不兼容时排查近乎不可能。

方案对比矩阵

对比维度 A: 自底向上 B: 自顶向下 C: 逐文件 D: 全量重写
最终代码质量 纯 Rust 纯 Rust C ABI 限制 纯 Rust
风险 最低
速度
需一次性设计

决策依据

运行时核心完全依赖最底层的 Ioctl 通信,先前主机模式移植 UK 项目中的大量问题均围绕 Ioctl 展开。因此,优先稳固底层 Ioctl 封装,再向上层层抽象,是风险可控、工程上最为明智的选择。

三、早期试错:方案 C 的深层矛盾

在未区分方案的情况下,也就是五一期间,我曾尝试逐文件翻译,直接对 Gdev 源文件进行 Rust 化,该过程暴露出三个根本性障碍,直接导致该路径被废弃。

3.1 头文件依赖传染

方案 C 要求每个 Rust .o 对外导出与 C 源码完全一致的符号:函数标记 #[no_mangle] pub extern “C” fn,结构体标记 #[repr©]。Gdev 的 include 结构呈典型 C 项目交叉引用形态:gdev_device.h 通过 gdev_system.h 间接包含 gdev_list.h、gdev_time.h、gdev_ioctl_def.h、gdev_nvidia_def.h 等 8 个头文件。翻译 device.c 时,其函数签名使用的 struct gdev_device 含有 40 个字段与 5 层嵌套结构体,必须在 Rust 侧手写等价的 #[repr©] 定义。尝试以 bindgen 自动生成绑定,单 bindings.rs 即达 1,200 行,是 device.c 自身 150 行的 8 倍。每翻一个文件,都需维护一个不断膨胀的绑定层,维护成本与翻译本身几乎等价。

3.2 #ifdef KERNEL 双模式编译的无优雅等价

Gdev 的核心设计是同一份 common/*.c 源码通过宏 KERNEL 编译两次:内核态使用 spin_lock、kthread_create,用户态使用 SysV 信号量 semop、msgrcv。C 中一行 #ifdef KERNEL 即可解决的问题,在 Rust 中只能通过 #[cfg(feature = “kernel”)] 拆分为两套完全独立的函数体。更深层的矛盾在于,用户态与内核态版本必须在链接时呈现完全相同的符号表,而 Rust 编译器不会自动保证两种 feature 下函数签名、结构体内存布局、调用约定的一致性,维护难度极高。

3.3 产出仅是“Rust 语法的 C”

翻译 device.c(150 行)经编译、链接后成功运行了 madd 和 mmul 两个 CUDA 内核测试,结果与原始 C 一致。然而代码审查揭示了本质问题。原 C 片段:

int gdev_device_init(struct gdev_device *gdev, int id) {
    gdev->id = id;
    gdev->mem_list = NULL;
    if (gdev_query(gdev)) goto fail_init;
    return 0;
fail_init:
    return -1;
}

对应的 Rust 必须写成:

#[no_mangle]
pub extern "C" fn gdev_device_init(gdev: *mut gdev_device, id: c_int) -> c_int {
    unsafe {
        (*gdev).id = id;
        (*gdev).mem_list = std::ptr::null_mut();
        if gdev_query(gdev) != 0 { return -1; }
        0
    }
}

为满足 C ABI 对接,Rust 被迫放弃 Result 错误传播、引用生命周期、所有权语义三大核心安全机制,返回 int 而非 Result,使用 raw pointer 而非 &mut GpuDevice。最终代码名义上是 Rust,但模块边界处类型安全彻底断裂,编译器既查不出空指针,也管不了资源泄漏。

由此确立方案 A

方案 A 从根本上解决了方案 C 的症结:不再向 CMake 中塞入 .o,而是构建独立的 Rust crate,每一层对外暴露 Rust 原生接口(Result、Option、trait),仅在最底层的 Ioctl 处使用小范围 unsafe 和 extern “C”。自底向上的完整类型安全链路得以保全,彻底杜绝了逐层翻译中 C ABI 的反复打断。

四、方案 A 细化:自底向上 15 阶段 Rust 移植计划

在开始方案A,先让opencode去仔细读一遍ioctl那些文件的调用和源码来更详细的去定制移植计划,在制定移植计划的过程中,方案A的第四阶段调整了 Ocelot 处理策略,这一层也是比较关键的部分,需要全部转成rust,用 Rust 重写 Ocelot 但不照搬 C++ 结构。

架构总览

层级 组件 原行数 语言 职责
Layer 4 SDAA Runtime (Ocelot) ~22,500 C++ 用户态 CUDA 式 Runtime
Layer 3 SDAA Driver API ~7,000 C CUDA API 封装
Layer 2 gdev Core Abstraction ~4,000 C 设备/内存/调度抽象
Layer 1 AIDEV Ioctl Wrapper ~1,500 C /dev/aicardN 的 mmap/ioctl
Layer 0 aicard.ko ~2,500 C 内核模块,不可翻

Ioctl 接口基于 libaidev.h 的 DRM ioctl 体系(pscnv_getparam, pscnv_gem_new, pscnv_chan_new 等 21 个函数),而非旧的 GDEV_IOCTL_* 遗留接口。

Layer 1: AIDEV Ioctl Wrapper (~1,500 lines → ~800 Rust lines, 4 阶段)

Stage 1.1 — 类型与常量定义
文件: gdev-layer1/src/types.rs
来源: libaidev.h 常量、aidev_drm.h 结构体、libaidev_ib.h 结构体。
验证: cargo check + 16 个 size_of 布局断言。

Stage 1.2 — 基础 ioctl 操作
文件: gdev-layer1/src/ioctl.rs
翻译 8 个基础包装函数,返回 Result 类型,封装 drmIoctl、pscnv_getparam、pscnv_gem_new/close、pscnv_vspace_new/free、pscnv_chan_new/free,并实现 EINTR 重试逻辑的 mock 测试。

Stage 1.3 — IB 通道 & Buffer Object 管理
文件: gdev-layer1/src/buffer.rs
将 libaidev_ib.c 翻译为 Rust,利用 OwnedFd、Mmap 等 RAII 类型接管生命周期。设计 IbChan 和 BufferObject 结构,确保 Drop 时自动 munmap/close,并支持 mock 验证资源释放。

Stage 1.4 — 设备原始操作
文件: device.rs, memory.rs, context.rs
翻译 aidev_gdev.c 核心,设计 Device、Vas、Context 的所有权图:Device 使用 OwnedFd + 引用计数;Vas 共享 Device 并持有 IbChan;Context 共享 Vas 并管理 fence/event buffer。所有分配、映射操作均以 RAII 保证安全释放。

Layer 2: gdev Core Abstraction (~4,000 lines → ~2,500 Rust lines, 4 阶段)

按核心类型、公共 API、调度器框架、完整集成的顺序,将 gdev_device.c、gdev_api.c 及四种调度策略翻译为 trait Scheduler 的多态实现,并贯通 glaunch、gsync 等核心操作。

Layer 3: SDAA Driver API (~7,000 lines → ~4,000 Rust lines, 3 阶段)

以 sdaa.h 类型系统为基础,分阶段实现 sdMemAlloc、sdCtxCreate、sdModuleLoad 等 API,采用 goblin crate 替换手动 ELF 解析(消除 gdev_sdaa.c 中最复杂的 1,282 行),并保留 mock 后端用于无卡验证。

Layer 4 修订:惯用 Rust 重写 Ocelot Runtime (~22,500 lines → ~3,400 Rust lines, 4 阶段)

对 C++ 遗产进行范式转换:

  • 单例 → Arc<Mutex>
  • 虚基类 executive::Device* → trait Device
  • void* 句柄 → 类型安全 newtype(FatBinaryHandle、ModuleId 等)
  • std::map → HashMap
  • 手动 malloc → Vec
  • 删除 SdaaDriver.cpp 转发层、Hydrazine 工具库及无用继承树。

修订后 Layer 4 总行数从原估 10,000 缩减至 3,400,节省约 7,200 行,主要源自:删除 pass-through 转发层(-658)、消除 Hydrazine 依赖(-3,000)、trait 替代继承(-2,000)、安全 Rust 消除错误处理样板(-1,500)。

五、实施进度:阶段 1.1 完成

Stage 1.1 执行结果

项目结构:
gdev-rust/layer1/
├── Cargo.toml # libc 依赖
└── src/
├── lib.rs # crate root + mod 声明
└── types.rs # 280 行:全部常量、#[repr©] 结构体、16 个布局测试

翻译映射:

  • aidev_drm.h 中的 27 个 #define 命令码 → AIDEV_GETPARAM* 等常量
  • aidev_drm.h 中的 14 个 ioctl 结构体 → #[repr©] struct + layout 测试
  • libaidev.h 的 DRM ioctl 宏与路径常量 → drm_ioc() const fn + DRM_* 常量
  • libaidev_ib.h 的两个结构体 → PscnvIbBo, PscnvIbChan

验证结果:

  • cargo check: 0 errors
  • cargo test: 16/16 passed

当前已反复确认 Step 1.1 无误,正式进入 Stage 1.2 的基础 ioctl 操作封装。
这里由于不知道为什么,之前下达的指令一阶段一总结未生效,现在已经持续做到阶段2了且耗时比较长,仍在运行中,等opencode agent运行到自然停止后再继续补充进度。

附录:方案 A 分层概览

阶段数 原行数 预估 Rust 行数
L1: Ioctl Wrapper 4 ~1,500 ~800
L2: Core Abstraction 4 ~4,000 ~2,500
L3: SDAA Driver API 3 ~7,000 ~4,000
L4: Ocelot Runtime 4 ~22,500 ~3,400
合计 15 ~35,000 ~10,700
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐