11 KiB
R7 计划:从"nm=0"到 Rust-ready 纯 C ABI
面向执行者(Qwen 3.8 Max),自包含。工作分支:
c-abi-migration。 背景:R6 已使 oak-editor / oak-render-worker 的U _ZN5olive= 0 (d13e4e800)。但按 riir.md 的目标(engine 拆模块 → Rust 重写), 还差两层皮:
oakengine/display.h的灰色契约——签名是void *,语义却是 C++ (往调用方内存写std::shared_ptr、参数实为olive::VideoParams*);- liboakengine.so 仍导出 3486 个 C++ 符号(
nm -D --defined-only | grep " T _Z"),Rust 模块化拆分要求导出面只剩oakengine_*。三条红线照旧(禁 inline 化 engine 实现、禁 no-op stub、禁 dlsym)。 每步闭环:全量构建 0 error → 全量 ctest 绿 → 立即提交。 flaky 规则:
oak_cli_transcode/oakengine_export_test/olive-gtest单独重跑一次,连续两次失败才算回归(oak_cli_transcode的间歇 SEGFAULT 是预存问题,手动跑通过为准)。
R7-A:display.h 灰色契约 POD 化
A.1 现状问题(为什么 nm=0 还不够)
engine/include/oakengine/display.h 的 11 个函数签名全是 void *,
但注释约定:
out_texture/out_frame指向调用方内存中的TexturePtr/FramePtr(std::shared_ptr),engine 在其上拷贝构造 shared_ptr;video_params实为const olive::VideoParams*;color_job实为const olive::ColorTransformJob*。
Rust 无法安全持有 shared_ptr、无法构造 C++ VideoParams。app 侧 47 处
TexturePtr/FramePtr 成员/变量(18 文件,见 A.4)同样要换。
A.2 新契约(设计钉死,照此实现)
所有权协议(核心决策,不许改):纹理/帧句柄是不透明指针,指向
engine 堆上的控制块(内部持有 std::shared_ptr,实现细节对 ABI 不可见)。
所有权经显式 retain/free 转移,禁止任何"写入调用方内存的 shared_ptr"。
/* engine/include/oakengine/display.h —— 重写版 */
/* ---- 渲染器 ---- */
OAKENGINE_API void *oakengine_display_renderer_create_dynamic(
const char *backend_id, void *parent_qobject);
OAKENGINE_API void *oakengine_display_renderer_create_opengl(
void *parent_qobject);
OAKENGINE_API int oakengine_display_renderer_init(void *renderer,
void *gl_context);
OAKENGINE_API void oakengine_display_renderer_destroy(void *renderer);
/* ---- 纹理(句柄 = OakEngineDisplayTexture*,不透明) ---- */
/* params 用 oak_video_params POD(oakengine/videoparams.h 已有),
* 不再是 const void*。 */
OAKENGINE_API void *oakengine_display_texture_create(
void *renderer, const oak_video_params *params,
const void *pixels, int linesize);
/* retain:返回同一句柄并把内部引用计数 +1(跨线程移交用,
* 见 A.3 协议)。free:-1,归零时释放。二者对 NULL 均为 no-op。 */
OAKENGINE_API void *oakengine_display_texture_retain(void *texture);
OAKENGINE_API void oakengine_display_texture_free(void *texture);
OAKENGINE_API int oakengine_display_texture_upload(
void *texture, const void *pixels, int linesize);
OAKENGINE_API int oakengine_display_texture_download(
void *texture, void *pixels, int linesize);
/* 只读属性查询(替代 texture->params()/width()/format() 等) */
OAKENGINE_API int oakengine_display_texture_get_params(
const void *texture, oak_video_params *out);
OAKENGINE_API int oakengine_display_texture_id(const void *texture);
/* ---- 帧(句柄 = OakEngineCodecFrame*,不透明,同协议) ---- */
OAKENGINE_API void *oakengine_codec_frame_create(void);
OAKENGINE_API void *oakengine_codec_frame_retain(void *frame);
OAKENGINE_API void oakengine_codec_frame_free(void *frame);
OAKENGINE_API int oakengine_codec_frame_set_video_params(
void *frame, const oak_video_params *params);
OAKENGINE_API int oakengine_codec_frame_get_params(
const void *frame, oak_video_params *out);
OAKENGINE_API int oakengine_codec_frame_allocate(void *frame);
OAKENGINE_API void *oakengine_codec_frame_data(void *frame);
OAKENGINE_API int oakengine_codec_frame_linesize(const void *frame);
/* ---- 色彩管理 blit ---- */
/* oak_color_transform_job POD(新定义,字段以 engine
* ColorTransformJob 拍平:processor 句柄 + input/output 空间 id +
* 各向异性等)。 */
OAKENGINE_API int oakengine_display_renderer_blit_color_managed(
void *renderer, const oak_color_transform_job *job,
void *dst_texture, const oak_video_params *params);
/* 跨后端纹理下载(viewerdisplay 的 download_from_texture 路径) */
OAKENGINE_API int oakengine_display_renderer_download_from_texture(
void *renderer, int texture_id, const oak_video_params *params,
void *dst_pixels, int linesize);
engine 实现(engine/src/capi/display.cpp 重写):
struct OakEngineDisplayTexture { olive::TexturePtr ptr; };
struct OakEngineCodecFrame { olive::FramePtr ptr; };
// create: new OakEngineDisplayTexture{renderer->create_texture(...)}
// retain/free: new/delete 控制块(引用计数即 shared_ptr 自身)
// POD↔C++:oak_video_params ↔ olive::VideoParams 的转换函数若
// capi 已有(viewer.cpp 的 get 路径)就抽成内部共享 helper
// (放 engine/src/capi/videoparamsinternal.h),不许复制粘贴第三份。
A.3 跨线程移交协议(最容易写错的地方,钉死)
viewerdisplay 的 load_frame_/load_texture_ 在解码线程生产、
显示线程消费。原语义靠 shared_ptr 引用计数保活。新协议:
- 生产侧
oakengine_display_texture_retain(t)后写入共享槽; - 消费侧取走句柄,旧句柄
free; - 槽清空时持有一方负责
free。 每个 retain 必须配对恰好一个 free。写完后 grep 审计配对数。
A.4 app 侧触点清单(47 处,按文件做,每文件一提交)
| 文件 | 处数 | 要点 |
|---|---|---|
app/widget/viewer/viewerdisplay.{h,cpp} |
19+15 | 最大。texture_/load_texture_/gizmo 纹理全换句柄;析构与各 reset 路径补 free;A.3 协议主要在这里 |
app/widget/scope/scopebase/scopebase.{h,cpp} |
6+9 | 同模式 |
app/widget/manageddisplay/manageddisplay.cpp |
6 | create_texture/blit/download |
app/widget/viewer/viewer.{h,cpp} |
1+9 | FramePtr 成员换句柄 |
app/widget/multicam/multicamdisplay.{h,cpp} |
1+4 | |
app/widget/scope/histogram/histogram.{h,cpp} |
2+1 | |
app/widget/scope/waveform/waveform.{h,cpp} |
1+1 | |
app/widget/scope/vectorscope/vectorscope.{h,cpp} |
1+1 | |
app/panel/viewer/viewerbase.h、app/panel/scope/scope.{h,cpp} |
3 |
完成判据:app 全仓库 grep TexturePtr|FramePtr = 0;
oakengine/display.h 全文无 shared_ptr、无 olive:: 出现在签名
(注释里也不许写 "olive::TexturePtr storage" 这种约定)。
A.5 验证
- 新增
engine/tests/oakengine_display_test.cpp(无 GL 环境测错误 路径与 retain/free 配对;GL 相关断言用现有 backend 检测跳过模式)。 olive-gtest的ViewerDisplayReproTest三个可跑通用例必须全过 (这是显示路径的回归网,挂一个就是真挂)。- 手动验证(报告里写明):打开素材 → 画面非黑;scope 面板渲染正常。
R7-B:engine visibility 收口(3486 → 只导出 oakengine_*)
B.1 原理(已具备的条件)
OAKENGINE_API 在 GCC/Clang 已是
__attribute__((visibility("default")))(export.h:40)。给
oakengine 目标加 -fvisibility=hidden 后,只有 oakengine_* 导出。
oakgl/oakvulkan 两个动态后端同理(已有 *-cabi-check OBJECT
目标,顺带确认它们导出面也只剩 C ABI)。
B.2 唯一难点:测试链接
隐藏符号后,直接引用 engine C++ 内部的测试会断链:
olive-gtest:1006 个U _ZN5olivetimeline-tests:29oakengine_export_test:9(make_oakengine_test族里引用内部的)compositing-tests:0(纯 facade,无碍)
解法(钉死):engine 源码改出 OBJECT 库,测试链对象文件而非
.so:
# engine/CMakeLists.txt
add_library(oakengine-obj OBJECT ${OLIVE_SOURCES})
# (POSITION_INDEPENDENT_CODE ON)
add_library(oakengine SHARED $<TARGET_OBJECTS:oakengine-obj>)
target_compile_options(oakengine-obj PRIVATE -fvisibility=hidden)
# 引用 engine C++ 内部的测试目标:
target_link_libraries(<test> PRIVATE oakengine-obj) # 替代 oakengine
olive-gtest(tests/gtest/CMakeLists.txt)改链oakengine-obj+oakengine(facade 符号从 .so 来,避免重复定义;若 ODR 冲突则只链 oakengine-obj,先把 facade 函数符号在 object 里的重复问题解决—— 二选一,以链接通过且 ctest 全绿为准,把选择写进提交信息)。timeline-tests、引用内部的oakengine_*_test同法。- 禁止:为了让测试过而把 engine 内部符号加 visibility("default") 白名单——那是开天窗。
B.3 验证
nm -D --defined-only cmake-build-debug/engine/liboakengine.so | grep -c " T _Z" # 目标 0
nm -D --defined-only cmake-build-debug/engine/liboakengine.so | grep -c " T " # 应等于 oakengine_* 函数数
nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" # 必须仍为 0
全量构建 0 error + 全量 ctest 绿(45 个)才提交。
R7-C(低优先级,时间够再做):app 的 engine C++ 头清理
app 仍 include ~40 个 engine C++ 头(grep -rn '#include "' app/ | grep -E '"(node|render|timeline|undo|task|pluginSupport)/')。不产生
符号引用(nm=0 已证),但 RIIR 拆模块时这些 include 会全部失效。
逐个换 facade/句柄头(cliphandle.h、nodevaluehandle.h 模式)。
本批不设完成判据,收尾时把剩余清单写进 riir.md 附录即可。
验收(R7 完成判据)
display.h全文无 C++ 类型签名/契约注释;app 无 TexturePtr/FramePtr。- liboakengine.so
T _Z= 0;oak-editorU _ZN5olive保持 0。 - 全量构建 0 error;全量 ctest 绿。
- 更新
plans/riir.md状态(边界已纯 → 可进 Step 1 拆分)、facade-migration-roadmap.mdR7 批次记录。 - 向用户报告,由用户宣布进入 riir.md §4 的模块拆分阶段。
状态:R7 已完成(eb634b53e,2026-07-27)
- R7-A ✅ display.h 已按 §A.2 契约重写(POD + 句柄 retain/free), app TexturePtr/FramePtr 清零。
- R7-B ✅ liboakengine.so 导出 3486 → 19(version script;19 个为 oakgl/oakvulkan dlopen 插件 ABI,ver 文件内注释钉死)。
- 复核修复 1 处:viewer.cpp 生产端推裸
void *、消费端解OakSharedBufferPtr的类型不匹配(vulkan 显示路径全挂), 已改为 oak_make_shared_frame/texture(retain)。 - R7-C(app 的 ~40 个 engine C++ 头清理)未做,转入 riir/ 模块 拆分阶段顺带处理(M 系列手册的适配头天然覆盖)。
- 验收:构建 0 error、ctest 45/45、双二进制 nm U _ZN5olive = 0。 RIIR 模块拆分(plans/riir/ M1-M9)解锁。