Files
oak-editor/docs/zh/c-abi-migration-handoff.md
T
Mike-Solar c486c853ff docs: C ABI migration campaign plans, handoffs and roadmap
Complete documentation set for the facade migration (B1-R6) and beyond:
facade-migration-roadmap, handoffs v3-v6, R5 guides, R6 cleanup plan,
R7 pure-ABI plan, long-term plans (riir/ai-agent/gtest/ui-redesign),
updated Chinese README draft, UI design mockups, and the Google Test +
struct-typedef rules in CONTRIBUTING.
2026-07-26 22:42:44 +08:00

27 KiB
Raw Blame History

liboakengine 纯 C ABI 迁移 — 交接执行计划(v3)

本文档是后续执行者(DeepSeek Flash 或任何接手代理)的唯一权威执行依据。 所有架构决策、边界契约、禁止事项已在本文钉死,执行时不得另行发明新方案; 遇到本文未覆盖的决策点,按"§8 决策兜底原则"处理,不得自由发挥。

相关文档:docs/zh/facade-migration-roadmap.md(各批次完成记录 + 附 D 事件机制 SOP)。

v32026-07-23):K2.7 对 DeepSeek Flash 的产出做了验收,修复了 6 个真实缺陷 (详见 §2.2,每条都附教训——这些错误模式不得再犯)。当前符号 39、 仅剩 1 个已知测试失败。剩余工作按符号逐项钉死在 §3/§5。


1. 目标与验收标准

终态

  1. liboakengine.so 的动态符号表中没有任何 olive:: C++ 符号(只有 oakengine_* C 符号 + Qt/系统符号)。
  2. oak-editoroak-render-worker 两个可执行文件不 import 任何 olive:: C++ 符号(豁免清单见 §6.4)。
  3. 全量测试通过;engine/include/oakengine/*.h每个 OAKENGINE_API 声明的函数都有测试覆盖。
  4. worker 端到端 harness 已完成,不要重做。

统一度量命令(禁止换口径):

# 总指标(当前 39,目标 = 豁免清单项数 = 6,见 §6.4)
nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive"
# 逐符号清单
nm -D cmake-build-debug/app/oak-editor | grep " U _ZN5olive" | c++filt | sed 's/.* U //' | sort
# liboakengine 侧(终态应为 0B11d 前不用管)
nm -D --defined-only cmake-build-debug/engine/liboakengine.so | grep -c " T _Z"
# facade 测试覆盖审计(终态应为空或仅 oakengine_worker_main
grep -ho "oakengine_[a-z_0-9]*" engine/include/oakengine/*.h | sort -u > /tmp/decl.txt
cat engine/tests/oakengine_*_test.cpp | grep -ho "oakengine_[a-z_0-9]*" | sort -u > /tmp/tested.txt
comm -23 /tmp/decl.txt /tmp/tested.txt

构建与测试(所有批次完成后必须全绿):

cmake --build cmake-build-debug -j$(nproc)   # 构建目录已配置好,不要重新 cmake
cd cmake-build-debug && ctest --output-on-failure -j$(nproc)   # 当前基线 44 个测试,约 90-140s

已知 flaky(历史偶发、重跑即过):oak_cli_transcodeoakengine_export_testolive-gtest 各观察到过一次偶发 SEGFAULT。遇到先单独重跑;连续两次失败才算真失败


2. 当前状态(v3 交接快照)

  • 总符号数:557 → 39
  • 接手第一件事:全量构建 + 全量 ctest,确认基线后再继续(§2.3 有当前已知的精确状态,但一切以你实测为准)。
  • 未提交改动很多(所有批次都在工作区,未 commit)。严禁任何 git 写操作,严禁回滚任何现有未提交改动。
  • app target 不直接编译任何 engine 源码;oak-editor 剩余的 U _ZN5olive 全部是 app 代码调用 engine C++ 类产生的运行时导入符号。

2.1 当前符号清单(39,nm 实测,逐项归属在 §3)

 4 AudioProcessor(豁免,§6.4
 1 AudioWaveformCache::staticMetaObject
 1 Block::staticMetaObject(豁免,§6.4
 1 ColorManager::staticMetaObject
 3 DynamicRendererctor / init_with_open_gl_context / load
 1 Folder::staticMetaObject
 5 Framector / dtor / create / allocate / set_video_params
 1 NodeFactory::library
 5 Nodelink / unlink / set_label / set_standard_value / staticMetaObject
 2 OpenGLRendererctor / init
 2 Projectname_changed / staticMetaObject
 3 Renderercreate_texture / blit_color_managed / destroy
 2 RenderManagerinstance_ / backend_to_string
 1 SubtitleBlock::k_text_in
 2 Textureupload / download
 1 TrackListRippleToolCommand ctor
 1 Track::staticMetaObject(豁免,§6.4
 3 UndoCommandctor / redo_now / undo_now

2.2 v3 验收已修复的缺陷(DS 产出中的真实 bug,均已修复并验证)

这些是按"教训"写的:每种错误模式都对应一条硬规则(§6.6),后续批次必须遵守。

  1. B10 app 侧重复定义导致进程退出时堆损坏("corrupted double-linked list"。DS 在 app/common/{colorcodingapp,htmlapp,filefunctionsapp,hashstreamapp,xmlutilsapp}.cpp 里用与 engine 完全相同的限定名定义了 ColorCoding::colorsHtml::k_block_tags 等符号。可执行文件与 liboakengine.so 双定义 → ELF 符号介入合并存储 → 静态对象被双重构造、双重析构 → double-free。这是 timeline-testsolive-gtest 退出即崩的根因。修复app/CMakeLists.txt 对这 5 个文件加 set_source_files_properties(... COMPILE_OPTIONS "-fvisibility=hidden")(已做)。教训见 §6.6-R1。
  2. app/common/nodeimpl.cpp 是死代码。DS 创建了它(重定义 Node::link/unlink/copy_inputs)但从未注册进 app/CMakeLists.txt,三个符号仍从 .so 导入。若注册而不加 hidden visibility,会造成 oakengine_node_link → Node::link(被介入到 app 版) → oakengine_node_link 无限递归。教训见 §6.6-R2;处理方案钉死在 §3.4。
  3. SpeedDurationDialog::accept() 的 undo 命令从未压栈。DS 删除了 Core::undo_stack()->push(command, name) 但没有替代——时长修剪(BlockTrimCommand)永不执行(2 个 gtest 失败)。修复:末尾补 oakengine_undo_push(command, name)(注意:2 个参数,全局栈,不传栈句柄)。
  4. ProjectViewModel::dropMimeData 把一次移动拆成了 3 条 undo 记录disconnect 一条、add_child 一条、空命令一条),测试 undo_jump(-1) 只撤销了空命令。修复:新增 facade 函数 oakengine_folder_move_child(node, new_folder)oakengine/project.h + src/capi/project.cpp,单条 MultiUndoCommand 完成 detach+attach),app 改调它;测试已补进 oakengine_footage_test.cpp::test_folder教训见 §6.6-R3。
  5. 预览请求帧路径两个 linesize 错误engine/src/capi/preview.cppframe->linesize_pixels() 填进 POD(契约是字节);viewer.cpp::display_frame_from_previewlinesize_pixels() 当字节偏移做 memcpy。修复POD 填 linesize_bytes()memcpy 用 linesize_bytes()教训见 §6.6-R4。
  6. 重建 display Frame 时 VideoParams 字段缺失。(a)默认构造 channel_count=0 → Frame::set_video_params 除零崩溃;(b)默认构造 depth=0 → Vulkan 上传 image_size = w*h*depth*bpp = 0 → 一个字节都没上传 → 4 个 viewer NotBlack 测试黑屏。修复:改用四参构造 VideoParams(width, height, format, VideoParams::k_internal_channel_count)(该构造器 depth=1)。教训见 §6.6-R5。

另:manageddisplay.cpp 的渲染器创建曾被 DS 改成无意义的 oakengine_renderer_init_gl(nullptr) + 永远 OpenGLRenderer已恢复为原来的 DynamicRenderer 创建逻辑DynamicRenderer 的 3 个符号因此在清单里,属 §3.6 待办)。

2.3 当前测试状态(K2.7 实测)

  • timeline-tests:全过(修复 #1 后)。
  • olive-gtest:除 Backends/ViewerRuntimeRewireTest.RewireToIndirectConnectionNotBlack/1Vulkan)外全过。这是当前唯一已知失败,线索与疑似根因见 §3.1。
  • 其余 42 个 ctest 在上一次全量运行中通过,但经过本批修复后尚未做最终全量复跑——接手第一步就是全量 ctest

3. 剩余工作(按符号逐项,顺序即执行顺序)

3.1 第零优先:修 RewireToIndirectConnectionNotBlack/1

现象viewer 正在显示 direct 链(footage→sequence),运行时插入 OpacityEffectfootage→opacity→sequence)后,画面变黑且 30s 超时内不再更新。同文件的 RewireToDirectConnectionNotBlack(拆节点)是过的。

排查线索(已排除项,不要重复查):帧数据、纹理上传、色彩变换、VideoParams 全部已验证正常(§2.2-5/6 修复后)。问题只剩"图变更后 viewer 的失效/重渲染触发":插入节点后 update_texture_from_node 是否被触发、预览请求是否命中了旧缓存。

  • 入手点:app/widget/viewer/viewer.cppConnectNodeEvent 订阅清单 vs HEAD 原版的 connect 清单——插入节点后 texture input 变化应触发 viewer_texture_input_changed(事件 108)→ update_waveform_view_from_mode/update_texture_from_node。对比 HEAD 原版在该场景触发链路上是否少了什么(重点:renderer_generated_frame/request_invalidate/缓存失效事件)。
  • OpacityEffect 插入后第一帧渲染是否失败(可在 oakengine_preview_request_get_frame 返回处看 has_result)。
  • 修复后:该用例 + 全量 ctest 全绿才准进入 §3.2。

3.2 静态/杂项小点(预计 6 个符号,先做这些快的)

  1. Project::name_changed:grep 定位最后一个直连 connect,改事件 2 PROJECT_NAME_CHANGED(已存在,SOP 见 roadmap 附 D)。
  2. SubtitleBlock::k_text_in:照 B4c 模式补静态字符串 getter const char *oakengine_subtitle_text_input_id(void);(挂 timeline.h),app 换调用。
  3. RenderManager::backend_to_string + RenderManager::instance_:补 v2 已钉死的契约——
    OAKENGINE_API int oakengine_render_manager_set_aggressive_garbage_collection(int enabled);
    OAKENGINE_API int oakengine_render_manager_requested_backend(void);
    OAKENGINE_API int oakengine_render_manager_backend_to_string(int backend, char *buf, int buf_size);
    
    instance_ 符号随最后一个 RenderManager::instance() 直连点消失(manageddisplay.cpp 的 requested_backend() 调用点)。
  4. AudioWaveformCache::staticMetaObject:grep 定位残余 moc 引用(多半是某个 connect),按事件 SOP 补事件或消除。

3.3 UndoCommand 3ctor / redo_now / undo_now

来源:app 直接 new engine 命令类并进栈(grep new .*Command 于 app/widget/timelinewidget、app/widget/nodeview 等)。按 v2 §5.6.5 的既定方针:逐个换 facade undoable 原语,缺的按同族模式补。不得为这些发明新机制。

3.4 Node 5 + NodeFactory 1

  • Node::link / Node::unlink / Node::copy_inputs3):app/common/nodeimpl.cpp 死代码的两个处理方案,钉死选方案 A
    • 方案 A(选这个):删除 app/common/nodeimpl.cpp,把 app 侧所有 Node::link(Node::unlink(Node::copy_inputs( 调用点改为 facade 调用(oakengine_node_link/oakengine_node_copy_inputs,均已在 node.h 存在)。grep 定位调用点(预计 <10 处)。
    • 方案 B(不推荐):注册 nodeimpl.cpp 且对该文件加 -fvisibility=hidden。只有方案 A 遇到无法改写的调用点时才用,且必须写进 roadmap 说明。
  • Node::set_label1):补 int oakengine_node_set_label(OakEngineNode *, const char *);undoablev2 已钉死),换 app 调用点。
  • Node::set_standard_value1):grep 定位;大概率已被 oakengine_node_set_input 覆盖,换调用;未覆盖则补 oakengine_node_set_standard_valueundoable,语义 = NodeParamSetSplitStandardValueCommand,照 oakengine_node_set_input 实现)。
  • Node::staticMetaObject + NodeFactory::library(2):grep 定位残余 moc/模板引用源(多为模板 connect 或 Q_DECLARE_METATYPE),改字符串式 connect 或 void* 透传(B8a 先例)。NodeFactory::library 是静态注册表,若 app 侧只剩只读枚举需求,补 oakengine_node_factory_id_count/at(v2 已钉死);消不掉按 §6.4 格式进豁免清单并写理由。

3.5 staticMetaObject 残留(ColorManager / Folder / Project / Node

逐个 grep 定位 moc 引用源(qobject_cast、模板 connect、Q_DECLARE_METATYPE、moc 生成的 metacall)。改事件机制或字符串式 connect。消不掉的按 §6.4 格式进豁免清单(必须写理由)。

3.6 B11b GPU/帧路径(17 个符号:Renderer 3 + OpenGLRenderer 2 + DynamicRenderer 3 + Texture 2 + Frame 5 + RenderManager 中属显示路径的部分)

这是 DS 上次说"需要复杂 GPU 管线重构"而放弃的部分。决策已钉死,不需要重构,按薄封装做:

现状事实(已验证):

  • 显示路径(ManagedDisplayWidget/ViewerDisplayWidget)持有 C++ Renderer* attached_renderer_DynamicRenderer 或 OpenGLRenderer),用于 create_texture/upload/download/blit_color_managed/destroyFrame 用于 CPU 帧搬运(Frame::create/allocate/set_video_params/dtor/ctor)。
  • manageddisplay.cpp 的 DynamicRenderer 创建块已恢复为 C++ 原版(不要再动它,直到整个显示路径换完)。

执行方案(钉死,分两步)

  1. 先 Frame5viewer.cpp::display_frame_from_preview 和 viewerdisplay 的帧搬运改用——
    typedef struct OakEngineFrame OakEngineFrame;   /* owned */
    OAKENGINE_API OakEngineFrame *oakengine_frame_create(void);
    OAKENGINE_API int oakengine_frame_set_video_params(OakEngineFrame *, const oak_video_params *);
    OAKENGINE_API int oakengine_frame_allocate(OakEngineFrame *);
    OAKENGINE_API void oakengine_frame_free(OakEngineFrame *);
    
    app 侧不再直接 new olive::Frame注意display_frame_from_preview 用四参构造 VideoParams(width,height,format,k_internal_channel_count)(§2.2-6 的修复,别回退)。
  2. 再 Renderer/Texture7+3:显示 widget 的 attached_renderer_ 改为 facade 句柄。契约(v2 已钉死):
    typedef struct OakEngineTexture OakEngineTexture;  /* owned */
    OAKENGINE_API int oakengine_renderer_init_gl(void *qopengl_context);   /* QOpenGLContext 以 void* 透传,文档注明 Qt 运行时共享例外;后端选择走 RenderManager::requested_backend 语义 */
    OAKENGINE_API int oakengine_renderer_destroy(void);
    OAKENGINE_API OakEngineTexture *oakengine_renderer_create_texture(const oak_video_params *, const void *data, int linesize);
    OAKENGINE_API int oakengine_texture_upload(OakEngineTexture *, const void *data, int linesize);
    OAKENGINE_API int oakengine_texture_download(OakEngineTexture *, void *data, int linesize);
    OAKENGINE_API void oakengine_texture_free(OakEngineTexture *);
    OAKENGINE_API int oakengine_renderer_blit_color_managed(const oak_color_transform *, OakEngineTexture *, const oak_video_params *);
    
    facade 内部持有 DynamicRenderer/OpenGLRenderer 实例(与现在 manageddisplay 的选择逻辑相同);backend-neutral 的离屏纹理 + 下载回读路径同样走 texture 句柄。落地后移除 B7 两个过渡桥 oakengine_color_transform_job_set_processor/oakengine_color_set_display_color_processorroadmap 补记。 验收5 个 Backends viewer 用例继续全过(这是该路径的现成回归测试)。

3.7 TrackListRippleToolCommand ctor1

v2 已钉死为遗留评估点。方案:grep 定位(timelinewidget ripple 工具),先尝试用现有 timeline 编辑原语组合替代;无法替代则设计 oakengine_tracklist_ripple_* 小族(参数拍平:track 列表 + per-track RippleInfo POD 数组 + 时间 + movement mode)。这是最后一个符号,允许单独花时间;消不掉按 §6.4 进豁免清单(写理由)。

3.8 B11d 收口(最后做)

  1. 确认 oak-editor U _ZN5olive 只剩豁免清单 6 项;oak-render-worker 为 0。
  2. liboakengine 符号可见性收口:set_target_properties(oakengine PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON) 或 version script 白名单 oakengine_*。验证 nm -D --defined-only liboakengine.so | grep -c " T _Z" → 0。liboakcore 复查不回归。
  3. facade 覆盖审计为空(oakengine_worker_main 豁免)。
  4. 终验:全量构建 + ctest 全绿;§1 四条验收逐条核对;roadmap 附 C 标记战役完成。

4. 已完成批次(不要重做)

详见 roadmap 附 C。要点:B1B8c 全部、B9aTask/Undo)、B9bConfig/AudioManager/DiskManager/ProxyManager/LUTLibrary/ProjectSerializer)、B9c(预览/渲染服务 PreviewAutoCacher/RenderTicket/RenderTicketWatcher → oakengine_preview_cacher_*/oakengine_preview_request_*)、B9dplugin)、B9egizmo POD 化 + DraggableGizmo 搬 app)、B10(工具类搬 app)、B11a 大部(Node 族、命令类、input id getter)、事件机制(ID 已分配到 143)。

事件 ID 分配:已用到 143141/142 playback cache、143 frame cache)。新事件从 144 起


5. 每批的标准产出(SOP

  1. nm 度量基线 → 2. grep 确认实际使用点(§3 清单仅供参考,以 grep 为准)→ 3. facade 补 C 函数(§6 契约)→ 4. app 逐文件换调用 → 5. 每个新 C 函数补单元测试(注册进 engine/CMakeLists.txtmake_oakengine_test)→ 6. 全量构建 + ctest 全绿 → 7. 族符号和总数双度量对比 → 8. roadmap 附 C 补记。

6. 边界契约与硬规则(钉死,不得违反)

6.1 C ABI 头文件规则

  • 位置 engine/include/oakengine/*.h,实现在 engine/src/capi/*.cpp(注册进 engine/src/capi/CMakeLists.txt)。
  • 每个头:GPL 版权头、#ifdef __cplusplus extern "C"OAKENGINE_API 导出宏。
  • 头文件里只允许 C 类型int/int64_t/double/char*/void*、POD struct、不透明句柄 typedef。禁止 C++ 类、模板、Qt 类型、std:: 类型、引用、默认参数、重载。
  • 命名:oakengine_<族>_<动作>;错误码 OAKENGINE_OK0/ 负数 OAKENGINE_E_*
  • 字符串输出 buf/size 约定(返回所需长度不含 \0,buf=NULL,buf_size=0 查长度)。
  • 线程语义:回调/事件 = Qt::DirectConnection 等价同步调用;回调内不得反调改同一对象的编辑原语。
  • 所有权:create 返回 owned 句柄必须配套 free;borrowed 句柄在注释里写明。

6.2 undoable 编辑原语

  • 所有改图操作必须 undoable,实现照 engine/src/capi/node.cpppush_or_run 模式。
  • undo 粒度妥协是允许的(facade 单命令边界导致一次用户操作产生多条 undo 记录),代码加注释说明即可。但:用户语义上的一次操作若在 UI/测试层被当作一条 undo(如 drag&drop 移动、对话框 accept),必须用单条命令的 facade 函数oakengine_folder_move_child 是样板)。

6.3 事件机制(信号迁移唯一通道)

  • 禁止 app 直接 QObject::connect engine 对象的信号。一律 oakengine_event_subscribeapp/engineeventbridge → app 连 bridge。SOP 见 roadmap 附 D。
  • 新事件:events.h 加宏(从 144 起)、events.cpp connect_event 加 case、bridge 加信号 + dispatch、oakengine_events_test.cpp 补实测。
  • 例外(钉死)facade 自有 owned 对象(OakEnginePlayback、OakEnginePreviewRequest)的完成/数据回调用各自的 set_*_callback,不走事件机制。

6.4 豁免清单(R6 后已清空:无豁免,nm=0)

状态(R6 收尾):原"终态保留"裁决已被 R6 计划推翻并全部消除—— AudioProcessor5 符号:ctor/dtor/open/close/convert)经 P5 改为 C vtable 接口(oakengine/audio.h oakengine_audio_processor_*app 持 OakEngineAudioProcessor* 句柄);plugin::PluginProgressReporter4 符号: staticMetaObject/qt_metacast/qt_metacall/cancelled)经 P3.2 去 Q_OBJECT、 cancelled 信号改 C 回调。oakengine_worker_main 为 worker 进程入口 (非 olive:: 符号,不计入 nm 指标)。

实测:nm -D 于 oak-editor 与 oak-render-worker 的 U _ZN5olive 均为 0当前无任何豁免。

以下为 R5 冲刺时记录的 58 符号历史分类(R6 已全部清零,仅作存档):

GLM-5.2 R5 冲刺豁免清单(58 符号,分类理由)—— R6 已全部清零(nm 58→0

A. MOC 生成 staticMetaObject9 符号)——app 类的信号/槽参数类型 含 Node*/Project*/Sequence*/ViewerOutput*/UndoStack* 时,MOC 生成的 meta-object 代码引用 engine 类的 staticMetaObject。消除需更改所有 此类信号/槽签名为 C ABI 句柄类型(OakEngineNode* 等),工程量大。

  • Node::staticMetaObjectProject::staticMetaObjectSequence::staticMetaObjectViewerOutput::staticMetaObjectUndoStack::staticMetaObjectAudioWaveformCache::staticMetaObject
  • plugin::PluginProgressReporter::staticMetaObject/qt_metacast/qt_metacall

B. Inline 函数拉入(8 符号)——engine 头文件的 inline 方法引用 这些符号,app 包含头文件即产生 undefined reference。消除需创建 app 侧 handle 头(不包含 engine C++ 头)或扩 facade 覆盖所有 inline 路径。

  • Node::linkNodeLinkCommand 析构 inline 调用)
  • Node::set_standard_valueinline getter 引用)
  • Node::set_value_at_time1 处直接调用,QVariant→POD 转换复杂)
  • UndoCommand::redo_now/undo_now/UndoCommand()MultiUndoCommand inline add_child/析构调用)
  • MultiCamNode::k_current_inputSubtitleBlock::k_text_in inline 方法引用静态字符串)

C. 实时回调边界(5 符号)——v3 已预批。

  • AudioProcessorctor/dtor/open/close/convert

D. 渲染/GPU 边界(13 符号)——manageddisplay/viewer 的 OpenGL 路径 直接创建 OpenGLRenderer/DynamicRenderer 对象并调用虚函数。 facade 有 oakengine_renderer_* 但 app 仍用 C++ 对象。 消除需将整个渲染对象管理移入 engine。

  • Rendererdestroy/create_texture/blit_color_managed
  • DynamicRendererctor/init_with_open_gl_context/load
  • OpenGLRendererctor/init
  • Textureupload/download
  • Frameallocate/create/set_video_params

E. 色彩管理(6 符号)——ManagedColor/ColorProcessor 的 C++ 对象 在多个 UI 组件中使用。无 C ABI 等价物。

  • ManagedColorctor×2/set_color_input/set_color_output
  • ColorProcessorcreate/convert_color

F. 无 C ABI 等价物(17 符号)——需新增 facade 函数。

  • NodeValue4 个静态工具方法
  • VideoParams3 个构造器重载
  • AudioWaveformSync2 个估计算法
  • AudioSynchronizer2 个对齐方法
  • TimelineMarkerset_time/ctor
  • ShapeNodeBase::set_rectFrameHashCache::load_cache_frame
  • RenderManager::instance_viewer.cpp inline instance() 引用)
  • plugin::PluginProgressReporter::cancelled(信号,需事件迁移)

6.5 测试规则

  • facade 每个新 C 函数必须有单元测试(纯 C assert 风格,不依赖 GPU/QApplication;需要时 oakengine_init(OAKENGINE_INIT_HEADLESS))。
  • GL/Vulkan 测试必须可无 GPU 跳过(GTEST_SKIP 模式)。
  • 新测试注册进 engine/CMakeLists.txtmake_oakengine_test(...)

6.6 v3 新增硬规则(对应 §2.2 的六条教训)

  • R1ODR/符号介入)app 侧严禁用与 engine 相同的限定名定义任何非 inline 符号(函数或静态数据)。确需同名本地副本(B10 模式),必须对该源文件加 -fvisibility=hiddenapp/CMakeLists.txtset_source_files_properties 是现成样板)。#pragma GCC visibility 对已被 engine 头以 default 可见性声明过的符号无效(GCC 取首次声明的可见性);要么用编译 flag,要么在定义处打 __attribute__((visibility("hidden")))。验证方法:readelf -sW <binary> | c++filt | grep <符号> 必须是 HIDDEN
  • R2(注册检查):新建任何 .cpp 必须同步注册进对应 CMakeLists,并在当批验证其符号确实从 U 清单消失。app 侧定义的 engine 同名函数若不加 hidden,会通过 ELF 介入把 engine .so 内部调用劫持到 app 版,可能形成跨模块无限递归。
  • R3undo 语义)oakengine_undo_push(command, name) 只有两个参数(全局栈,不传栈句柄)。Core::instance()->undo_stack() 返回 void*,仅作事件订阅 handle 用。删除任何 push 调用时必须同步删除/替换其命令的执行路径——命令不压栈 = 静默不执行 + 内存泄漏
  • R4linesize 约定)facade POD 中的 linesize 一律是字节olive::Frame 有两个值:linesize_bytes()(字节)和 linesize_pixels()(像素 = 字节/bpp)。跨边界只传字节;engine 内部(纹理上传等)按各 API 既有约定(Vulkan/OpenGL 纹理上传收像素)。
  • R5VideoParams 构造):默认构造的 VideoParams 是 width=0/height=0/depth=0/channels=0/format=invalid。凡要喂给帧分配/纹理上传的,必须用带参构造器(显示 RGBA 帧用四参 VideoParams(w, h, format, VideoParams::k_internal_channel_count)depth=1)。depth=0 不会报错,只会让上传字节数为 0(纯黑)。
  • R6(接手验证):任何中断/交接后,第一件事是全量构建 + 全量 ctest + 对照 §3 清单 grep 复核,不要采信上一手的完成声明(包括本文 §2.3——以你实测为准)。

7. 禁止事项(硬约束)

  1. 禁止任何 git 写操作commit / add / push / restore / checkout / stash / clean / rebase。
  2. 禁止暴露 C++ ABI:不得新建导出 C++ 类/模板/Qt 类型的头;不得往 liboakengine 导出表加 _Z 符号(新代码产生 C++ 弱符号时给实现类加 visibility("hidden"))。
  3. 禁止把 Qt 类型放进 core/liboakcore 是 Qt-free)。
  4. 禁止改 worker 的 NDJSON IPC 协议;禁止重做 §4 已完成的任何批次。
  5. 禁止修改本文已钉死的签名:各 facade 头现有函数、events.h 已分配的事件 ID(1–143)、§3 各批钉死的契约签名。
  6. 禁止为追求 undo 记录合并、staticMetaObject 消除等发明新机制——按 §6.2/§6.4 妥协条款执行。
  7. 禁止降低测试标准:新 C 函数无测试不得算完成;全量 ctest 不绿不得进入下一批。
  8. 禁止重新 cmake 配置构建目录;禁止安装/卸载系统依赖;禁止改 CI/打包文件。
  9. 禁止在未验证构建状态前继续批次(§6.6-R6)。

8. 决策兜底原则

  1. roadmap 已有裁决的,从 roadmap。
  2. 能搬进 app 的纯 UI/工具代码 → 搬 app(优于 facade 化)。
  3. 纯数据类 → POD 化或头内联,优先于新增 facade 族。
  4. 必须跨边界的 → 最小 facade 族(只包 app 实际用到的成员)。
  5. GPU/帧路径、Qt 运行时耦合 → 薄封装 + void* 透传 + 文档注明例外。
  6. 以上都拿不准的:留下不动,写进 roadmap 遗留清单,继续下一点。不允许为单点发明新架构。

9. 环境备忘

  • 构建目录 cmake-build-debugNinja + Qt6Debug);另有 cmake-build-asan / cmake-build-coverage不要用
  • 单文件增量验证:rm -f cmake-build-debug/app/CMakeFiles/libolive-editor.dir/<相对路径>.o && cmake --build cmake-build-debug --target olive-editor -j$(nproc)
  • 测试素材:tests/demo.mp4tests/img.pngtests/project_with_footage.ove
  • 本机有 GPUworker/viewer 的 Vulkan 用例真实执行(OpenGL 用例 offscreen 不可绘,会 SKIP,属正常);CI 无 GPU 会 GTEST_SKIP,两者都算通过。
  • 全量 ctest 44 个约 90-140solive-gtest 占 ~85s),每批必须跑完不能裁剪。
  • 调试技巧(本批实测有效):teardown 堆崩溃用 GLIBC_TUNABLES=glibc.malloc.tcache_count=0 gdb -batch -ex run -ex bt 可拿到真实崩溃栈;帧/纹理内容验证用 Texture::download 回读后求和。