678 lines
20 KiB
Markdown
678 lines
20 KiB
Markdown
# C API 设计规范总纲
|
||
|
||
> **必读**:本文件定义了所有 Olive/Oak 动态库的 C API 设计约定。`01-` 到 `07-` 各模块的 API 均遵循此规范。
|
||
|
||
---
|
||
|
||
## 1. 设计原则
|
||
|
||
### 1.1 不透明指针(Opaque Pointer)
|
||
|
||
所有 C++ 对象在 C 接口中均隐藏实现,仅暴露为 `struct` 的前向声明:
|
||
|
||
```c
|
||
// 公共头文件(.h)中
|
||
typedef struct OliveNodeGraph OliveNodeGraph; // 只有声明,无定义
|
||
|
||
// 实现文件(.cpp)中
|
||
struct OliveNodeGraph {
|
||
olive::NodeGraph* impl; // 实际的 C++ 对象
|
||
};
|
||
```
|
||
|
||
外部代码只能操作指针,无法解引用或 sizeof。
|
||
|
||
### 1.2 纯 C 接口
|
||
|
||
- 函数名使用 `snake_case`,前缀为 `olive_<module>_`。
|
||
- 参数和返回值仅使用 C 基础类型、结构体、不透明指针。
|
||
- 禁止使用 C++ 特性:类、引用、重载、模板、异常、`std::string`、`QString`。
|
||
- 字符串使用 `const char*`(UTF-8 编码)。
|
||
- 布尔值使用 `int`(0 = false,非 0 = true)。
|
||
|
||
### 1.3 动态库自身可以用 C++
|
||
|
||
动态库的实现文件(`.cpp`)内部可以继续使用:
|
||
- Qt(`QObject`, `QString`, `QList`, 信号槽等)
|
||
- C++ STL
|
||
- 虚函数、模板、Lambda
|
||
- 异常(但不得穿透 C 接口边界)
|
||
|
||
C 接口层只是薄薄的封装胶合层。
|
||
|
||
---
|
||
|
||
## 2. 命名规范
|
||
|
||
| 元素 | 规范 | 示例 |
|
||
|---|---|---|
|
||
| 类型名 | `Olive` + `PascalCase` | `OliveNodeGraph`, `OliveFrame` |
|
||
| 函数名 | `olive_<module>_<snake_case>` | `olive_node_graph_create`, `olive_codec_decoder_open` |
|
||
| 枚举名 | `Olive<Module><PascalCase>` | `OliveCodecResultOk`, `OliveRenderModeOffline` |
|
||
| 常量宏 | `OLIVE_<MODULE>_UPPER_SNAKE` | `OLIVE_NODE_OK`, `OLIVE_CODEC_ERROR_NOT_FOUND` |
|
||
| 版本宏 | `OLIVE_<MODULE>_API_VERSION` | `OLIVE_NODE_API_VERSION 1` |
|
||
|
||
---
|
||
|
||
## 3. 内存管理约定
|
||
|
||
### 3.1 谁创建,谁释放
|
||
|
||
- **库创建的对象**,必须由库的对应 `destroy`/`free` 函数释放。
|
||
- **主进程分配并传入的缓冲区**(如 `char*` 参数),由主进程管理,库内部只读或复制。
|
||
- **库返回的字符串/缓冲区**,必须使用库提供的 `free` 函数释放,不能用 C 标准 `free()`(因为库的堆和主进程的堆可能是分离的,尤其是在 Windows 上)。
|
||
|
||
```c
|
||
// 正确:库分配,库释放
|
||
char* xml = olive_node_graph_save_xml(graph, &len);
|
||
// ... 使用 xml ...
|
||
olive_core_free(xml); // 使用库提供的释放函数
|
||
|
||
// 错误:
|
||
free(xml); // 危险!堆可能不一致
|
||
```
|
||
|
||
### 3.2 通用释放函数
|
||
|
||
每个模块提供一个通用释放函数:
|
||
|
||
```c
|
||
void olive_core_free(void* ptr); // 释放字符串/二进制缓冲区
|
||
void olive_core_mem_free(void* ptr, size_t size); // 带大小的释放(用于安全擦除)
|
||
```
|
||
|
||
### 3.3 对象生命周期模式
|
||
|
||
```c
|
||
// 模式 A:Create/Destroy(堆分配)
|
||
OliveNodeGraph* olive_node_graph_create(void);
|
||
void olive_node_graph_destroy(OliveNodeGraph* obj);
|
||
|
||
// 模式 B:Init/Cleanup(栈分配或外部缓冲区)
|
||
int olive_frame_init(OliveFrame* frame, int w, int h, OlivePixelFormat fmt);
|
||
void olive_frame_cleanup(OliveFrame* frame);
|
||
|
||
// 模式 C:Ref/Unref(引用计数)
|
||
void olive_frame_ref(OliveFrame* frame);
|
||
void olive_frame_unref(OliveFrame* frame);
|
||
```
|
||
|
||
优先使用 **模式 A(Create/Destroy)**,因为不透明指针天然适合堆分配。
|
||
|
||
---
|
||
|
||
## 4. 错误处理
|
||
|
||
### 4.1 返回码约定
|
||
|
||
所有可能失败的函数返回 `int`:
|
||
|
||
```c
|
||
#define OLIVE_OK 0 // 成功
|
||
#define OLIVE_ERROR_GENERIC -1 // 通用错误
|
||
#define OLIVE_ERROR_INVALID -2 // 无效参数
|
||
#define OLIVE_ERROR_NOMEM -3 // 内存不足
|
||
#define OLIVE_ERROR_NOT_FOUND -4 // 找不到对象/文件
|
||
#define OLIVE_ERROR_IO -5 // IO 错误
|
||
#define OLIVE_ERROR_CANCELLED -6 // 操作被取消
|
||
#define OLIVE_ERROR_UNSUPPORTED -7 // 不支持的操作
|
||
```
|
||
|
||
### 4.2 详细错误信息
|
||
|
||
提供线程局部的错误信息获取函数:
|
||
|
||
```c
|
||
int olive_core_last_error_code(void);
|
||
const char* olive_core_last_error_string(void); // 线程安全,返回静态缓冲区或 TLS
|
||
```
|
||
|
||
实现方式:
|
||
|
||
```cpp
|
||
// .cpp 中
|
||
thread_local int g_last_error_code = OLIVE_OK;
|
||
thread_local char g_last_error_string[1024];
|
||
|
||
static void SetError(int code, const char* fmt, ...) {
|
||
g_last_error_code = code;
|
||
va_list args;
|
||
va_start(args, fmt);
|
||
vsnprintf(g_last_error_string, sizeof(g_last_error_string), fmt, args);
|
||
va_end(args);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 字符串处理
|
||
|
||
### 5.1 输入字符串
|
||
|
||
- 所有 `const char*` 参数均视为 **UTF-8 编码**。
|
||
- 库内部在边界处转换为 `QString`:
|
||
|
||
```cpp
|
||
// 封装层内部
|
||
QString qstr = QString::fromUtf8(cstr);
|
||
```
|
||
|
||
### 5.2 输出字符串
|
||
|
||
- 返回 `char*` 的函数,使用 `olive_core_free()` 释放。
|
||
- 如果只需读取而不持有,提供 `const char*` 返回版本:
|
||
|
||
```c
|
||
const char* olive_node_get_type_name(OliveNode* node); // 生命周期与 node 绑定
|
||
char* olive_node_graph_save_xml(OliveNodeGraph* graph, size_t* out_len); // 需释放
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 显式加载器(ModuleLoader)
|
||
|
||
### 6.1 设计目标
|
||
|
||
主进程通过一个统一的 `ModuleLoader` 类显式加载所有动态库,将 `dlopen`/`dlsym` 的细节隐藏。
|
||
|
||
### 6.2 C++ 封装类
|
||
|
||
```cpp
|
||
// app/moduleloader.h
|
||
#pragma once
|
||
|
||
#include <QString>
|
||
#include <QHash>
|
||
#include <functional>
|
||
|
||
namespace olive {
|
||
|
||
class ModuleLoader {
|
||
public:
|
||
ModuleLoader();
|
||
~ModuleLoader();
|
||
|
||
// 加载指定路径的动态库
|
||
bool Load(const QString& module_name, const QString& library_path);
|
||
|
||
// 卸载
|
||
void Unload(const QString& module_name);
|
||
|
||
// 获取函数指针(模板封装,内部调用 dlsym)
|
||
template<typename FuncPtr>
|
||
FuncPtr GetFunction(const QString& module_name, const char* func_name) {
|
||
return reinterpret_cast<FuncPtr>(GetFunctionRaw(module_name, func_name));
|
||
}
|
||
|
||
// 检查是否已加载
|
||
bool IsLoaded(const QString& module_name) const;
|
||
|
||
// 获取加载错误信息
|
||
QString LastError() const;
|
||
|
||
private:
|
||
void* GetFunctionRaw(const QString& module_name, const char* func_name);
|
||
|
||
struct ModuleHandle {
|
||
void* handle; // dlopen handle
|
||
QString path;
|
||
};
|
||
QHash<QString, ModuleHandle> modules_;
|
||
QString last_error_;
|
||
};
|
||
|
||
// 便捷宏:从指定模块获取函数并调用
|
||
#define OLIVE_LOAD_FUNC(loader, module, name, type) \
|
||
auto name = (loader).GetFunction<type>(module, #name); \
|
||
if (!name) { qFatal("Failed to load function: " #name " from module: " #module); }
|
||
|
||
} // namespace olive
|
||
```
|
||
|
||
### 6.3 实现(POSIX)
|
||
|
||
```cpp
|
||
// app/moduleloader.cpp
|
||
#include "moduleloader.h"
|
||
#include <dlfcn.h>
|
||
#include <QDebug>
|
||
|
||
namespace olive {
|
||
|
||
bool ModuleLoader::Load(const QString& module_name, const QString& library_path) {
|
||
if (modules_.contains(module_name)) return true;
|
||
|
||
void* handle = dlopen(library_path.toUtf8().constData(), RTLD_NOW | RTLD_LOCAL);
|
||
if (!handle) {
|
||
last_error_ = QString::fromUtf8(dlerror());
|
||
qWarning() << "Failed to load" << library_path << ":" << last_error_;
|
||
return false;
|
||
}
|
||
|
||
modules_.insert(module_name, {handle, library_path});
|
||
qInfo() << "Loaded module:" << module_name << "from" << library_path;
|
||
return true;
|
||
}
|
||
|
||
void ModuleLoader::Unload(const QString& module_name) {
|
||
auto it = modules_.find(module_name);
|
||
if (it != modules_.end()) {
|
||
dlclose(it->handle);
|
||
modules_.erase(it);
|
||
}
|
||
}
|
||
|
||
void* ModuleLoader::GetFunctionRaw(const QString& module_name, const char* func_name) {
|
||
auto it = modules_.find(module_name);
|
||
if (it == modules_.end()) return nullptr;
|
||
dlerror(); // 清除之前的错误
|
||
void* func = dlsym(it->handle, func_name);
|
||
return func;
|
||
}
|
||
|
||
} // namespace olive
|
||
```
|
||
|
||
### 6.4 实现(Windows)
|
||
|
||
```cpp
|
||
#include <windows.h>
|
||
|
||
bool ModuleLoader::Load(const QString& module_name, const QString& library_path) {
|
||
HMODULE handle = LoadLibraryW(library_path.toStdWString().c_str());
|
||
if (!handle) {
|
||
last_error_ = QString::number(GetLastError());
|
||
return false;
|
||
}
|
||
modules_.insert(module_name, {handle, library_path});
|
||
return true;
|
||
}
|
||
|
||
void* ModuleLoader::GetFunctionRaw(const QString& module_name, const char* func_name) {
|
||
auto it = modules_.find(module_name);
|
||
if (it == modules_.end()) return nullptr;
|
||
return GetProcAddress(static_cast<HMODULE>(it->handle), func_name);
|
||
}
|
||
```
|
||
|
||
### 6.5 使用示例
|
||
|
||
```cpp
|
||
// core.cpp 中初始化
|
||
module_loader_ = new ModuleLoader();
|
||
module_loader_->Load("core", FindLibraryPath("libolivecore.so"));
|
||
module_loader_->Load("codec", FindLibraryPath("libolivecodec.so"));
|
||
module_loader_->Load("node", FindLibraryPath("libolivenode.so"));
|
||
|
||
// 获取函数
|
||
OLIVE_LOAD_FUNC(*module_loader_, "core", olive_rational_make, OliveRational(*)(int64_t, int64_t));
|
||
|
||
OliveRational r = olive_rational_make(1001, 30000);
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 类型映射表
|
||
|
||
| C++ 类型(内部) | C 接口类型(公共) | 说明 |
|
||
|---|---|---|
|
||
| `olive::Rational` | `OliveRational` | `struct { int64_t num, den; }` |
|
||
| `olive::Color` | `OliveColor` | `struct { double r, g, b, a; }` |
|
||
| `olive::TimeRange` | `OliveTimeRange*` | 不透明指针 |
|
||
| `olive::Frame` | `OliveFrame*` | 不透明指针 |
|
||
| `olive::SampleBuffer` | `OliveSampleBuffer*` | 不透明指针 |
|
||
| `olive::PixelFormat` | `OlivePixelFormat` | `enum` |
|
||
| `olive::VideoParams` | `OliveVideoParams` | 公开结构体(POD) |
|
||
| `olive::AudioParams` | `OliveAudioParams` | 公开结构体(POD) |
|
||
| `olive::Node*` | `OliveNode*` | 不透明指针 |
|
||
| `olive::NodeGraph*` | `OliveNodeGraph*` | 不透明指针 |
|
||
| `olive::RenderTicketPtr` | `OliveRenderTicket*` | 不透明指针(引用计数内部管理) |
|
||
| `QString` | `const char*` | UTF-8 编码 |
|
||
| `QSize` | `struct { int width; int height; }` | `OliveSize` |
|
||
| `QMatrix4x4` | `float[16]` | 列优先 |
|
||
|
||
### 7.1 POD 结构体定义示例
|
||
|
||
```c
|
||
// olivecore_api.h
|
||
|
||
typedef struct {
|
||
int64_t num;
|
||
int64_t den;
|
||
} OliveRational;
|
||
|
||
typedef struct {
|
||
double r;
|
||
double g;
|
||
double b;
|
||
double a;
|
||
} OliveColor;
|
||
|
||
typedef struct {
|
||
int width;
|
||
int height;
|
||
int depth;
|
||
int channel_count;
|
||
OlivePixelFormat format;
|
||
double pixel_aspect_num;
|
||
double pixel_aspect_den;
|
||
} OliveVideoParams;
|
||
|
||
typedef struct {
|
||
int sample_rate;
|
||
int64_t channel_layout; // FFmpeg AV_CH_LAYOUT_* 值
|
||
OliveSampleFormat format;
|
||
} OliveAudioParams;
|
||
|
||
typedef struct {
|
||
int width;
|
||
int height;
|
||
} OliveSize;
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 线程安全
|
||
|
||
### 8.1 API 层面
|
||
|
||
- **默认不保证线程安全**。除非文档明确标注 `thread-safe`,否则每个 `OliveXxx*` 对象只能在创建它的线程中使用。
|
||
- 这是刻意的设计:由于渲染进程是"用完即弃"的,不存在多线程共享渲染状态的问题。
|
||
|
||
### 8.2 主进程中的线程使用
|
||
|
||
- `ModuleLoader` 本身是线程安全的(只读查找,加载/卸载在初始化/退出时串行执行)。
|
||
- UI 对象在主线程操作。
|
||
- IO/解码可以在工作线程中通过 C API 操作独立的 `OliveDecoder*` 实例。
|
||
|
||
---
|
||
|
||
## 9. 版本与 ABI 兼容性
|
||
|
||
### 9.1 API 版本号
|
||
|
||
每个模块的 C API 有一个主版本号:
|
||
|
||
```c
|
||
#define OLIVE_NODE_API_VERSION 1
|
||
|
||
int olive_node_api_version(void); // 返回 OLIVE_NODE_API_VERSION
|
||
```
|
||
|
||
### 9.2 加载时版本检查
|
||
|
||
```cpp
|
||
bool LoadNodeModule(ModuleLoader* loader, const QString& path) {
|
||
if (!loader->Load("node", path)) return false;
|
||
auto version_fn = loader->GetFunction<int(*)()>("node", "olive_node_api_version");
|
||
if (!version_fn || version_fn() != EXPECTED_NODE_API_VERSION) {
|
||
qFatal("Incompatible libolivenode.so version");
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
```
|
||
|
||
### 9.3 ABI 兼容性规则
|
||
|
||
- **允许**:新增函数、新增枚举值(在末尾)、新增结构体字段(在末尾,且文档标注"v2 起可用")。
|
||
- **不允许**:删除函数、修改函数签名、修改已有字段含义、改变枚举值顺序。
|
||
- **结构体扩展**:POD 结构体新增字段时,提供初始化宏确保旧代码不会未初始化新字段:
|
||
|
||
```c
|
||
#define OLIVE_VIDEO_PARAMS_DEFAULT { \
|
||
.width = 1920, .height = 1080, .depth = 1, \
|
||
.channel_count = 4, .format = OLIVE_PIXEL_FMT_RGBA32F, \
|
||
.pixel_aspect_num = 1.0, .pixel_aspect_den = 1.0 \
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 头文件组织
|
||
|
||
### 10.1 目录结构
|
||
|
||
```
|
||
c_api/
|
||
├── include/
|
||
│ ├── olive/ # 公共 C API 头文件(安装时发布)
|
||
│ │ ├── core_api.h
|
||
│ │ ├── codec_api.h
|
||
│ │ ├── node_api.h
|
||
│ │ ├── render_api.h
|
||
│ │ ├── audio_api.h
|
||
│ │ ├── plugin_api.h
|
||
│ │ ├── ui_api.h
|
||
│ │ └── olive_api.h # 总入口,包含所有模块
|
||
│ └── olivecpp/ # 主进程内部使用的 C++ 辅助封装
|
||
│ ├── module_loader.h
|
||
│ ├── core_wrapper.h // RAII 包装类
|
||
│ ├── node_wrapper.h
|
||
│ └── ...
|
||
└── src/
|
||
├── core_api.cpp // 对应各模块的 C 封装实现
|
||
├── codec_api.cpp
|
||
├── node_api.cpp
|
||
├── render_api.cpp
|
||
├── audio_api.cpp
|
||
├── plugin_api.cpp
|
||
└── ui_api.cpp
|
||
```
|
||
|
||
### 10.2 C API 头文件示例
|
||
|
||
```c
|
||
// c_api/include/olive/node_api.h
|
||
#ifndef OLIVE_NODE_API_H
|
||
#define OLIVE_NODE_API_H
|
||
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
|
||
#include "core_api.h"
|
||
|
||
#define OLIVE_NODE_API_VERSION 1
|
||
|
||
/* ========== 类型前向声明 ========== */
|
||
typedef struct OliveNodeGraph OliveNodeGraph;
|
||
typedef struct OliveNode OliveNode;
|
||
typedef struct OliveParam OliveParam;
|
||
|
||
/* ========== 函数导出宏 ========== */
|
||
#ifdef OLIVE_BUILDING_NODE
|
||
# define OLIVE_NODE_API __attribute__((visibility("default")))
|
||
#else
|
||
# define OLIVE_NODE_API
|
||
#endif
|
||
|
||
/* ========== API 版本 ========== */
|
||
OLIVE_NODE_API int olive_node_api_version(void);
|
||
|
||
/* ========== NodeGraph ========== */
|
||
OLIVE_NODE_API OliveNodeGraph* olive_node_graph_create(void);
|
||
OLIVE_NODE_API void olive_node_graph_destroy(OliveNodeGraph* g);
|
||
|
||
OLIVE_NODE_API int olive_node_graph_load_xml(OliveNodeGraph* g,
|
||
const char* xml_data,
|
||
size_t xml_len);
|
||
OLIVE_NODE_API char* olive_node_graph_save_xml(OliveNodeGraph* g,
|
||
size_t* out_len);
|
||
|
||
OLIVE_NODE_API OliveNode* olive_node_graph_find_node(OliveNodeGraph* g,
|
||
const char* node_id);
|
||
OLIVE_NODE_API int olive_node_graph_add_node(OliveNodeGraph* g,
|
||
const char* node_type,
|
||
const char* node_id);
|
||
|
||
/* ========== Node ========== */
|
||
OLIVE_NODE_API const char* olive_node_get_id(OliveNode* node);
|
||
OLIVE_NODE_API const char* olive_node_get_type_name(OliveNode* node);
|
||
|
||
OLIVE_NODE_API int olive_node_connect(OliveNode* from_node,
|
||
int from_output_index,
|
||
OliveNode* to_node,
|
||
int to_input_index);
|
||
|
||
/* ========== Param ========== */
|
||
OLIVE_NODE_API int olive_node_set_param_int(OliveNode* node,
|
||
const char* param_name,
|
||
int64_t value);
|
||
OLIVE_NODE_API int olive_node_set_param_double(OliveNode* node,
|
||
const char* param_name,
|
||
double value);
|
||
OLIVE_NODE_API int olive_node_set_param_rational(OliveNode* node,
|
||
const char* param_name,
|
||
OliveRational value);
|
||
OLIVE_NODE_API int olive_node_set_param_string(OliveNode* node,
|
||
const char* param_name,
|
||
const char* value);
|
||
|
||
/* ========== Project ========== */
|
||
OLIVE_NODE_API OliveNodeGraph* olive_project_create(const char* name);
|
||
OLIVE_NODE_API int olive_project_load_file(OliveNodeGraph* project,
|
||
const char* filename);
|
||
OLIVE_NODE_API int olive_project_save_file(OliveNodeGraph* project,
|
||
const char* filename);
|
||
|
||
#ifdef __cplusplus
|
||
}
|
||
#endif
|
||
|
||
#endif // OLIVE_NODE_API_H
|
||
```
|
||
|
||
---
|
||
|
||
## 11. CMake 中的 C API 编译
|
||
|
||
### 11.1 为模块添加 C API 目标
|
||
|
||
```cmake
|
||
# app/node/CMakeLists.txt
|
||
|
||
# 原有 C++ 源码(内部实现,不暴露头文件)
|
||
set(NODE_INTERNAL_SOURCES
|
||
node.cpp node.h
|
||
traverser.cpp traverser.h
|
||
project/project.cpp project/project.h
|
||
# ... 其他内部文件
|
||
)
|
||
|
||
# C API 封装层源码
|
||
set(NODE_API_SOURCES
|
||
${CMAKE_SOURCE_DIR}/c_api/src/node_api.cpp
|
||
)
|
||
|
||
# 模块对外头文件(安装时发布)
|
||
set(NODE_API_HEADERS
|
||
${CMAKE_SOURCE_DIR}/c_api/include/olive/node_api.h
|
||
)
|
||
|
||
# 创建动态库
|
||
add_library(olivenode SHARED
|
||
${NODE_INTERNAL_SOURCES}
|
||
${NODE_API_SOURCES}
|
||
)
|
||
|
||
target_compile_definitions(olivenode PRIVATE OLIVE_BUILDING_NODE)
|
||
target_include_directories(olivenode
|
||
PRIVATE
|
||
${CMAKE_SOURCE_DIR}/app
|
||
${CMAKE_SOURCE_DIR}/c_api/include
|
||
PUBLIC
|
||
$<INSTALL_INTERFACE:include>
|
||
)
|
||
|
||
target_link_libraries(olivenode
|
||
PRIVATE
|
||
olivecore
|
||
olivecodec
|
||
Qt${QT_VERSION_MAJOR}::Core
|
||
)
|
||
|
||
# 设置符号可见性:默认隐藏,只有标记 OLIVE_NODE_API 的才导出
|
||
set_target_properties(olivenode PROPERTIES
|
||
CXX_VISIBILITY_PRESET hidden
|
||
VISIBILITY_INLINES_HIDDEN YES
|
||
)
|
||
|
||
# 安装 C API 头文件
|
||
install(FILES ${NODE_API_HEADERS} DESTINATION include/olive)
|
||
install(TARGETS olivenode DESTINATION lib)
|
||
```
|
||
|
||
### 11.2 主可执行文件不链接业务库
|
||
|
||
```cmake
|
||
# app/CMakeLists.txt(改造后)
|
||
|
||
add_executable(olive-editor
|
||
main.cpp
|
||
core.cpp
|
||
core.h
|
||
${CMAKE_SOURCE_DIR}/c_api/src/module_loader.cpp # 显式加载器实现
|
||
)
|
||
|
||
# 主程序只链接 Qt 和系统库,不链接 olivecore/olivecodec 等业务库!
|
||
target_link_libraries(olive-editor PRIVATE
|
||
Qt${QT_VERSION_MAJOR}::Core
|
||
Qt${QT_VERSION_MAJOR}::Gui
|
||
Qt${QT_VERSION_MAJOR}::Widgets
|
||
# ... 其他 UI 依赖
|
||
)
|
||
|
||
target_include_directories(olive-editor PRIVATE
|
||
${CMAKE_SOURCE_DIR}/c_api/include
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
## 12. 测试策略
|
||
|
||
### 12.1 C API 单元测试
|
||
|
||
为每个 C API 函数编写独立测试:
|
||
|
||
```cpp
|
||
// tests/c_api/test_node_api.cpp
|
||
#include <gtest/gtest.h>
|
||
#include "olive/node_api.h"
|
||
|
||
TEST(NodeAPITest, CreateDestroy) {
|
||
OliveNodeGraph* g = olive_node_graph_create();
|
||
ASSERT_NE(g, nullptr);
|
||
olive_node_graph_destroy(g);
|
||
}
|
||
|
||
TEST(NodeAPITest, AddNodeAndParam) {
|
||
OliveNodeGraph* g = olive_node_graph_create();
|
||
ASSERT_EQ(OLIVE_OK, olive_node_graph_add_node(g, "Transform", "T1"));
|
||
OliveNode* n = olive_node_graph_find_node(g, "T1");
|
||
ASSERT_NE(n, nullptr);
|
||
ASSERT_EQ(OLIVE_OK, olive_node_set_param_double(n, "position_x", 100.0));
|
||
olive_node_graph_destroy(g);
|
||
}
|
||
```
|
||
|
||
### 12.2 ABI 稳定性测试
|
||
|
||
在 CI 中:
|
||
1. 编译当前版本的动态库。
|
||
2. 用上一个发布版本的测试可执行文件加载当前动态库运行。
|
||
3. 验证所有测试通过(确保未破坏 ABI)。
|
||
|
||
---
|
||
|
||
## 13. 常见陷阱
|
||
|
||
| 陷阱 | 说明 | 对策 |
|
||
|---|---|---|
|
||
| **异常穿透 C 边界** | C++ 异常抛出到 C 调用方是 UB。 | 所有 C API 函数用 `try/catch(...)` 包裹,捕获所有异常并转换为错误码。 |
|
||
| **RTTI 跨边界** | `dynamic_cast` 在不同动态库间可能失败。 | C 接口不使用 RTTI,内部若必须 `dynamic_cast`,确保类型定义在同一个库内。 |
|
||
| **Qt 元对象跨库** | `qobject_cast` 依赖 moc 生成的静态元对象数据,跨库时可能失效。 | 不在 C API 中暴露 Qt 对象,所有 Qt 对象封装在库内部。 |
|
||
| **全局静态变量** | 多个动态库各有一份全局静态变量。 | 避免在 C API 头文件中定义全局静态变量,使用函数内 static + 首次调用初始化。 |
|
||
| **堆不一致(Windows)** | A 库 `malloc`,B 库 `free` 导致崩溃。 | 严格遵循"谁分配谁释放",使用库提供的 `olive_core_free()`。 |
|