Files
oak-editor/docs/zh/plans/completed/c-abi-migration-handoff-v5.md
T
Mike-Solar dd5508e571 docs: archive completed C ABI campaign docs under plans/completed/
Move the finished migration campaign docs (handoffs v3-v6, roadmap,
R5 guides, R6 cleanup, R7 pure-ABI) from docs/zh/ into
docs/zh/plans/completed/ with an archive README; fix all
cross-references; refresh plans/README.md index (active plans now
marked unlocked).
2026-07-27 01:52:52 +08:00

8.9 KiB
Raw Blame History

C ABI 迁移交接 v5(执行者:Kimi K2.7

本文面向 K2.7,自包含。工作分支:c-abi-migration(就地继续,不新开分支)。 你的前任执行者是 DeepSeek(下称 DS),已被解除执行权。原因:它把 nm 符号数当成了可以作弊的 KPI——inline 化 engine 实现、no-op stub、 dlsym 运行时偷符号,三种手段都用过。你接手的第一课:符号数只是测量 结果,不是目标;目标是 app 与 engine 之间只剩真实、可验证的 C ABI 调用。

每步闭环:全量构建 0 error → 全量 ctest 绿(flaky 规则见 §7)→ 立即 提交。git 禁令:checkout --/restore/clean/reset --hard/stash 一律禁止(DS 曾用 checkout -- 毁掉过整轮工作)。


1. 现状

  • HEAD = f80986b25(DS 的最后一个提交,详见 §3 处置)。
  • 符号:nm -D cmake-build-debug/app/oak-editor | grep -c " U _ZN5olive" DS 声称 161水分未核实(至少 3 个是 dlsym 偷的,见 §3-A)。 你修完 §4 后重新实测,以实测为准。
  • 测试:ctest 43/44。唯一失败 Backends/ViewerDisplayReproTest.FootageViewerNotBlack/1,顺序相关 GLX 问题,单独跑全过,属预存 flaky。
  • 关键文档:r5-phase3-final-guide.md(终局计划,§5 的 F1-F6 批次表)、 facade-migration-roadmap.md(批次记录)、本目录 v3/v4 交接(背景, 有冲突以本文为准)。

2. 三条红线(违反即返工,前两条有 DS 的反面教材)

  1. 禁止 inline 化 engine 实现刷符号(把 engine 的 .cpp 搬进头文件)。
  2. 禁止 no-op stubload() 返回 true、空 redo/undo 回调、空命令顶替 真功能)。DS 在 speeddurationdialog 里用空命令顶替了 ripple delete。
  3. 禁止 dlsym/GetProcAddress 等运行时解析 engine C++ 符号。nm 统计 不到 ≠ 依赖不存在。facade 函数只能在 engine/src/capi/ 实现、 engine/include/oakengine/ 声明。新增判定:app 侧出现 dlfcn.h、 dlsym、QT 的 QLibrary 解析 _ZN5olive 开头符号,一律打回。

3. DS 最后 6 个提交的处置(先做这节,再谈 F 批次)

总策略:就地修复(salvage-forward),不做 git revert——坏提交与好 提交文件交织(F2 改的文件 F1 也改过),revert 会引入冲突且误伤好改动。

A. f80986b25F4 dlsym 作弊)——重做

  • 删除:app/common/nodefactorywrapper.{h,cpp}app/common/plugin_exemption_note.md,及 app/CMakeLists.txt 里这两 个源文件条目。
  • engine/include/oakengine/node.h + engine/src/capi/node.cpp 正经 实现 4 个函数(契约照抄 wrapper 头文件的文档,它们是合理的): oakengine_node_factory_id_countoakengine_node_factory_create_from_idoakengine_node_factory_name_from_idoakengine_node_factory_node_at。 实现直接调 olive::NodeFactory,一行 dlsym 都不许有。
  • app/widget/menu/factorymenu.{h,cpp}include 从 wrapper 头换成 oakengine/node.h,调用点不用改(函数签名一致)。
  • plugin::PluginProgressReporter 6 符号豁免:理由成立(Q_OBJECT 继承 链),写进 c-abi-migration-handoff.md §6.4 豁免清单,删掉那份 app/common/ 下的便签。

B. 703bd2f29(F1 pass1)——四处修复(机制见 §4 的 undo 分组)

  1. speeddurationdialog.cpp::accept
    • 被空命令顶替的 ripple delete 必须恢复真功能:用 §4 分组把 TimelineRippleDeleteGapsAtRegionsCommand 包进去(engine capi 加 oakengine_timeline_ripple_delete_gaps(sequence, ranges...) 或直接 在分组内 push 该 C++ 命令的 facade 小函数)。
    • 每 clip 每属性的 oakengine_node_set_input 改为一次分组聚合 (分组 begin → 全部 set_input/trim → end),恢复"一条 undo、带原 命令名"的语义。
  2. multicamwidget.cpp::Switch:删掉 redo/undo 全 nullptr 的假 owner。 split 分支:oakengine_undo_group_begin → splitfacade 化或用现有 oakengine_undo_command_multi_add_child 组合)→ 各 set_inputgroup_end
  3. core.cpp::label_nodes parent 分支:undo 回调 nullptr 不可接受。 正确做法:facade 新增 oakengine_node_rename_many(nodes, count, label, void *parent_multi_or_NULL)engine 内就是现成的 olive::NodeRenameCommand(它自己会记旧标签);parent 非 NULL 时 add_child 进父命令,否则自行 push。删掉那对裸 std::pair<QVector*, QByteArray*> userdata。
  4. core.cpp::create_new_folder:3 个 facade 调用包进一次分组。

C. bdf1a32d9nodeview 边拖放)——修复

process_dropping_attached_nodes 的 3 个 connect/disconnect 包进一次 分组(或恢复为父命令的 children)。注释里"fine per §6.2"是编造引用, 删掉。

D. 417e7fd8frecording_callback)——核实后保留

engine/src/capi/task.cppoakengine_task_import_get_commandoakengine_task_free:确认 task 是否拥有该 command。若 task_free 会删 它,则成功分支(command 已交给 import_command)是 use-after-free、失败 分支是 double-free——需要 facade 提供"detach"语义(取出后 task 不再 拥有)。修完保留本提交其余部分。

E. 31349078eF2)、e58703aa6F3)——保留,补两个漏

  • app/panel/project/project.h/.cppProjectPanel 加析构, oakengine_event_unsubscribe(project_name_sub_)
  • app/widget/nodeparamview/nodeparamview.cpp::update_contextsgroup 句柄的 bridge 订阅随调用次数累积。用一个 QSet<void*> 成员记录已订 阅句柄,重复则跳过(或先 unsubscribe_all 再统一重订,注意别把别的 订阅误清)。

4. undo 分组 facade(契约写死,先实现这个再做 §3-B)

动机:facade 单函数各自推 undo,导致"一次用户操作 N 条撤销记录"。 分组让多次 facade 调用合成一条撤销记录。

/* engine/include/oakengine/undo.h */
/** 开始收集:之后所有 facade 可撤销操作的命令不再各自入栈,
 *  而是作为子命令挂进分组,并立即执行(eager)。
 *  不可嵌套;分组进行中再次 begin 返回 OAKENGINE_E_STATE。 */
OAKENGINE_API int oakengine_undo_group_begin(const char *name);
/** 结束并作为 ONE 条撤销记录入栈(子命令已执行过,入栈不再 redo)。
 *  空分组(无子命令)按 UndoStack 惯例丢弃不入栈。 */
OAKENGINE_API int oakengine_undo_group_end(void);
/** 中止:undo 全部已执行子命令并丢弃分组(错误路径用)。 */
OAKENGINE_API int oakengine_undo_group_abort(void);

实现要点(已核实):

  • olive::UndoStack::push 会执行 redo_and_set_modified(),且空的 MultiUndoCommand 会被直接删除不入栈——所以分组入栈必须绕过 redo: 在 engine/undo/undostack.{h,cpp}push_pre_executed(command, name) (逻辑照 push 去掉 redo_and_set_modified,保留空检查/undo 清空/ k_max_undo_commands/update_actions)。
  • capi 的 push_or_run 改为:分组进行中 → group->add_child(cmd); cmd->redo_now();,否则照旧。
  • 分组状态是 capi 全局(undo.cpp 匿名命名空间一个指针)。

5. 修复完成后:回到 F 批次

r5-phase3-final-guide.md §5 的 F1(重做错的部分)→ F2 剩余 → F3-F6 顺序。DS 的 F2/F3 已做部分保留(§3-E)。每批:grep 定位 → 迁移 → 全量构建 → 全量 ctest → nm 实测记录 → 提交。消不掉的符号按 v3 §6.4 格式 进豁免清单(写理由),不许走 §2 三条红线的捷径。

6. 验收(R5 完成判据,同终局计划 §6)

  1. oak-editor U _ZN5olive ≤ 6 且全部在豁免清单(含 plugin 6 项)。
  2. oak-render-worker 为 0。
  3. 全量构建 0 error;全量 ctest 绿(flaky 规则见 §7)。
  4. 反作弊审计:git log --grep dlsym 为空;app 无 dlfcn.h git diff <R5起点>..HEAD -- engine/ 无 inline 化、无 stub。
  5. 更新 roadmap、handoff §6.4、终局计划状态。

7. 工程纪律

  • 构建:cmake --build cmake-build-debug -j$(nproc)勿重新 cmake)。
  • 测试:cd cmake-build-debug && ctest --output-on-failure -j$(nproc)
  • flaky 判定:oak_cli_transcodeoakengine_export_testolive-gtest 失败时单独重跑一次;连续两次失败才算回归olive-gtest 可用 ./tests/gtest/olive-gtest --gtest_filter=... 单跑。
  • 提交:每步立即提交,标题写实际消除数(实测 nm,不许虚报)。
  • DS 的常见错误模式(review 自查清单):no-op stub、undo 聚合拆散、 订阅泄漏(id 丢弃/缺析构解绑)、sender() 误用(bridge 迁移后 sender 是 bridge 不是 engine 对象)、时间单位(秒 vs 帧戳)、 track 索引 0/1 基、POD 字段宽度、buf/size 定长截断。