OBS Studio 动态数组 darray 深度解析:C 风格可增长数组的 API、实现原理与源码级用法
本文基于 OBS Studio 官方 API 参考文档 reference-libobs-util-darray.rst,系统讲解 util/darray.h 提供的动态数组(dynamic array)机制:darray 基础结构、DARRAY(type) 类型宏、以及 da_* 系列宏的完整语义。结合 libobs/util/darray.h 的实现源码与 libobs 中的真实调用示例(场景项管理、序列化缓冲、效果解析器等),读者可以掌握这套“C 版 std::vector”的初始化/扩容/插入/移动语义,以及为什么向函数传递动态数组时必须使用 typedef 而不是裸引用内部 da 成员。
一、动态数组是什么:C 语言中的 std::vector
OBS 的核心库 libobs 是纯 C 实现,需要一种可动态增长的线性容器来承载源码中大量变化的数据集合(场景项指针、效果参数、字节流缓冲等)。官方文档(docs/sphinx/reference-libobs-util-darray.rst)对它的定义是:
Dynamically resizing arrays (a C equivalent to std::vector).
引入方式只有一个头文件:
#include <util/darray.h>
对应源码位于 libobs/util/darray.h。该头文件自身不依赖项目其他 CMake 目标,仅包含 bmem.h(分配器)、c99defs.h 以及标准库 string.h/stdlib.h/assert.h,全部函数体都以 static inline 形式写在头文件内——这意味着所有操作都在编译期内联展开,无调用开销。
1.1 基础结构体 darray
底层结构体极其简单:
struct darray {
void *array; /* The array pointer. 元素缓冲区指针 */
size_t num; /* The number of items. 当前元素个数 */
size_t capacity;/* The capacity of the array. 已分配容量 */
};
array:指向以字节方式管理的一维缓冲区(内部所有操作都按element_size * idx做偏移计算);num:当前有效元素数量;capacity:已分配容量(元素个数)。
文档还定义了一个全局哨兵常量,在 darray.h 中:
#define DARRAY_INVALID ((size_t)-1)
凡是“按值查找”类操作(da_find)在找不到目标时,返回 DARRAY_INVALID(即 size_t 的最大值),调用方必须以此判断查找结果。
1.2 DARRAY(type) 宏:让容器“半类型安全”
直接使用 struct darray 时所有元素都是 void *,编译器无法检查类型。因此头文件提供了 DARRAY(type) 宏(darray.h#L438-L446):
#define DARRAY(type) \
union { \
struct darray da; \
struct { \
type *array; \
size_t num; \
size_t capacity; \
}; \
}
这是一个匿名 union:同一块内存既可以当作 struct darray da 交给底层 darray_* 内联函数操作,又可以直接以 type *array 的形式按真实类型访问元素(例如 items.array[i]、items.num)。各 da_* 宏的本质就是“计算 sizeof(*(v).array) 后转发给对应的 darray_* 内联函数”,例如:
#define da_push_back(v, item) darray_push_back(sizeof(*(v).array), &(v).da, item)
#define da_erase(dst, idx) darray_erase(sizeof(*(dst).array), &(dst).da, idx)
源码注释也坦率地说明(darray.h#L430-L436):这种方式“仍然不是 100% 类型安全,但比直接用 darray 好得多”,并且作者刻意没有用巨型宏为每种类型生成类型安全的内联函数,因为“那太乱”。
此外,头文件中还有一个可选的 ENABLE_DARRAY_TYPE_TEST 编译开关:开启后 da_push_back、da_find 等写入型宏会在 if (false) 分支里构造一次“类型赋值”检查,从而在不产生运行时代码的前提下让编译器在编译期发现类型不匹配(C++ 使用 auto,C 使用 typeof 的 GNU 扩展,见 darray.h#L468-L500)。
二、基本用法与官方示例
文档给出的最小完整示例是“创建一个存放 0..9 的整数数组”:
/* creates an array of integers: 0..9 */
DARRAY(int) array_of_integers;
da_init(array_of_integers);
for (size_t i = 0; i < 10; i++)
da_push_back(array_of_integers, &i);
[...]
/* free when complete */
da_free(array_of_integers);
使用要点(均来自原文档约定):
da_*宏的参数是动态数组值本身,不要写&array_of_integers;da_push_back的第二个参数是指向数据的指针(&i),宏内部执行一次memcpy,因此不会保留你的局部变量地址;- 生命周期结束必须调用
da_free,它会释放缓冲区并把三个成员全部归零,数组可再次da_init复用。
2.1 作为函数参数:用 typedef 而不是取 da 成员的引用
文档明确指出:把动态数组作为参数传给函数时,推荐先声明一个 typedef:
typedef DARRAY(int) int_array_t;
void generate_integers(int_array_t *integers, int start, int end)
{
for (int i = start; i < end; i++)
da_push_back(*integers, &i);
}
[...]
int_array_t array_of_integers;
da_init(array_of_integers);
generate_integers(&array_of_integers, 0, 10);
/* free when complete */
da_free(array_of_integers);
替代方案是把动态数组装进一个普通结构体,再传结构体指针。
IMPORTANT NOTE(原文档的重要警告,必须遵守):虽然技术上可以直接接收内部 darray 结构(通过 da 成员)并在函数内部再用 DARRAY 宏重新声明变量,但这样做不安全且不被推荐。典型风险是:函数内部的类型声明与调用方实际传入的动态数组类型不一致时,编译器无法发现,会造成内存访问错误(越界、错误偏移)。typedef 或容器结构体能让“数组类型”随指针一起传播,从根本上消除这类错误。
2.2 仓库中的真实 typedef 用法
libobs 源码中大量使用了上述 typedef 模式,可以印证文档的推荐写法:
- 场景项指针数组:
typedef DARRAY(struct obs_scene_item *) obs_scene_item_ptr_array_t;(libobs/obs-scene.c#L36); - 字节缓冲:
DARRAY(uint8_t) bytes;用于序列化输出(libobs/util/array-serializer.h#L26-L29); - 效果解析器的参数/变量数组(libobs/graphics/effect-parser.h#L31-L32)、效果参数数组(libobs/graphics/effect.h#L27-L28);
- 属性数组
typedef DARRAY(struct obs_property *) obs_property_da_t;(libobs/obs-properties.c#L359); - 性能分析器条目数组(libobs/util/profiler.h#L62)。
一个非常典型的真实用法是 obs-scene.c 中的 remove_all_items(libobs/obs-scene.c#L206-L230):先用 da_init 初始化一个指针数组,持锁遍历场景项链表并 da_push_back 收集待删除项,解锁后再按 items.array[i] 逐个释放,最后 da_free。这个“先收集、后在锁外释放”的模式正是指针型 DARRAY 最常见的用途。
三、完整 API 参考(按功能分组)
以下逐一继承官方文档中列出的全部 da_* 宏,并在每条之后结合 libobs/util/darray.h 的实现补充语义细节。所有宏都遵循“传值不传引用”的约定。
3.1 生命周期与容量管理
| 宏 | 原型 | 说明 |
|---|---|---|
da_init |
void da_init(da) |
初始化:array=NULL, num=0, capacity=0(darray.h#L47-L52) |
da_free |
void da_free(da) |
释放缓冲区并重置为初始状态;释放走 bfree(见下) |
da_alloc_size |
size_t da_alloc_size(v) |
返回 sizeof(*(v).array) * v.num,即已存元素占用的字节数(num × 元素大小),注意它不是 capacity 对应的已分配缓冲区总大小 |
da_reserve |
void da_reserve(da, size_t capacity) |
预留容量。实现上若新容量不大于当前 capacity 则直接返回;否则 bmalloc 新缓冲区、memcpy 已有 num 个元素后释放旧缓冲(darray.h#L80-L95) |
da_resize |
void da_resize(da, size_t new_size) |
调整 num;扩大时对新增部分 memset 清零(文档表述为 “Resizes the dynamic array with zeroed values”),缩小时只改 num 不动缓冲区 |
关于分配器:所有缓冲都通过 libobs 的通用内存接口 bmalloc/bfree 分配(libobs/util/bmem.h#L34-L36)。这使得动态数组能统一接入 libobs 的内存追踪/调试分配器。
3.2 容量增长策略:翻倍扩容
理解 da_push_back 等写入操作,必须先理解 darray_ensure_capacity(darray.h#L97-L116):
new_cap = (!dst->capacity) ? new_size : dst->capacity * 2;
if (new_size > new_cap)
new_cap = new_size;
即:首次分配时按需求量精确分配,此后按容量翻倍增长,且保证不小于需求量。这与 std::vector 的常见增长策略一致,从而摊还了 da_push_back 的均摊 O(1) 复杂度。文档没有单独列出一个“ensure_capacity”宏,但从源码结构看它是所有写入路径(push/insert/resize)的共同底层。
另外注意 darray_ensure_capacity 中 memcpy 的长度用的是 element_size * dst->capacity(旧容量的满拷贝),而 darray_reserve 用 element_size * dst->num——前者在“容量大于数量”时多拷贝了尾部未初始化字节,功能上无害(新缓冲区随后按新容量使用),属于实现层面的宽松处理。
3.3 读取
| 宏 | 原型 | 说明 |
|---|---|---|
da_end |
void *da_end(da) |
返回最后一个元素的指针;数组为空时返回 NULL(darray.h#L72-L78) |
da_find |
size_t da_find(da, const void *item_data, size_t starting_idx) |
从 starting_idx 起按 memcmp 逐元素全量比较;找不到返回 DARRAY_INVALID。实现内含 assert(idx <= da->num)(darray.h#L170-L183) |
直接按索引访问则完全绕开宏:v.array[i](类型化视图)或 v.da.array + 偏移(裸视图)。这也是为什么 DARRAY(type) 的 union 设计有价值——索引访问是类型安全的。
3.4 追加(push)
| 宏 | 原型 | 说明 |
|---|---|---|
da_push_back |
size_t da_push_back(da, const void *data) |
尾部追加,返回新元素索引。实现是 ++num 后 ensure_capacity 再 memcpy 到 darray_end(darray.h#L185-L191) |
da_push_back_new |
void *da_push_back_new(da) |
追加一个清零元素并返回其指针,适合“先取指针、再逐字段填充”的用法(如构建 obs_property 列表) |
da_push_back_array |
size_t da_push_back_array(da, const void *src_array, size_t item_count) |
一次性批量追加,返回首批新元素的索引。注意 item_count 是元素个数;实现走 darray_resize 扩容后单次 memcpy,且对 dst==NULL/src_array==NULL/num==0 有防御(darray.h#L204-L218) |
da_push_back_da |
size_t da_push_back_da(da, src) |
把一个 DARRAY 的全部元素追加到另一个后面(源码提供,等价于 push_back_array(src.array, src.num),见 darray.h#L220-L223) |
关于 da_push_back_new 有一段实现细节值得注意:在 GCC 下它被重写为一个 GCC 语句表达式宏,因为源码注释说明 “GCC 12 with -O2 generates a warning -Wstringop-overflow in da_push_back_new, which could be false positive”(darray.h#L512-L525)。这说明该宏需要处理真实的现代编译器告警场景。
3.5 插入(insert)
| 宏 | 原型 | 说明 |
|---|---|---|
da_insert |
void da_insert(da, size_t idx, const void *data) |
在 idx 处插入;若 idx == num 则退化为 da_push_back;否则 memmove 右移后续元素。含 assert(idx <= dst->num)(darray.h#L225-L244) |
da_insert_new |
void *da_insert_new(da, size_t idx) |
在 idx 处插入清零元素并返回指针;idx == num 时等价 da_push_back_new(darray.h#L246-L263) |
da_insert_array |
void da_insert_array(dst, size_t idx, src, size_t n) |
批量插入;先 resize(num + n) 再 memmove 腾位、memcpy 写入(darray.h#L265-L280) |
da_insert_da |
void da_insert_da(da_dst, size_t idx, da_src) |
把一个 DARRAY 插到另一个的指定位置(darray.h#L282-L286) |
所有插入操作都会触发 num - idx 量级的 memmove,复杂度 O(n);对大数组频繁在中间插入应权衡改用“尾部追加 + da_move_item/da_swap 重排”的方式。
3.6 删除(erase / pop)
| 宏 | 原型 | 说明 |
|---|---|---|
da_erase |
void da_erase(da, size_t idx) |
删除指定索引元素,memmove 前移后续元素;idx >= num 时静默返回(另有 assert(idx < dst->num) 保护)(darray.h#L288-L297) |
da_erase_item |
void da_erase_item(da, const void *item_data) |
先 da_find 再 da_erase,删除第一个值匹配的项(darray.h#L299-L304) |
da_erase_range |
void da_erase_range(da, size_t start_idx, size_t end_idx) |
删除 [start_idx, end_idx) 半开区间;单元素时退化为 da_erase,删除全部时直接 num = 0(darray.h#L306-L330) |
da_pop_back |
void da_pop_back(da) |
删除末尾元素(实现上就是 da_erase(num-1)) |
da_pop_front |
void da_pop_front(da) |
删除首元素(源码提供,darray.h#L332-L338) |
da_clear |
void da_clear(da) |
仅置 num = 0,保留缓冲区容量,适合“清空复用”场景(darray.h#L118-L121) |
删除同样不回收容量:反复 erase 不会缩小 capacity,只有 da_free 才真正归还内存。
3.7 复制 / 移动 / 合并 / 拆分
| 宏 | 原型 | 说明 |
|---|---|---|
da_copy |
void da_copy(da_dst, da_src) |
值拷贝:源为空时释放目标,否则 resize 目标后整段 memcpy(darray.h#L145-L153) |
da_copy_array |
void da_copy_array(da, const void *src_array, size_t size) |
从普通数组指针整体拷贝 |
da_move |
void da_move(da_dst, da_src) |
无分配转移:先 da_free(dst),再把 src 的整个 darray 结构 memcpy 过来,并将 src 三成员清零——“move 语义”在 C 中的实现(darray.h#L161-L168) |
da_join |
void da_join(da_dst, da_src) |
把 src 全部追加到 dst 末尾,并 da_free(src);调用后 src 已释放,不可再使用(darray.h#L348-L352) |
da_split |
void da_split(da_dst1, da_dst2, da_src, size_t split_idx) |
按索引把 src 拆成两段:dst1 得到 [0, split_idx),dst2 得到 [split_idx, num)。两个目标若非空会先被释放;被拆分的 src 本身不受影响(darray.h#L354-L376) |
3.8 元素重排
| 宏 | 原型 | 说明 |
|---|---|---|
da_move_item |
void da_move_item(da, size_t src_idx, size_t dst_idx) |
把元素从 src_idx 搬到 dst_idx,中间元素整体 memmove。实现中需要一块 element_size 大小的临时内存,分配失败会 bcrash("darray_move_item: out of memory")(darray.h#L378-L403) |
da_swap |
void da_swap(da, size_t idx1, size_t idx2) |
交换两个索引处的值;同样是 malloc 临时块 + 三次 memcpy,失败时 bcrash(darray.h#L405-L427) |
注意 da_move_item/da_swap 与其余所有操作不同——它们会临时 malloc 一块堆内存(元素大小)。从源码结构看这是为了避免元素自重叠 memmove 的复杂度。
四、实现层面的几个关键事实
综合 libobs/util/darray.h 全文,可以提炼出以下对使用者最重要的实现事实:
- 全部为
static inline+ 宏转发。头文件头部注释写道 “Specifying size per call with inline maximizes compiler optimizations”(每次调用显式传入元素大小 + 内联,最大化编译器优化)。da_*宏每次都会计算sizeof(*(v).array),因此同一元素类型的访问路径完全静态化。 - 字节寻址核心是
darray_item:(uint8_t *)da->array + element_size * idx(darray.h#L67-L70)。da_find的按值比较、da_erase_item的删除、da_insert的腾位,全部建立在其上。 - 调试断言内建:
da_find、da_insert*、da_erase、da_erase_range、da_pop_*都带assert边界检查(如assert(idx <= dst->num)、assert(end > start)),在 Debug 构建中越界访问会立即暴露。 - 内存归属:缓冲经由
bmalloc/bfree(libobs/util/bmem.h),而da_move/da_join/da_split这类“跨数组”操作决定了调用方对释放责任的划分——尤其da_join会释放源数组、da_move会把源数组置零。 - C/C++ 双语言兼容:
extern "C"包裹加上da_type_test中针对__cplusplus的两套 GNU 语句表达式实现,说明同一套头文件在 libobs(C)与 frontend(C++)中都被使用。
五、测试与验证
仓库为 darray 提供了 cmocka 单元测试:test/cmocka/test_darray.c。当前的基础用例验证 da_push_back_array 后 num == 1 且缓冲区内容与源字节一致:
DARRAY(uint8_t) testarray;
da_init(testarray);
uint8_t t = 1;
da_push_back_array(testarray, &t, sizeof(uint8_t));
assert_int_equal(testarray.num, 1);
assert_memory_equal(testarray.array, &t, 1);
da_free(testarray);
(注意该测试中第二参数是 sizeof(uint8_t),即把“1 个字节”当作批量长度传入的用法;结合 darray_push_back_array 把 num 参数解释为元素个数的实现,阅读测试时应以 darray.h#L204-L218 的语义为准。)测试构建入口在 test/cmocka 目录,可作为回归验证 darray 行为变更的参照。
六、使用建议小结
结合文档约定与 libobs 源码中的成熟用法,给出实践清单:
- 声明:一律
DARRAY(T) name;+da_init(name);,退出路径保证da_free(name);(da_free可重复安全调用,释放后会置零); - 传参:用
typedef DARRAY(T) T_array_t;传指针(参考obs_scene_item_ptr_array_t等真实先例),或用容器结构体封装;不要把&v.da当参数传递; - 读取:索引访问直接用
v.array[i]/v.num;取尾部指针用da_end(v);按值查找用da_find并以DARRAY_INVALID判失败; - 构建复杂元素:优先
da_push_back_new/da_insert_new拿到清零指针后填字段,避免栈上临时变量; - 性能敏感路径:已知规模时先
da_reserve或批量da_push_back_array/da_insert_array,减少翻倍扩容与多次memcpy;需要“清空但保留容量”时用da_clear; - 合并/拆分:
da_join会消耗源、da_move会清空源、da_split保留源——按各自语义管理释放责任,避免 double-free 或悬挂使用。
以上所有结论均以当前仓库 libobs/util/darray.h 的源码、docs/sphinx/reference-libobs-util-darray.rst 的官方说明以及 test/cmocka/test_darray.c 的测试用例为依据,适用于本仓库(OBS Studio 主分支)中的 libobs 工具层;若你在插件或前端代码中扩展此类容器,建议沿用 libobs 既有的 typedef + 容器结构体模式,以保持与现有调用风格一致。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00