OBS Studio 动态数组 darray 深度解析:C 风格可增长数组的 API、实现原理与源码级用法
【免费下载链接】obs-studioOBS Studio - Free and open source software for live streaming and screen recording项目地址: https://gitcode.com/GitHub_Trending/ob/obs-studio
本文基于 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 + 容器结构体模式,以保持与现有调用风格一致。
【免费下载链接】obs-studioOBS Studio - Free and open source software for live streaming and screen recording项目地址: https://gitcode.com/GitHub_Trending/ob/obs-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考