0%

RKYOLO诞生记 (一):告别C++"动物园",为何我选择用Rust从零开始?

大家好,我是个大三的学生。最近因为导师的一个课题,需要把一个我自己训练的、用于识别玉米穗的 YOLO11n 模型,部署到一块 Rockchip RK3588 开发板上。

这其实还有个“前传”:为了让板子支持最新的模型,我已经折腾了好一阵子,把整个 Linux 内核连带 RKNPU 驱动都给交叉编译升级了一遍。所以,当底层环境就绪,模型也转换成.rknn格式后,我当时觉得,接下来应该就是顺理成章的“应用开发”了。

很自然地,我找到了野火社区的“鲁班猫”AI 教程,他们提供了一个yolo11的 C++示例。我寻思着,既然有现成的,改改用就行了呗?

事实证明,我还是太年轻了。

当我把官方提供的yolo11.rknn模型(COCO 数据集,80 个类别)跑起来时,一切正常。但当我换上我自己的、只有一个类别的玉米穗模型时,程序就陷入了沉默的崩溃,连个像样的错误日志都没有。

这次失败,让我下定决心:放弃修改,从零重构。

这个决定看似冲动,但其实背后有一个在我脑子里盘旋了好几个月的执念:一个 AI 模型推理能力,如果不能被轻松地集成到其他应用里,那它的价值就大打折扣。 无论是导师的农业项目,还是我一直想玩的、带目标检测的 ROS 机器人,它们需要的都不是一个只能在终端里敲命令运行的独立程序,而是一个稳定、可靠、可以被轻松调用的“能力模块”

这篇博客,就是想聊聊我看到的那个 C++示例是如何打破了我这个“集成梦”,以及为什么最终选择了 Rust 作为我的“破局之刃”。

解剖 C++“动物园”:一些让我无法妥协的设计

首先要说明一点,野火的这个示例项目(LubanCat AI Manual Code),它的项目结构很大程度上继承自 Rockchip 的官方模型仓库(rknn_model_zoo)。所以,我接下来要吐槽的,并不仅仅是某一个示例项目的问题,而是一种在嵌入式 C++开发中相当普遍的、让我难以忍受的设计惯性。

1. 硬编码的“想当然”

当我开始寻找程序崩溃的原因时,postprocess.h里的这几行代码让我瞬间明白了问题所在:

1
2
3
4
5
6
// yolo11/cpp/postprocess.h
#define OBJ_NAME_MAX_SIZE 64
#define OBJ_NUMB_MAX_SIZE 128
#define OBJ_CLASS_NUM 80 // <--- 喂喂喂!
#define NMS_THRESH 0.45
#define BOX_THRESH 0.25

OBJ_CLASS_NUM,这个代表物体类别总数的宏,被“想当然”地写死成了80。整个后处理代码,包括各种循环和内存计算,都依赖这个写死的值。我的玉米穗模型只有一个类别,当后处理逻辑试图去访问它根本不存在的第 2 到第 80 个类别的预测数据时,不出错才怪了。

这还只是冰山一-角。代码里还假设模型永远有 3 个输出分支,用app_ctx->io_num.n_output / 3来计算每个分支的张量数。这种写法,让代码变得极其脆弱。这已经不是一个“框架”了,它更像一个“一次性脚本”,只能和特定模型绑定。每次模型结构发生丁点变化,都得回来改代码,然后重新编译,太麻烦了。

2. 紧耦合的“大杂烩”——我的“集成梦”是如何被打破的

这,才是我最无法容忍的一点。看看它的构建系统,CMakeLists.txt里赫然写着:

1
2
3
// yolo11/cpp/CMakeLists.txt
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../3rdparty/ 3rdparty.out)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../utils/ utils.out)

这意味着,为了编译这一个yolo11的小小示例,我需要把整个包含3rdpartyutils的上层代码库——也就是那个庞大的“动物园”(Zoo)——完整地下载下来。

集成个屁!

我魂牵梦绕了几个月,想的是怎么把 YOLO 能力作为一个干净的库,轻松地链接到其他项目里。结果这个 Demo 告诉我,想用我?行啊,先把我的整个“动物园”亲戚都请过来。这还怎么集成?难道我的 ROS 项目也要被迫依赖这一大堆它根本用不上的utils3rdparty吗?

这种设计,彻底扼杀了我将其作为“能力模块”进行二次开发的可能性。

3. 手动管理的“安全隐患”

最后,是老生常谈的 C++资源管理问题。代码里充斥着手动malloc/free,以及经典的goto cleanup;模式。

这种写法本身没错,但在复杂的逻辑中,它完全依赖开发者的记忆力和纪律性。少写一个free,或者在某个错误处理分支忘了goto,就意味着一次隐蔽的内存泄漏。在需要 7x24 小时稳定运行的嵌入式设备上,这种隐患是致命的。

我的破局之道:选择 Rust 的三重考量

面对以上种种,我意识到“缝缝补补”是没有出路的。我需要的是一个安全、独立、现代化的解决方案。于是,我决定用 Rust 来重写这一切。

1. 从“记住释放”到“自动管理”——RAII 带来的心智解放

我最看重 Rust 的一点,就是它的 RAII(资源获取即初始化)机制。我做的第一件事,就是创建一个专门的rknn-ffi Crate,然后把需要手动管理的rknn_context句柄用一个 Rust 结构体包起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// rkyolo/rknn-ffi/src/context.rs
pub struct RknnContext {
ctx: raw::rknn_context,
}

impl Drop for RknnContext {
fn drop(&mut self) {
// 当RknnContext对象离开作用域时,这里的代码会自动执行
debug!("Dropping RknnContext and calling rknn_destroy...");
unsafe {
raw::rknn_destroy(self.ctx);
}
}
}

就这么简单。Drop trait 就像一个和编译器签下的“契约”,保证了只要RknnContext对象消亡,rknn_destroy就一定会被调用。我再也不用担心忘记释放资源了,因为编译器会帮我记住。这种心智上的解放,让我可以更专注于业务逻辑本身。

2. 从“依赖泥潭”到“清晰边界”——Cargo 带来的工程尊严

告别了复杂的CMakeLists.txt,Rust 的包管理器 Cargo 让项目工程变得前所未有的清爽。

我的rkyolo项目通过 Cargo 的workspace功能,从一开始就被划分为职责分明的几个部分:rknn-ffi(负责与 C 库交互)、rkyolo-core(核心算法)、rkyolo-app(应用程序)。每个部分都是一个独立的 Crate,依赖关系在各自的Cargo.toml里一目了然。

这种设计带来了极大的灵活性,完美地回应了我最初的“集成梦”。未来任何项目想复用我的核心推理能力,只需要依赖rkyolo-core这个 Crate 就行,完全不会被其他不相关的东西所拖累。

3. 从“处处unsafe”到“安全封装”——FFI 带来的健壮抽象

Rust 并没有回避与 C 交互时unsafe的必要性,但它提供了一套机制,让我们能把这些“危险”的操作关进一个可控的“笼子”里。

我的rknn-ffi Crate 就是这样一个“笼子”。所有与 C 库的裸指针交互、内存操作,都被严格限制在其中,并被封装成安全的、返回Result类型的 Rust 函数。这样一来,上层的rkyolo-corerkyolo-app就可以在 100% safe 的 Rust 环境中开发,享受编译器提供的全部安全检查。

结论:一项值得的战略投资

是的,用 Rust 重写这一切花了我两天时间。但这两天并不是“重复造轮子”,而是在打地基

我投入时间,换来的是一个在编译时就杜绝了内存泄漏、依赖清晰、易于集成、核心逻辑与应用完全解耦的坚固地基。我相信,这项前期的“战略投资”,为我后续快速、高质量地完成导师的任务,甚至探索更有趣的机器人项目,铺平了道路。

当然,地基打好只是开始。很快,我就在这块坚实的地基上,遇到了第一个真正的拦路虎——模型能跑了,但结果却“指鹿为-马”。

下一篇,我们就来聊聊我是如何侦破这个棘手的数值迷案的。