首页
/ OBS Studio 动态字符串工具 dstr 全解析:libobs 的 C 语言字符串核心 API 与实现机制

OBS Studio 动态字符串工具 dstr 全解析:libobs 的 C 语言字符串核心 API 与实现机制

2026-09-04 16:57:34作者:庞眉杨Will

OBS Studio 的核心渲染与输出层 libobs 是一个纯 C 库,它没有 std::string 可用,取而代之的是自己实现的动态字符串结构 struct dstr 与一组字符串辅助函数。本文基于官方 API 参考文档 reference-libobs-util-dstr.rst,逐节覆盖 #include <util/dstr.h> 提供的全部接口,并结合 libobs/util/dstr.hlibobs/util/dstr.c 的真实实现与 plugins/obs-outputs/mp4-output.c 中的实战调用,讲清每个函数的语义、边界行为和内存约定,帮助你在开发 OBS 插件或阅读 libobs 源码时安全、高效地使用这套字符串设施。

一、struct dstr:三字段动态字符串

文档将 struct dstr 描述为“与 std::string 大致等价的字符串辅助结构”。其定义见 libobs/util/dstr.h#L36-L40

struct dstr {
    char *array;      /* 底层缓冲区(bmalloc/brealloc 分配) */
    size_t len;      /* 字符数,不含结尾的 null 终止符 */
    size_t capacity;  /* 缓冲区已分配的容量 */
};

三个成员各管一件事:array 是堆上缓冲区,len 精确记录当前字符串长度(不含 '\0'),capacity 记录缓冲区可容纳的空间。从源码结构看,libobs 维持一条清晰的不变量:只要 array 非空,array[dst->len] 始终是 '\0',且 len + 1 <= capacity。所有修改长度的函数(dstr_ncatdstr_insertdstr_remove 等)在结尾都会显式补上 null 终止符,这保证了你可以随时把 dst->array 当普通 C 字符串传给 printfstrcmp 等标准库函数。

缓冲区扩张由头文件中的内联函数 dstr_ensure_capacity 统一完成:

static inline void dstr_ensure_capacity(struct dstr *dst, const size_t new_size)
{
    size_t new_cap;
    if (new_size <= dst->capacity)
        return;

    new_cap = (!dst->capacity) ? new_size : dst->capacity * 2;
    if (new_size > new_cap)
        new_cap = new_size;
    dst->array = (char *)brealloc(dst->array, new_cap);
    dst->capacity = new_cap;
}

可以推断其设计意图:首次分配按实际需求给足容量,之后按倍增capacity * 2)扩容,必要时再一次性拉大到 new_size。这与 std::string 的几何扩容策略一致,使 dstr_cat 等追加操作的均摊复杂度为 O(1)。注意它使用的是 libobs 的分配器 brealloc(见 libobs/util/bmem.h),因此配套的释放函数必须用 bfree,而不是直接 free

二、通用字符串辅助函数(General String Helper Functions)

文档第一组 API 面向裸 C 字符串,与 dstr 无关但同头文件提供。逐个对照实现:

大小写不敏感比较:astrcmpi / wstrcmpi

EXPORT int astrcmpi(const char *str1, const char *str2);
EXPORT int wstrcmpi(const wchar_t *str1, const wchar_t *str2);

大小写不敏感的 strcmp / wcscmp 等价物,返回负数、0、正数。实现位于 dstr.c#L38-L56:逐字符用 toupper 归一化后比较。关键细节:两个参数都是 NULL 安全的——任何一侧为 NULL 都会被替换为静态空串 astrblank,因此 astrcmpi(name, NULL) == 0 不会崩溃,而是等价于与空串比较。宽字符串版本 wstrcmpi 使用 towupper,语义相同。

限定长度比较:astrcmp_n / wstrcmp_n / astrcmpi_n / wstrcmpi_n

int astrcmp_n(const char *str1, const char *str2, size_t n);
int wstrcmp_n(const wchar_t *str1, const wchar_t *str2, size_t n);
int astrcmpi_n(const char *str1, const char *str2, size_t n);
int wstrcmpi_n(const wchar_t *str1, const wchar_t *str2, size_t n);

只比较前 n 个字符(任一字符串提前结束则停止)。n == 0 时直接返回 0,其余细节(NULL 安全、比较方向)与全量版本一致,实现见 dstr.c#L78-L164

大小写不敏感的子串查找:astrstri / wstrstri

char *astrstri(const char *str, const char *find);
wchar_t *wstrstri(const wchar_t *str, const wchar_t *find);

strstr / wcsstr 的大小写不敏感版本。实现(dstr.c#L166-L198)并不逐字符做大小写归一化扫描,而是复用 astrcmpi_n:以 strlen(find) 为长度,在每个候选起点做定长比较,命中即返回指向该起点的指针,未命中返回 NULLNULL 入参(strfind 为空)同样返回 NULL 而非崩溃。

去除首尾填充:strdepad / wcsdepad

char *strdepad(char *str);
wchar_t *wcsdepad(wchar_t *str);

原地删除字符串首尾的填充字符。文档明确填充字符集合为 tab、空格、CR、LF,对应实现中的判定函数:

static inline bool is_padding(int ch)
{
    return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r';
}

(见 dstr.c#L200-L232)。注意两点:一是它只处理首尾,不折叠中间空白;二是返回的是原指针 str(前导填充通过 memmove 前移),NULL 或空串原样返回。这类函数常用于解析 INI 配置、日志行首尾裁剪等场景。

字符串切分:strlist_split / strlist_free

char **strlist_split(const char *str, char split_ch, bool include_empty);
void strlist_free(char **strlist);

把字符串按分隔符 split_ch 切分为以 NULL 结尾的子串指针数组;若分隔符不存在,第一个子串就是原串本身。参数说明:

  • str:待切分字符串;
  • split_ch:单个分隔字符;
  • include_empty:相邻分隔符产生的空子串是否保留。设为 true"a,,b" 切出 3 项("a""""b"),设为 false 时切出 2 项。

文档给出的示例用法:

char **words = strlist_split("OBS    Studio", ' ', false);
int count = 0;

for (char **word = words; *word; ++word) {
    count++;
    blog(LOG_DEBUG, "%s", *word);
}

strlist_free(words);
// count == 2

多个连续空格会被跳过(因为 include_emptyfalse),最终 count == 2。从源码看,strlist_split 采用单次分配设计:先扫一遍统计段数与总字节数,再用一次 bmalloc(total_size) 同时分配出指针表和全部子串缓冲区(指针表在前,子串数据紧随其后)。这也解释了 strlist_free 为什么只需一行 bfree(strlist)——整个数组只有一个分配块。对 NULL 输入,它返回 NULL,调用方可以安全地跳过。

三、生命周期管理:init / move / free 家族

初始化

void dstr_init(struct dstr *dst);              // 全部清零
void dstr_init_move(struct dstr *dst, struct dstr *src);
void dstr_init_move_array(struct dstr *dst, char *str);
void dstr_init_copy(struct dstr *dst, const char *src);
void dstr_init_copy_dstr(struct dstr *dst, const struct dstr *src);

实现都是 dstr.h 中的 static inline 函数:

  • dstr_init 仅把三字段清零(array=NULL, len=0, capacity=0),零成本;
  • dstr_init_movesrc 整体拷贝给 dst 后将 src 归零——所有权转移,不拷贝数据,是 C 版“移动语义”;
  • dstr_init_move_array 直接认领一个由 bmalloc 分配的字符串,只 strlen 一次计算 lencapacity 记为 len+1,无拷贝、无重分配;
  • dstr_init_copy / dstr_init_copy_dstr 则是“init + copy”的组合,产生真实数据拷贝。

释放与数组批量释放

void dstr_free(struct dstr *dst);

dstr_freebfree 释放 array 并归零三字段。头文件中还提供了文档未单列、但同族常用的 dstr_array_free(array, count)(逐个释放 dstr 数组,dstr.h#L185-L190)以及 dstr_move / dstr_move_array(先释放旧值再接管新所有权)。约定:dstr_free 释放的必须是 bmalloc/brealloc 系分配的内存,这也是整个 libobs util 层统一的内存纪律。

拷贝族

void dstr_copy(struct dstr *dst, const char *array);
void dstr_copy_dstr(struct dstr *dst, const struct dstr *src);
void dstr_ncopy(struct dstr *dst, const char *array, const size_t len);
void dstr_ncopy_dstr(struct dstr *dst, const struct dstr *src, const size_t len);
  • dstr_copy:整串拷贝,语义是“替换 dst 当前内容”。实现(dstr.c#L345-L358)先 dstr_free 旧缓冲区,若源为空串则直接置空;非空时 ensure_capacity(len+1)memcpy 连 null 一起拷贝。
  • dstr_copy_dstr:内联实现(dstr.h#L219-L228),同样先释放 dst 再按需拷贝,src->len 为 0 时不分配。
  • dstr_ncopy / dstr_ncopy_dstr:只拷贝前 len 个字符。dstr_ncopy_dstr 会用 size_min(len, str->len) 截断越界请求,并用 bmemdup(array, newlen+1) 精确分配 newlen+1 字节、补 null——即 capacity 恰好等于 len+1,不留冗余。

容量控制:dstr_resizedstr_reserve

void dstr_resize(struct dstr *dst, const size_t num);
void dstr_reserve(struct dstr *dst, const size_t num);

二者分工对应 std::stringresizereserve

  • dstr_resize 改变逻辑长度 len。放大时新字符被置零(array[num]=0),缩小时直接截短;num==0 时退化为 dstr_free 完全释放。实现见 dstr.h#L239-L249
  • dstr_reserve 只扩不缩容量,不改变 lendstr_reservecapacity <= dst->len 时直接返回——文档中“值小于当前保留大小时不生效”的说法由此得到源码印证:它不会缩小缓冲区。

判断空串则用内联的 dstr_is_emptydstr.h#L251-L259):array 为空、len 为 0 或首字符为 '\0' 均视为空。

四、拼接、插入与删除:修改类 API

拼接

void dstr_cat(struct dstr *dst, const char *array);
void dstr_cat_dstr(struct dstr *dst, const struct dstr *str);
void dstr_cat_ch(struct dstr *dst, char ch);
void dstr_ncat(struct dstr *dst, const char *array, const size_t len);
void dstr_ncat_dstr(struct dstr *dst, const struct dstr *str, const size_t len);
  • dstr_cat(内联,dstr.h#L261-L269):空指针或空串直接返回,否则委托 dstr_ncat
  • dstr_cat_chdstr.h#L271-L276):单字符追加的快路径,一次扩容、一次写字符、一次补 null;
  • dstr_ncat:核心追加逻辑——new_len = dst->len + lenensure_capacity(new_len+1) 后把 len 字节 memcpy 到尾部并补 null;
  • dstr_ncat_dstr 同样用 size_min(len, str->len) 防越界。

按索引插入

void dstr_insert(struct dstr *dst, const size_t idx, const char *array);
void dstr_insert_dstr(struct dstr *dst, const size_t idx, const struct dstr *str);
void dstr_insert_ch(struct dstr *dst, const size_t idx, const char ch);

idx 处插入字符串/单字符。三个函数都含同一优化(以 dstr_insert 为例):idx == dst->len 时直接走 dstr_cat 快路径,避免 memmove;否则扩容后用 memmove[idx, len] 整体后移 len 字节、再 memcpy 写入新内容。dstr_insert_ch 的移位量为 1,是插入路径上最轻量的形态。

按索引删除

void dstr_remove(struct dstr *dst, const size_t idx, const size_t count);

idx 起删除 count 个字符(dstr.c#L508-L525)。两个边界优化值得注意:count == dst->len 时整个字符串被删,直接 dstr_free;删除区段正好抵达串尾idx+count == len)时只需在 idx 处打一个 null,无需移动。中间删除才用 memmove 前移尾部。

五、格式化输出:dstr_printf / dstr_catf 及 v 变体

void dstr_printf(struct dstr *dst, const char *format, ...);
void dstr_vprintf(struct dstr *dst, const char *format, va_list args);
void dstr_catf(struct dstr *dst, const char *format, ...);
void dstr_vcatf(struct dstr *dst, const char *format, va_list args);

dstr_printf 将 dst 内容替换为格式化结果,dstr_catf追加v 版本接收 va_list,便于再封装可变参函数。可格式化的输出正是这套 API 在 libobs 中高频出现的原因。实现采用标准两遍 vsnprintf 策略(dstr_vprintf):

va_list args_cp;
va_copy(args_cp, args);

int len = vsnprintf(NULL, 0, format, args_cp);   // 第一遍:只算长度
va_end(args_cp);

if (len < 0)
    len = 4095;                                    // 出错时的回退容量

dstr_ensure_capacity(dst, ((size_t)len) + 1);     // 按需扩容
len = vsnprintf(dst->array, ((size_t)len) + 1, format, args);  // 第二遍:真正写入
...
dst->len = len < 0 ? strlen(dst->array) : (size_t)len;

第一遍传 NULL 缓冲只获取所需长度,据此扩容,第二遍再写入,杜绝截断风险;vsnprintf 返回负数时按 4095 字节兜底。头文件中 dstr_printf / dstr_catf 还挂了 PRINTFATTR(2, 3) 宏(非 MSVC 下即 __attribute__((format(printf, 2, 3))),见 dstr.h#L42-L46),让编译器在编译期检查格式串与实参匹配——这是使用这些函数时应顺带利用的免费安全网。

六、查找、替换与比较

查找

const char *dstr_find_i(const struct dstr *str, const char *find);  // 大小写不敏感
const char *dstr_find(const struct dstr *str, const char *find);    // 区分大小写

二者均为内联(dstr.h#L278-L286):分别把 str->array 交给 astrstri 或标准库 strstr,命中返回指向首处出现位置的指针(该指针指向 dstr 内部缓冲,随后续修改可能失效),未命中返回 NULL

替换

void dstr_replace(struct dstr *str, const char *find, const char *replace);

替换 find所有出现dstr_replacereplace_lenfind_len 的长短分三条路径优化:替换串更短时在原地 memmove 收缩;更长时先统计出现次数、一次性 ensure_capacity 再逐个替换;等长时逐段 memcpyreplaceNULL 按空串处理(即删除所有 find)。它也是 dstr_safe_printf 的基础——后者把格式串中的 $1$4 占位符安全地替换为给定值,常用于把用户输入代入模板而不经 printf

比较

int dstr_cmp(const struct dstr *str1, const char *str2);
int dstr_cmpi(const struct dstr *str1, const char *str2);
int dstr_ncmp(const struct dstr *str1, const char *str2, const size_t n);
int dstr_ncmpi(const struct dstr *str1, const char *str2, const size_t n);

将 dstr 与 C 字符串比较,相等返回 0,否则返回非 0。内联实现(dstr.h#L288-L308)中 dstr_cmp 对两侧 NULL 都做了兜底(空 dstr 按 "" 比较),dstr_cmpi / dstr_ncmpi 则复用第二节的 astrcmpi / astrcmpi_n。注意文档对返回值约定只承诺“0 相等、非 0 不等”,不承诺符号方向——比较结果只应用于等值判断。

七、截取、去填充、宽字符转换与大小写转换

左/中/右截取

void dstr_left(struct dstr *dst, const struct dstr *str, const size_t pos);
void dstr_mid(struct dstr *dst, const struct dstr *str, const size_t start, const size_t count);
void dstr_right(struct dstr *dst, const struct dstr *str, const size_t pos);
  • dstr_left:把 str 的前 pos 个字符拷入 dstdst 先被 resize 到 posmemcpydstr.c#L686-L691);
  • dstr_mid:从 start 起拷 count 个字符,实现为“整体拷贝到临时 dstr 再 dstr_ncopy 偏移段”(dstr.c#L693-L700);
  • dstr_right:从 pos 索引开始拷到串尾(dstr.c#L702-L709)。

三者都以 dst 为目标写,且 dst 与 str 可以是同一对象。

去填充

void dstr_depad(struct dstr *dst);

dst->array 调用第二节讲的 strdepad重算 lendstr.c#L675-L684);若去完后变成空串则直接 dstr_free。这解释了裸函数与 dstr 版本的分工:strdepad 只动缓冲区,dstr_depad 额外维护长度不变量。

宽字符串互转

void dstr_from_wcs(struct dstr *dst, const wchar_t *wstr);   // wchar_t* -> UTF-8 存入 dstr
wchar_t *dstr_to_wcs(const struct dstr *str);                 // dstr -> 新分配的宽字符串

dstr_from_wcsdstr.c#L731-L741)先用 wchar_to_utf8(wstr, 0, NULL, 0, 0) 探长度,再 dstr_resize 后一次性转换写入;dstr_to_wcs 委托平台转换函数 os_utf8_to_wcs_ptr 返回新分配的宽字符串,文档明确要求用 bfree() 释放——因为分配来自 bmem 体系。libobs 内部统一以 UTF-8 为存储形态,这对处理 OBS 的本地化文本与跨平台路径尤为重要(头文件中另有 dstr_from_mbs / dstr_to_mbs 处理系统多字节编码,文档未单列,可参考 dstr.h#L133-L134)。

大小写转换与尾部字符

void dstr_to_upper(struct dstr *str);
void dstr_to_lower(struct dstr *str);
char dstr_end(const struct dstr *str);

dstr_to_upper / dstr_to_lowerdstr.c#L743-L787)走的是“UTF-8 → wchar_t → 逐字符 towupper/towlower → 转回 UTF-8”的往返路径,因此对 Unicode 文本的大小写转换是安全的,而不仅仅是 ASCII 区间;空串直接返回。dstr_end 返回最后一个字符,空串返回 0(dstr.h#L310-L316)。

八、C++ 侧的 RAII 封装:DStrstrref

libobs 是 C 库,但 OBS 前端大量 C++ 代码同样需要安全地管理 dstr。libobs/util/dstr.hpp 提供了一个极简 RAII 包装类 DStr

class DStr {
    dstr str;

    DStr(DStr const &) = delete;          // 禁拷贝构造
    DStr &operator=(DStr const &) = delete;

public:
    inline DStr() { dstr_init(&str); }
    inline DStr(DStr &&other) : DStr() { dstr_move(&str, &other.str); }   // 移动构造
    inline DStr &operator=(DStr &&other) { dstr_move(&str, &other.str); return *this; }
    inline ~DStr() { dstr_free(&str); }                               // 析构即释放

    inline operator dstr *() { return &str; }
    inline operator const dstr *() const { return &str; }
    inline operator char *() { return str.array; }
    inline operator const char *() const { return str.array; }
    inline dstr *operator->() { return &str; }
};

它禁用了拷贝(防止两份对象 bfree 同一缓冲的双重释放),支持移动,析构自动 dstr_free;同时通过转换算子让你既能 dstr *ds = DStrVar; 传指针给 C API,也能直接当 const char * 用,或 ds->arrayds->len 访问字段。这与第三节的 dstr_move 所有权转移语义是一脉相承的设计。

配合 libobs/util/lexer.h#L30-L33 中的 struct strrefarray + len,指向既有缓冲区的无拷贝段),头文件还提供了 dstr_init_copy_strrefdstr_copy_strrefdstr_cat_strref 等 strref 接口(dstr.h#L69-L91),用于在词法分析等零拷贝场景与 dstr 之间传递文本片段。

九、实战案例:mp4 录制的文件路径拼装

dstr 的典型用法在 plugins/obs-outputs/mp4-output.cgenerate_filename 中一目了然(L388-L408):

dstr_copy(dst, dir);                        // 以设置里的目录为起点
dstr_replace(dst, "\\", "/");               // 统一 Windows 反斜杠
if (dstr_end(dst) != '/')
    dstr_cat_ch(dst, '/');                  // 按需补分隔符
dstr_cat(dst, filename);                    // 追加格式化文件名

find_best_filenameL361-L386)则展示了“循环探测 + 移动所有权”的完整模式:

struct dstr testpath;
dstr_init_copy_dstr(&testpath, path);       // 拷贝一份用于试探
for (;;) {
    dstr_resize(&testpath, extstart);       // 截掉序号,保留扩展名前缀
    dstr_catf(&testpath, space ? " (%d)" : "_%d", num++);
    dstr_cat(&testpath, ext);
    if (!os_file_exists(testpath.array)) {
        dstr_free(path);                     // 释放原路径
        dstr_init_move(path, &testpath);     // 零拷贝接管 testpath
        break;
    }
}

注意 testpath.array 可直接当 char * 传给 os_file_exists 的“dstr 即 C 字符串”不变量,以及 dstr_init_move 如何避免最后一次成功探测结果的二次拷贝。这正是 dstr API 在 libobs 各插件中反复出现的标准组合。

十、使用要点与内存纪律小结

结合文档与源码,把关键约定归纳如下:

  1. 所有权单一:每个缓冲区只有一个属主。dstr_init_move / dstr_move 转移后源对象即归零,切勿再用源对象。
  2. 配对分配器:dstr 内部走 bmalloc/brealloc,释放必须用 bfreedstr_free 已封装好);dstr_to_wcs 返回的宽串同样以 bfree 释放。
  3. len 与 capacity 的区别dstr_reserve 只扩容不改 len 且不可缩小;dstr_resizelen,放大置零、缩到 0 则完全释放。
  4. NULL 安全边界:比较/查找/拼接类函数对 NULL 入参普遍做了空串兜底,但 dstr_middstr_right 的索引类参数仍需调用方保证合法。
  5. 返回指针生命周期dstr_find 系列返回的是内部缓冲指针,任何后续修改操作都可能使其失效,只应视为瞬时句柄。
  6. C++ 代码优先用 DStr:让析构接管释放,配合移动语义传递,可避免绝大多数手工 dstr_free 遗漏。

这套 API 覆盖的函数清单与本文小节一一对应,均出自 docs/sphinx/reference-libobs-util-dstr.rst 文档所列接口;实现细节可对照 libobs/util/dstr.h(内联函数与 EXPORT 声明)与 libobs/util/dstr.c(跨平台导出函数)逐条验证。理解 struct dstr 的三字段不变量与倍增扩容策略后,OBS 源码中成百上千处 dstr_* 调用的行为都可以直接推演,这也是阅读 libobs、obs-outputs、obs-websocket 等模块源码的一把基础钥匙。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384