背景
Olive Video Editor是一个GitHub上曾经获得过9.1K stars的项目,是一个致力于成为专业级的视频编辑软件。遗憾的是,它停止更新了,原作者说他没有放弃,但一年多还是两年没更新过了。2025年6月我决定自己来延续它的生命,我fork了一份,改名叫OliveCE,后来改名叫Oak Video Editor。
当时我做的第一件事是添加OpenFX插件支持。遗憾的是这对当时的我还是太复杂了,加上社团活动繁忙,我折腾了一年。今年7月我总算发布了v0.4,v0.4.2是一个算得上稳定的版本,起码能用了。
但是我快要被C++的各种崩溃折腾疯了。这是因为很多C++第三方库不支持智能指针,Olive自己也是裸指针和智能指针混用,而且OpenFX也不支持智能指针,用get获取裸指针也不安全。而且整个Olive几乎没有测试,也很难测试,我废了好大功夫,行覆盖率才50%。我决定整个Rust重写。AI对我百般劝阻,劝我别这么做,但我一意孤行。
绞杀者模式
我面临的第一个问题是,Olive是一个巨大的单体,有600多个编译单元,全部链接到一个ELF里面。但如果直接从头重写,我总觉得这样比较麻烦。我选择了绞杀者模式:首先拆库,然后逐个重写为Rust,最后重新合并。
拆库:双层适配器模型
为了尽可能减少浪费在C++这里的时间,以及避免大范围重构的风险(Olive几乎没有测试),我选择了一种“双层适配器”模型:在被调用者一侧,用C函数包装C++代码;在调用者一侧,用C++类再包裹回名字一样的C++。打个比方,这个类:
class Clazz{
privte:
//...
public:
Clazz();
~Clazz();
void func(int param);
//...
};
可以这样包裹:
// clazz_internal.h
struct clazz_wrapper {
Clazz *handle;
};
// clazz.h
typedef struct clazz_wrapper* OakClazz;
OakClazz init_clazz();
void free_clazz(OakClazz self);
void clazz_func(OakClazz self, int param);
// clazz.cpp
#include <clazz.h>
#include <clazz_internal.h>
OakClazz init_clazz(){
return new struct clazz_wrapper { .handle = new Clazz() };
}
void free_clazz(OakClazz self){
delete self.handle;
delete self;
}
void clazz_func(OakClazz self, int param){
self->handle->func(param);
}
另一边:
class Clazz{
privte:
OakClazz handle = nullptr;
//...
public:
Clazz(){
handle = init_clazz();
}
~Clazz(){
free_clazz(handle);
}
void func(int param){
clazz_func(handle, param);
}
//...
};
然后其他代码完全不用改。
问题:虚函数表怎么处理?
其实我忘了当时咋处理的了,几种思路: 1. 自己写虚函数表,在wrapper里自己实现动态分发 2. wrapper采用相同的继承方式,派生类修改基类的handle为自己的handle。
绞杀者重写:逐API集成测试
选择Rust的一大理由就是测试方便,肯定不能浪费。首先,让LLM给每个函数写单元测试,然后给对外暴露的API写集成测试。这是基于这样的理论:如果集成测试能跑通,程序一定是没问题的,因为只要有任何一个节点出问题,集成测试就跑不通。最后,给整个渲染引擎写集成测试,再覆盖一部分。
问题:AI写的测试是假测试(恒真断言等)怎么办?
首先,你要用足够好的AI,比如Kimi K3,或者让Kimi K3监督DeepSeek。然后,你要用另一个AI在没有上下文的情况下独立审计这个AI的代码。经过这两轮以后,你再自己抽查几个测试,基本就没什么大问题了。
问题:AI写代码太快了,我review的速度跟不上,怎么办
对于我来说,Review只干这几件事:审查代码风格、审查架构违规、审查注释完整性,偶尔抽查几个函数是不是假实现。其他的代码和上面的问题一样,交给另一个AI去review。
重新合并成单体
经过绞杀者重写,Oak已经是纯Rust了,但还有内存问题,还有崩溃。
最开始我尝试用类似FFmpeg的结构体句柄:
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CHandle {
/// Opaque box pointer.
pub ctx: *mut c_void,
/// Atomic increment.
pub addref: Option<unsafe extern "C" fn(*mut c_void)>,
/// Atomic decrement; destroys at zero.
pub release: Option<unsafe extern "C" fn(*mut c_void)>,
/// ABI version.
pub abi_version: u32,
}
但AI似乎没法很好地处理引用计数。可能是因为我用的AI不是Fable 5吧,Anthropic禁止我用我也没办法啊……然后我就在想,拆库的意义是什么呢?大部分代码都有单元测试,集成测试也全跑通了,我为什么要留着模块之间的C语言边界?又不打算作为C库给别人用,更新的话肯定整个包一起更新。重新单体化有个好处:所有的边界全部重新改为Rust,内存安全天然得到保证。于是我决定重新单体化。
总结与反思
这次重写,我认为主要的亮点在于,为我以后重写一个大型项目建立了一套固定方法,即拆库然后绞杀者模式。主要是我重写进行的太快了,一个月就弄完了,这套方法的优势没体现出来:本来可以让C++和Rust共存几个版本,逐步进行替换的。当然这次重写也有一些没想到的点,比如在发现AI代码有假实现以后,应该整个模块Review,而不是让AI继续做。这让我吃了不少苦头。
这次重写我还发现了一个组合:我用一个聪明模型(Kimi K3)去看着便宜模型(DeepSeek V4 Flash 0731)去工作,聪明模型Review便宜模型的代码。这样基本不会出现假实现的情况,也比较省钱。
(完)
Oak Video Editor的self-hosted Gitea:https://git.oakvideoeditor.org/oak-team/oak-editor GitHub mirror:https://github.com/OakVideoEditorCommunity/oak
感谢openKylin对本项目的支持。
