Files
oak-editor/docs/zh/plans/riir/01-adapter-pattern.md
T
Mike-Solar 774eda75fe 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
2026-08-05 14:48:08 +08:00

9.7 KiB
Raw Blame History

01 · 双层适配器模式(所有模块共用规范)

本规范是拆分能在"只拆不写"约束下成立的核心机制。任何模块的 C API 设计与适配类实现都必须照此执行。命名、内存所有权、错误码、 线程与信号的处理在此冻结

0. 接口铁律(2026-08 修订,优先级高于本文件其余各节)

模块边界上的接口必须是纯 C 的、面向对象的

  1. 对象 = 不透明句柄。每个跨界类型是一个不透明结构体指针: typedef struct OakModClazz OakModClazz;手写 struct 标签的不透明 指针,禁止用 void * 充当对象——现有代码里 void *parent_qobjectvoid *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 里暴露一组 extern "C" 函数:

/* 构造/析构 */
OAKMOD_API OakModClazz *oakmod_clazz_init(/* 与某个构造函数对应的参数 */);
OAKMOD_API void       oakmod_clazz_free(OakModClazz *self);

/* 普通成员函数:self 为第一参数,其余参数按 §3 POD 化 */
OAKMOD_API <ret>    oakmod_clazz_<func>(OakModClazz *self, ...);

/* 静态成员函数:无 self */
OAKMOD_API <ret>    oakmod_clazz_<func>_s(/* 参数 */);

规则:

  1. OakModClazz不透明类型typedef struct OakModClazz OakModClazz;), 提供侧实现里它就是 olive::Clazz*reinterpret_cast),消费侧 永远无法解引用。
  2. init 每个对应一个实际在用的构造函数重载;返回 NULL 表示失败。 free 对 NULL 是 no-op。所有权:init 创建的对象归调用方,必须 配对 free;借用指针(不转移所有权)在文档注释里写 /* borrowed */
  3. 多个构造重载用后缀区分:oakmod_clazz_init(默认)、 oakmod_clazz_init_from_fileoakmod_clazz_init_copy 等。
  4. 命名全小写,模块前缀 oak<mod>_oakundo/oaknode/oaktimeline/ oakcodec/oakrender/oaktask/oakaudio/oakplugin/oakcommon/oakstorage)。
  5. 导出宏 OAKMOD_APIoakengine/export.h 样式 __attribute__((visibility("default")))),模块编译加 -fvisibility=hidden——每个模块出生即 visibility 干净 R7-B 的全局收口变成顺水推舟。

2. 消费侧:同名 C++ 适配类

消费模块里放一个与原始类同名的适配类(放在消费侧私有头 <mod>/adapter/clazz.hnamespace 保持 olive):

namespace olive {

class Clazz {
public:
	Clazz(/* 原构造签名 */)
		: h_(oakmod_clazz_init(/* 相同参数 */)) {}
	~Clazz() { oakmod_clazz_free(h_); }

	// 原成员函数签名原样保留,体内转发一行
	Ret func(Arg a) { return oakmod_clazz_func(h_, a); }

	// 句柄逃生口(确实需要混用时的过渡手段,注释标注)
	OakModClazz *handle() const { return h_; }

private:
	OakModClazz *h_;
	// 拷贝语义与原类一致:原类不可拷贝就 = delete;
	// 原类可拷贝则 init_copy。
};

}

效果:消费模块里所有 clazz.func(a)new Clazz(...)clazz->func(a) 调用点零改动——原来 include 提供侧 C++ 头的 地方,改 include 本模块的适配头即可。这就是"双层适配器": C++ 调用点 → 同名适配类 → C ABI → 提供侧 C++ 实现

注意:

  • 适配类是逐模块私有的oakrender 消费的 Node 适配类和 oaktimeline 消费的 Node 适配类是两份独立的小头文件,各自只有 自己用到的方法子集。允许重复,禁止共享(共享就重新耦合了)。
  • 适配类只转发本模块实际用到的方法(按 02 的依赖矩阵逐个 grep 确认清单,写在模块手册里)。
  • 原类有继承体系的(如 UndoCommand 子类族),见 §5。

3. 参数与返回值的 POD 化

C ABI 上只允许:整数、doubleint64_t、指针、const char *、 以及 include/oak<mod>/types.h 里定义的纯 C POD(无构造/析构/ 方法)。Qt/C++ 类型按下表映射:

C++ 类型 C ABI 形态
QString const char *(入,UTF-8);出:buf/size 两段式(先 NULL 查长度)
olive::core::Rational 两个 int64_tnum, den);时间戳用 int64_t 帧戳
TimeRange 两个 int64_tin_ts, out_ts
QVariant/NodeValue oak_node_value PODoakengine/node.h 已有,各模块复用该定义或复制同构 POD)
VideoParams/AudioParams oak_video_params/POD 字段拍平
QList<T>/QVector<T> 指针 + count;返回集合用 count + at(i) 访问器对
Color 4 × double
Qt::enum/内部枚举 int(取值表写进手册,两侧枚举序数一致性用 static_assert 或测试钉死)
std::shared_ptr<T> 不透明句柄 + retain/free(协议见 display.h R7-A 的先例)

4. 信号、回调与线程(2026-08 修订:上层对下层只有命令)

Qt 信号不许跨模块;下层对上层也不许持有回调——上层调用下层时 必须知道其影响(改了什么全在返回值/出参里),变更通知由调用方所在 层发出,不经下层反向通知。各模块 C ABI 因此一律不含 subscribe/unsubscribe 类函数。处理规则:

  1. 同步命令:提供侧把结果放在返回值/出参;消费侧适配类在调用后 自行发 Qt 信号(适配类知道刚执行了什么命令,见 §7 例)。原 connect() 到适配类信号的 widget 代码零改动。
  2. 唯一例外——异步任务:后台执行单元(oaktask 任务、oakrender 渲染 ticket)提交时拿不到结果,允许进度/完成回调作为该命令的 返回通道:oak<mod>_<async>_start(handle, done_fn, userdata) 形式, 一次性语义,完成后自动失效。
  3. 线程语义照现状:异步回调在发射线程同步调用(DirectConnection 等价),需要跨线程排队是消费侧适配类自己的事(它可以用 QMetaObject::invokeMethod(..., Qt::QueuedConnection))。
  4. facade→app 的 oakengine_event 通道不在此列:那是引擎对最外层 的唯一通知机制(riir.md §6.1),事件由 facade 在命令完成后发射, 不由下层模块直接发射。

5. 继承与虚函数

原类的继承体系不出模块。跨界时:

  • 基类引用(如 UndoCommand *)→ 基类句柄(OakUndoCommand *)。
  • 消费侧不构造具体子类,只经提供侧的工厂函数: oakmod_clazz_create_<variant>(...)(返回基类句柄)。
  • 消费侧确实需要子类行为的(极少数,需逐个人工论证),在提供侧 加专用函数,不放虚函数跨界。

6. 错误码

沿用 facade 约定:OAKMOD_OK=0,负值错误 OAKMOD_E_INVALID/E_STATE/E_NOT_FOUND/E_FAILED),在各模块 types.h 里统一定义(值与 oakengine 的现有值对齐)。会失败的 init 返回 NULL,错误细节经 oakmod_last_error(buf, size) 取 (线程局部,照 facade 的 set_error 模式)。

7. 一个完整例子(UndoStack,M2 照此落地)

/* oakundo/include/oakundo/undostack.h */
typedef struct OakUndoStack OakUndoStack;
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);
OAKUNDO_API int64_t oakundo_undostack_index(const OakUndoStack *self);
/* ... 完整表见 M2 手册(纯命令接口,无任何 subscribe ... */
// 消费侧 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(
			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:
	OakUndoStack *h_;
};