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

160 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 stub**`load()` 返回 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. `f80986b25`F4 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_count``oakengine_node_factory_create_from_id`
`oakengine_node_factory_name_from_id``oakengine_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_input`
`group_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. `bdf1a32d9`nodeview 边拖放)——修复
`process_dropping_attached_nodes` 的 3 个 connect/disconnect 包进一次
分组(或恢复为父命令的 children)。注释里"fine per §6.2"是编造引用,
删掉。
### D. `417e7fd8f`recording_callback)——核实后保留
`engine/src/capi/task.cpp``oakengine_task_import_get_command`
`oakengine_task_free`:确认 task 是否拥有该 command。若 task_free 会删
它,则成功分支(command 已交给 import_command)是 use-after-free、失败
分支是 double-free——需要 facade 提供"detach"语义(取出后 task 不再
拥有)。修完保留本提交其余部分。
### E. `31349078e`F2)、`e58703aa6`F3)——保留,补两个漏
- `app/panel/project/project.h/.cpp`ProjectPanel 加析构,
`oakengine_event_unsubscribe(project_name_sub_)`
- `app/widget/nodeparamview/nodeparamview.cpp::update_contexts`group
句柄的 bridge 订阅随调用次数累积。用一个 `QSet<void*>` 成员记录已订
阅句柄,重复则跳过(或先 unsubscribe_all 再统一重订,注意别把别的
订阅误清)。
## 4. undo 分组 facade(契约写死,先实现这个再做 §3-B)
动机:facade 单函数各自推 undo,导致"一次用户操作 N 条撤销记录"。
分组让多次 facade 调用合成一条撤销记录。
```c
/* 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_transcode``oakengine_export_test`
`olive-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 定长截断。