docs(riir): design inter-module C ABI interfaces and split out oakstorage

- riir.md: split project file read/write into standalone oakstorage
  module (pluggable backend, DB-replaceable); add Mermaid data flow
  diagram of all modules; adjust batch table and milestones
- riir/04-interfaces.md: new provides/consumes contract matrix for
  all modules
- riir/M10-oakstorage.md: new manual with frozen C API (typed opaque
  handles, manual vtable for storage backends, URI addressing)
- riir/01: codify pure-C OO interface rules (typed handles instead of
  void*, init/free pairs, manual vtables, no C++ objects or member
  calls across shared library boundaries); upper layers issue commands
  only, no module-to-module event subscriptions (async task callbacks
  excepted)
- sync 00/02/03 and M2-M9 manuals: remove subscribe APIs, replace
  void* with typed handles, update tests accordingly
This commit is contained in:
2026-08-05 14:48:08 +08:00
parent b55d626c0c
commit 774eda75fe
14 changed files with 563 additions and 97 deletions
+54 -29
View File
@@ -4,6 +4,32 @@
> C API 设计与适配类实现都必须照此执行。命名、内存所有权、错误码、
> 线程与信号的处理在此**冻结**。
## 0. 接口铁律(2026-08 修订,优先级高于本文件其余各节)
模块边界上的接口必须是**纯 C 的、面向对象的**:
1. **对象 = 不透明句柄**。每个跨界类型是一个不透明结构体指针:
`typedef struct OakModClazz OakModClazz;`(**手写 struct 标签的不透明
指针**,禁止用 `void *` 充当对象——现有代码里 `void *parent_qobject`
`void *oaktask_import_take_command()` 这类用法是反面教材,新接口一律
禁止,旧接口在所属模块拆分时顺手改为有类型句柄)。
2. **构造/析构 = init/free 函数对**`oakmod_clazz_init*()` /
`oakmod_clazz_free()`。free 对 NULL 是 no-op。
3. **成员函数 = 首参为 self 句柄的普通函数**
`oakmod_clazz_<func>(OakModClazz *self, ...)`;静态成员函数无 self
`_s` 后缀)。
4. **多态 = 手工虚函数表**。确需"基类句柄 + 多种实现"(如存储后端、
undo 命令、渲染后端插件)时,在公共头里定义纯 C 函数指针表
`typedef struct { ...; int (*save)(...); ... } OakModClazzVTable;`),
提供侧填充、消费侧经表调用,**不得让 C++ vtable 跨界**。模板见
`M10-oakstorage.md` §2.3 的 `OakStorageBackend`
5. **禁止 C++ 对象跨越动态库边界**:边界上只出现 C 类型(整数、double、
指针、`const char *`、纯 C POD、不透明句柄)。C++ 类实例、引用、
`std::` 类型、Qt 类型一律不得出现在任何模块的 `include/` 公共头里。
6. **禁止跨界调用 C++ 成员函数**:消费侧对提供侧对象的一切操作必须经
该对象的 C ABI 函数;拿到句柄后 `reinterpret_cast` 回 C++ 指针再调
成员函数视为违规(nm 审计 + 代码评审双保险)。
## 1. 提供侧:C API 层
对被消费的每个 C++ 类 `Clazz`,模块在 `include/oak<mod>/clazz.h`
@@ -32,7 +58,7 @@ OAKMOD_API <ret> oakmod_clazz_<func>_s(/* 参数 */);
3. 多个构造重载用后缀区分:`oakmod_clazz_init`(默认)、
`oakmod_clazz_init_from_file``oakmod_clazz_init_copy` 等。
4. 命名全小写,模块前缀 `oak<mod>_`oakundo/oaknode/oaktimeline/
oakcodec/oakrender/oaktask/oakaudio/oakplugin/oakcommon)。
oakcodec/oakrender/oaktask/oakaudio/oakplugin/oakcommon/oakstorage)。
5. 导出宏 `OAKMOD_API``oakengine/export.h` 样式
`__attribute__((visibility("default")))`),模块编译加
`-fvisibility=hidden`——每个模块**出生即 visibility 干净**
@@ -99,22 +125,26 @@ C ABI 上只允许:整数、`double`、`int64_t`、指针、`const char *`、
| `Qt::enum`/内部枚举 | `int`(取值表写进手册,两侧枚举**序数一致性**用 static_assert 或测试钉死) |
| `std::shared_ptr<T>` | 不透明句柄 + retain/free(协议见 display.h R7-A 的先例) |
## 4. 信号、回调与线程
## 4. 信号、回调与线程(2026-08 修订:上层对下层只有命令)
Qt 信号不许跨模块。处理优先级:
Qt 信号不许跨模块;**下层对上层也不许持有回调**——上层调用下层时
必须知道其影响(改了什么全在返回值/出参里),变更通知由**调用方所在
层**发出,不经下层反向通知。各模块 C ABI 因此一律不含
subscribe/unsubscribe 类函数。处理规则:
1. **回调注册**`oakmod_clazz_set_<event>_cb(self, fn, userdata)`
提供侧在发信号处调 `fn(event_pod, userdata)`。userdata 所有权归
注册方,适配类析构时先 `set_*_cb(self, NULL, NULL)` 反注册
2. **事件总线**:模块级通知(非单对象)用
`oakmod_subscribe(event_id, fn, userdata)` → 返回订阅 id
`oakmod_unsubscribe(id)`——照 `oakengine/events.h` 的现成模式。
3. 线程语义照现状:提供侧在发射线程同步调回调(DirectConnection
1. **同步命令**提供侧把结果放在返回值/出参;消费侧适配类在调用后
自行发 Qt 信号(适配类知道刚执行了什么命令,见 §7 例)。原
`connect()` 到适配类信号的 widget 代码零改动
2. **唯一例外——异步任务**:后台执行单元(oaktask 任务、oakrender
渲染 ticket)提交时拿不到结果,允许进度/完成回调作为该命令的
返回通道:`oak<mod>_<async>_start(handle, done_fn, userdata)` 形式,
一次性语义,完成后自动失效。
3. 线程语义照现状:异步回调在发射线程同步调用(DirectConnection
等价),需要跨线程排队是消费侧适配类自己的事(它可以用
`QMetaObject::invokeMethod(..., Qt::QueuedConnection)`)。
4. **适配类可以把 C 回调再转回 Qt 信号**:适配类继承 QObject、
静态 trampoline 里 `emit` 同名信号——消费侧原有 `connect()` 全部
零改动。这是大多数 widget 侧适配的默认做法
4. **facade→app 的 `oakengine_event` 通道不在此列**:那是引擎对最外层
的唯一通知机制(riir.md §6.1),事件由 facade 在命令完成后发射,
不由下层模块直接发射
## 5. 继承与虚函数
@@ -139,40 +169,35 @@ Qt 信号不许跨模块。处理优先级:
```c
/* oakundo/include/oakundo/undostack.h */
typedef struct OakUndoStack OakUndoStack;
OAKUNDO_API OakUndoStack *oakundo_undostack_init(void *parent_qobject);
typedef struct OakUndoObjectParent OakUndoObjectParent; /* borrowed QObject 挂载点 */
OAKUNDO_API OakUndoStack *oakundo_undostack_init(
const OakUndoObjectParent *parent);
OAKUNDO_API void oakundo_undostack_free(OakUndoStack *self);
OAKUNDO_API void oakundo_undostack_push(OakUndoStack *self,
OakUndoCommand *cmd, const char *name);
OAKUNDO_API int oakundo_undostack_can_undo(const OakUndoStack *self);
/* ... 完整表见 M2 手册 ... */
OAKUNDO_API int64_t oakundo_undostack_subscribe(OakUndoStack *self,
int event_id, oakundo_event_fn fn, void *userdata);
OAKUNDO_API void oakundo_unsubscribe(int64_t id);
OAKUNDO_API int64_t oakundo_undostack_index(const OakUndoStack *self);
/* ... 完整表见 M2 手册(纯命令接口,无任何 subscribe ... */
```
```cpp
// 消费侧 adapter/undostack.h —— app/other 模块里 connect() 零改动
// 消费侧 adapter/undostack.h —— app/other 模块里 connect() 零改动
// 通知规则(§4):适配类发了变更命令,它知道影响,信号由适配类自己 emit。
class UndoStack : public QObject {
Q_OBJECT
public:
explicit UndoStack(QObject *p = nullptr)
: QObject(p), h_(oakundo_undostack_init(p)) {
sub_ = oakundo_undostack_subscribe(h_, OAKUNDO_EVENT_INDEX_CHANGED,
&UndoStack::tramp, this);
}
~UndoStack() override { oakundo_unsubscribe(sub_); oakundo_undostack_free(h_); }
: QObject(p), h_(oakundo_undostack_init(
reinterpret_cast<const OakUndoObjectParent *>(p))) {}
~UndoStack() override { oakundo_undostack_free(h_); }
void push(UndoCommand *c, const QString &n) {
oakundo_undostack_push(h_, c->handle(), n.toUtf8().constData());
emit index_changed(int(oakundo_undostack_index(h_))); // 命令后自发通知
}
bool canUndo() const { return oakundo_undostack_can_undo(h_) != 0; }
signals:
void index_changed(int);
private:
static void tramp(int event_id, int64_t a, int64_t b, void *ud) {
if (event_id == OAKUNDO_EVENT_INDEX_CHANGED)
emit static_cast<UndoStack *>(ud)->index_changed(int(a));
}
OakUndoStack *h_;
int64_t sub_;
};
```