Files
oak-editor/docs/zh/plans/riir/M10-oakstorage.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

177 lines
8.6 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.
# M10 · oakstorage 拆分手册(工程持久化,数据库替换预备)
> 内容:工程文件的读写——`engine/node/project/serializer/` 的**落盘路径**
> Load/Save/SaveData/LoadData 的文件分支 + 版本化 serializerXXXXXX 族)
> 与 `engine/task/project/{load,save,loadotio,saveotio}` 的**文件 IO 部分**。
> **不含**剪贴板序列化(copy/paste 的节点图 XML 留在 oaknode 的
> serializer 族,见 M3)。
> 依赖:oaknodeproject/root/序列化建图取图)、oakcommon。
> 被依赖:oaktaskload/save 任务委托)、facade。
> 拆分顺序:M3aoaknode 之后、oakserialize 同批)。
>
> **本模块存在的理由**:把"工程从哪来、存到哪去"收敛成唯一模块、唯一
> 接口,当前后端是 XML .ove 文件,**未来整体替换为数据库**。替换时新增
> 一个后端实现并注册即可,oaktask / facade / app 零改动。因此本模块的
> 接口按"存储后端无关"设计:URI 寻址、手工虚函数表、字节流 + 句柄,
> 接口里**不出现任何文件路径特有的概念以外的存储语义**(事务、连接串
> 等都藏在后端内部)。
## 1. 目标形态
```
oakstorage/
include/oakstorage/{storage.h, backend.h, types.h, export.h}
src/
capi_storage.cpp # storage.h 实现:URI 分发 + 会话
backends/ove_xml/ # 内建后端:XML .ove(现有 serializer 落盘路径迁入)
backends/... # 未来:oakdb(数据库后端)
tests/ # oakstorage_gtest
```
## 2. 冻结 C API
遵守 01 §0 铁律:纯 C、有类型不透明句柄、init/free、self 首参、
多态用手工虚函数表。
### 2.1 `oakstorage/types.h`
```c
/* 错误码(0=OK,负值错误;版本/格式探测用正值信息码) */
#define OAKSTORAGE_OK 0
#define OAKSTORAGE_TOO_OLD 1 /* 工程版本过旧(信息码) */
#define OAKSTORAGE_TOO_NEW 2 /* 工程版本过新 */
#define OAKSTORAGE_UNKNOWN_VERSION 3
#define OAKSTORAGE_E_INVALID -1
#define OAKSTORAGE_E_STATE -2
#define OAKSTORAGE_E_NOT_FOUND -3
#define OAKSTORAGE_E_FAILED -4
#define OAKSTORAGE_E_NO_BACKEND -5 /* 无后端认领该 URI */
#define OAKSTORAGE_E_FORMAT -6 /* 解析失败(XML/DB 约束) */
#define OAKSTORAGE_E_IO -7 /* 读写失败 */
```
### 2.2 `oakstorage/storage.h`(消费侧主接口)
```c
/* 打开的工程会话:包裹一个已加载(或待保存)的 oakmodel Project。
* owned 句柄,配对 oakstorage_project_free。 */
typedef struct OakStorageProject OakStorageProject;
/* --- 静态函数(无 self,对应"类方法" ---
* 全部是同步命令:成败与结果全在返回值/出参里,调用方知道影响,
* 由调用方(oaktask/facade)负责对外通知;本模块无任何回调/事件。 */
/* 探测 URI:返回认领该 URI 的后端名(buf/size,先 NULL 查长度),
* 或负值(OAKSTORAGE_E_NO_BACKEND)。不写盘、不建会话。 */
OAKSTORAGE_API int oakstorage_probe(const char *uri, char *buf, int buf_size);
/* 打开工程(load)。URI scheme 选后端:
* file:///path/to/proj.ove → ove-xml 后端
* file:///…/proj.otio → otio 后端(import 语义)
* oakdb://… → 未来数据库后端
* 失败返回 NULL,细节经 oakstorage_last_error。 */
OAKSTORAGE_API OakStorageProject *oakstorage_open(
const char *uri, int *result_code);
/* 把 project 保存到 URIsave / save-as)。
* `options`:位掩码,OAKSTORAGE_SAVE_COMPRESS 等;后端忽略不识别的位。 */
#define OAKSTORAGE_SAVE_COMPRESS 0x1
OAKSTORAGE_API int oakstorage_save(OakNodeProject *project,
const char *uri, unsigned options);
/* --- 成员函数(self 首参) --- */
OAKSTORAGE_API void oakstorage_project_free(OakStorageProject *self);
/* 取出工程句柄:所有权转移给调用方(此后 self 为空壳,仍须 free)。
* 对应 oakengine/task.h 的 take_project 语义。 */
OAKSTORAGE_API OakNodeProject *oakstorage_project_take_project(
OakStorageProject *self);
/* borrowed:不转移所有权 */
OAKSTORAGE_API OakNodeProject *oakstorage_project_project(
const OakStorageProject *self);
/* 会话来源 URIbuf/size */
OAKSTORAGE_API int oakstorage_project_uri(const OakStorageProject *self,
char *buf, int buf_size);
OAKSTORAGE_API int oakstorage_last_error(char *buf, int buf_size);
OAKSTORAGE_API int oakstorage_debug_alive_count(void); /* 测试专用 */
```
### 2.3 `oakstorage/backend.h`(手工虚函数表——后端注册接口)
多态按 01 §0.4:纯 C 函数指针表,提供侧(后端)填充,oakstorage 核心
经表调用。**后端实现不进公共头**;数据库后端未来只是多注册一行。
```c
/* 存储后端虚表。所有函数必需;返回码用 OAKSTORAGE_*。
* 句柄协议:load 成功时 *out_project 收到 owned OakNodeProject*
* 后端可在 vtable 之外持有任意私有状态(连接池、事务句柄等)。 */
typedef struct OakStorageBackend {
const char *name; /* "ove-xml" / "otio" / "oakdb"(静态字符串) */
const char *uri_scheme; /* "file" / "oakdb";同一 scheme 可注册多个
* 后端,按 can_handle 顺序裁决 */
/* 是否认领该 URI(后缀、magic bytes、连接可达性等,后端自决) */
int (*can_handle)(const char *uri);
/* 加载:URI → owned Project 句柄;*result_code 收 OAKSTORAGE_* */
OakNodeProject *(*load)(const char *uri, int *result_code,
char *err_buf, int err_buf_size);
/* 保存:Project → URIoptions 透传 storage.h 的位掩码 */
int (*save)(OakNodeProject *project, const char *uri, unsigned options,
char *err_buf, int err_buf_size);
} OakStorageBackend;
/* 注册/注销。oakstorage 核心不拷贝表体——后端必须保证表与 name 字符串
* 在 unregister 前存活(内建后端为静态存储期,天然满足)。 */
OAKSTORAGE_API int oakstorage_backend_register(const OakStorageBackend *backend);
OAKSTORAGE_API int oakstorage_backend_unregister(const char *name);
```
**数据库替换路径(未来的活,接口已预留)**
1.`backends/oakdb/``can_handle``oakdb://``load/save` 走 SQL
建表 schema 是后端私事;
2. `oakstorage_backend_register(&oakdb_backend);` 一行接入;
3. oaktask/facade/app 不动;`file://` 的 .ove 后端继续共存(迁移期
双后端并存,经 URI 显式选择)。
**反向约束**:任何"必须改本手册 §2.2 才能接数据库"的需求,说明接口
冻结有洞——先改本手册再动手。
### 2.4 与 oaktask 的边界(任务只是壳)
M8 的任务工厂保留原签名,实现改为薄委托:
```c
/* oaktask/project.cpp(概念) */
OakTaskTask *oaktask_create_project_load(const char *filename) {
/* 任务体内:oakstorage_open(uri) → 完成回调里
* oakstorage_project_take_project() */
}
```
import/conform/precache 等非工程 IO 任务不经 oakstorage。
## 3. 切割点
| 现状 | 处理 |
|---|---|
| `node/project/serializer/*` 落盘路径在 oaknode 内 | 文件分支 + serializerXXXXXX 版本族迁入 `oakstorage/backends/ove_xml/`;**节点图 XML 生成/解析(SaveData/LoadData 的内存形态)仍属 oaknode**ove-xml 后端经 oaknode C ABI`oaknode_serializer_*` 族)取图/建图。剪贴板分支留 oaknodeM3/M3b |
| `task/project/load|save` 含文件 IO | IO 部分下沉 oakstorage;task 保留任务编排(进度、取消、事件) |
| `task/project/loadotio|saveotio` | 同上,注册为 "otio" 后端(scheme=filecan_handle 认 .otio |
| serializer 对 Qt 文件对话框/布局的引用 | 布局信息(SerializedLayoutInfo)随保存走 options 的不透明 blobbuf/size),不进入本手册冻结面 |
## 4. 测试(映射 03 §2/§3
- **round-trip 字节一致**(金标准):project_with_footage.ove →
`oakstorage_open``oakstorage_save` 到临时 URI → 两文件字节一致;
再 load 后 `oakstorage_project_project()` 非空、root 非空。
- probe.ove(压缩/未压缩各一)、.otio、未知 scheme →
E_NO_BACKEND。
- 错误路径:不存在文件 open → NULL + last_error 非空;TOO_NEW 版本
头 → result_code = OAKSTORAGE_TOO_NEW。
- 后端虚表:注册一个内存 mock 后端(`mem://`load/save 记日志),
断言 open/save 全走虚表、unregister 后 probe 报 E_NO_BACKEND——
**这条用例就是"数据库可插拔"的接口验证**
- `oakstorage_debug_alive_count()`open/take/free 配对无泄漏。