首页
/ OBS Studio libobs callback 模块深度解析:calldata、signal 与 procedure handler 的底层机制与实战用法

OBS Studio libobs callback 模块深度解析:calldata、signal 与 procedure handler 的底层机制与实战用法

2026-09-05 17:59:47作者:段琳惟

在 OBS Studio 的 libobs 核心库中,callback 模块是实现"事件驱动"编程的基础设施:source 的创建/销毁/重命名、音量变化、输出启动/停止等几乎所有事件回调,都经由 calldata_t 传递参数、经由 signal_handler_t 分发事件、经由 proc_handler_t 实现无声明的直接函数调用。本文以 API 参考文档 reference-libobs-callback.rst 为骨架,逐一讲清三类对象的完整 API 语义与生命周期约束,并结合 libobs/callback 目录下的真实源码(calldata.hsignal.cproc.c)剖析其内存模型、线程安全设计,以及 libobs 内核自身如何使用这些 API。

calldata_t:类型化参数容器

文档定义 calldata_t 对象用于"从信号处理器或向过程处理器传递参数"。它是三类对象共同的参数载体,对应头文件 calldata.h

生命周期:init / free 的配对约束

API 参考给出的生命周期函数只有两个,但约束非常严格:

函数 签名 语义
calldata_init void calldata_init(calldata_t *data) 初始化(清零)一个栈上 calldata 结构
calldata_free void calldata_free(calldata_t *data) 释放一个 calldata 结构;仅当使用过 calldata_init() 时才可调用

第二条约束是实践中最容易踩的坑:如果 calldata_t * 是作为回调参数传入的(例如 signal_callback_t 收到的 cd),它由信号分发方拥有生命周期,回调内部不得调用 calldata_free。从 calldata.c 同目录的头文件实现 可以看到,calldata_free 实际只负责 bfree(data->stack),而是否执行取决于 fixed 标志位——固定栈模式的 calldata 不会释放内存,这正与 OBS 内核中大量"栈上分配 calldata"的用法相对应。

源码中还有一个文档未展开的补充 API calldata_init_fixedcalldata.h),它让 calldata 直接复用调用者提供的字节缓冲区,避免堆分配。libobs 内核触发 source 信号时的真实写法(obs-internal.h)正是这种模式:

static inline void obs_source_dosignal(struct obs_source *source, const char *signal_obs,
                                       const char *signal_source)
{
    struct calldata data;
    uint8_t stack[128];

    calldata_init_fixed(&data, stack, sizeof(stack));
    calldata_set_ptr(&data, "source", source);
    if (signal_obs && !source->context.private)
        signal_handler_signal(obs->signals, signal_obs, &data);
    if (signal_source)
        signal_handler_signal(source->context.signals, signal_source, &data);
}

可以推断这一模式是性能驱动的:每帧视频都可能触发 video_tickpre_tick 等信号,用 128 字节栈缓冲替代动态扩容,可以避免高频事件路径上的堆分配。

五种参数类型的读写 API

文档将读写函数分为 setter 组与 getter 组,类型覆盖 int / float / bool / ptr / string 五种:

Setter 组(向 calldata 中写入参数):

函数 签名 说明
calldata_set_int void (calldata_t *data, const char *name, long long val) 写入整型参数
calldata_set_float void (calldata_t *data, const char *name, double val) 写入浮点参数
calldata_set_bool void (calldata_t *data, const char *name, bool val) 写入布尔参数
calldata_set_ptr void (calldata_t *data, const char *name, void *ptr) 写入指针参数
calldata_set_string void (calldata_t *data, const char *name, const char *str) 写入字符串参数

Getter 组(从 calldata 中读取参数):

函数 签名 说明
calldata_int long long (const calldata_t *data, const char *name) 读取整型,不存在时返回 0
calldata_float double (const calldata_t *data, const char *name) 读取浮点,不存在时返回 0.0
calldata_bool bool (const calldata_t *data, const char *name) 读取布尔,不存在时返回 false
calldata_ptr void * (const calldata_t *data, const char *name) 读取指针,不存在时返回 NULL;返回值无需 free
calldata_string const char * (const calldata_t *data, const char *name) 读取字符串,不存在时返回 NULL

文档对 calldata_ptr 的说明尤其重要:核心信号处理器(如 source 的信号)中声明为 ptr source 的参数,必须用 calldata_ptr 取出,并强转为 obs_source_t * 使用。这一点在 OBS 内核中被大量验证,例如音量控制模块连接 source 的 volume / destroy 信号时(obs-audio-controls.c):

signal_handler_connect(sh, "volume", fader_source_volume_changed, fader);
signal_handler_connect(sh, "destroy", fader_source_destroyed, fader);

calldata.h 的实现还能看到文档未提及的一层细节:calldata_int 等便捷函数是"乐观读取"——内部先调用返回 boolcalldata_get_int(参数存在且类型匹配才成功),失败时静默返回零值。而 calldata_get_string 的字符串指针指向 calldata 内部缓冲区,其有效期与 calldata 的生命周期绑定,跨调用缓存该指针是未定义行为(从实现结构推断:stack 扩容时会整体重新分配)。

signal_handler_t:事件分发中枢

Signals 是 libobs 中"所有基于事件的回调"的承载者(头文件 signal.h,实现 signal.c)。

类型与回调签名

typedef void (*signal_callback_t)(void *data, calldata_t *cd);
  • data:连接信号时通过 signal_handler_connect 传入的私有数据,在回调中被原样传回,用于在 C 语言中模拟"方法绑定 this";
  • cd:由 signal_handler_signal 触发时传入的参数对象,所有权属于分发方,回调内只读、不可释放。

完整生命周期 API

函数 签名 语义与返回值
signal_handler_create signal_handler_t *(void) 创建新的信号处理器,初始引用计数为 1(见 signal.c
signal_handler_destroy void (signal_handler_t *handler) 销毁;引用计数归零才真正释放(见下文)
signal_handler_add bool (signal_handler_t *handler, const char *signal_decl) 按声明字符串添加信号;声明非法或重名时返回 false
signal_handler_add_array bool (signal_handler_t *handler, const char **signal_decls) 批量添加,数组以 NULL 结尾;任一条失败则返回 false
signal_handler_connect void (signal_handler_t *handler, const char *signal, signal_callback_t callback, void *data) 连接回调;(signal, callback, data) 组合已存在时什么都不做
signal_handler_connect_ref void (signal_handler_t *handler, const char *signal, signal_callback_t callback, void *data) 连接回调并递增引用计数,使 handler 在断开前不会被真正销毁;即使组合已存在,引用计数仍会递增
signal_handler_disconnect void (signal_handler_t *handler, const char *signal, signal_callback_t callback, void *data) 断开回调;组合不存在时什么都不做
signal_handler_signal void (signal_handler_t *handler, const char *signal, calldata_t *params) 触发信号,按连接顺序调用所有已连接回调

信号声明字符串

signal_handler_add 接受一个"信号声明字符串",其文法由 decl.c 解析:函数名后跟零或多个 类型 名称 参数对,可用类型即 calldata 的五种(intfloatboolptrstring)以及无参数的 void。libobs 内核的真实声明示例(obs.c):

signal_handler_add(obs->signals, "void deduplication_changed(ptr source)");

signal.c 的实现 可以看到两条运行时约束:

  1. 重名检测getsignal 遍历信号链表,若同名信号已存在,打印 LOG_WARNING 并返回 false——因此插件注册自定义信号时必须避免与内核信号(如 signal 的 source 信号集)撞名;
  2. 两级锁:handler 持有全局 mutex 保护信号链表,每个 signal_info 另持有一个递归 mutex 保护回调数组(signal.c),这使得"在回调内部 disconnect 当前信号"成为合法操作——断开会延迟到本轮分发结束后才真正执行。

引用计数与延迟移除:connect_ref 的深意

文档对 signal_handler_connect_ref 的描述——"递增处理器内部引用计数,阻止其在信号断开前被销毁"——在 signal.c 中逐行得到印证:

if (keep_ref)
    os_atomic_inc_long(&handler->refs);

idx = signal_get_callback_idx(sig, callback, data);
if (keep_ref || idx == DARRAY_INVALID)
    da_push_back(sig->callbacks, &cb_data);

注意一个微妙差异:signal_handler_connect 对已存在的 (signal, callback, data) 组合不重复入队(幂等),而 connect_ref 每次都入队且每次递增引用计数。与之配对,signal_handler_disconnect 在删除一个 keep_ref 回调时执行原子递减,归零时立即调用真正销毁逻辑(signal.c)。

此外,signal.c 还实现了文档未收录的两个内部能力,值得插件开发者了解:

  • signal_handler_remove_current:回调内部调用即可"自杀",通过线程局部变量 current_signal_cb 标记当前正在执行的回调;
  • signal_handler_connect_global:连接一个接收 handler 上所有信号的观察回调(void (*)(void *data, const char *signal, calldata_t *cd)),适合日志或调试探针场景。

触发阶段的完整流程(signal.c)是:加锁 → 置 signalling = true → 顺序调用所有未标记 remove 的回调(期间设置线程局部指针)→ 逆序遍历删除被标记的回调 → 再执行全局回调 → 扣减因 keep_ref 回调被移除而累积的引用计数。这一设计保证了信号分发期间的回调增删不会造成数组遍历失效。

proc_handler_t:无声明的函数调用

Procedure handlers 用于"在无法直接访问函数声明或回调指针的情况下调用函数"(头文件 proc.h,实现 proc.c)。典型场景是 Lua/Python 脚本层或 IPC 层需要通过字符串名调用 C 侧注册的行为,而不必持有函数指针。

函数 签名 语义
proc_handler_create proc_handler_t *(void) 创建过程处理器
proc_handler_destroy void (proc_handler_t *handler) 销毁过程处理器
proc_handler_add void (proc_handler_t *handler, const char *decl_string, proc_handler_proc_t proc, void *data) 按声明字符串注册一个过程及其回调、私有数据
proc_handler_call bool (proc_handler_t *handler, const char *name, calldata_t *params) 按名字调用过程;找不到时返回 false

回调签名与信号回调同构:void (*proc_handler_proc_t)(void *data, calldata_t *cd)decl_string 的文法与信号声明完全一致(复用 decl.cparse_decl_string),声明非法时打印 LOG_ERROR 并放弃注册(proc.c);重名注册打印 LOG_WARNING 并保持旧注册不变。

proc.c 的实现结构 可以推断 proc_handler_call 的线程安全策略:先在互斥锁内把 proc_info 结构(含回调指针与 data)整体拷贝到栈上,解锁后再调用——调用过程不持锁,因此过程回调内部可以再向同一 handler 注册新过程而不会死锁(mutex 为递归锁,双重保险)。源码头注释中还留有一行 /* TODO: replace with hash table lookup? */,说明当前查找是线性扫描,过程数量多时调用方应有感知。

端到端实战:声明、连接、触发

把三块 API 组合起来,一个完整的事件链路如下(用法与 libobs 内核模式一致,参数命名对齐 obs-internal.h 的内核惯例):

#include <callback/signal.h>

static void on_volume_changed(void *data, calldata_t *cd)
{
    // 回调参数里的 calldata 只读,绝不能 calldata_free
    double vol = calldata_float(cd, "volume");
    (void)data;
    // 业务逻辑...
}

int main(void)
{
    // 1. 创建并声明(重名或文法错误返回 false)
    signal_handler_t *sh = signal_handler_create();
    if (!signal_handler_add(sh, "void volume_changed(double volume)")) {
        // 处理声明冲突
    }

    // 2. 连接
    signal_handler_connect(sh, "volume_changed", on_volume_changed, NULL);

    // 3. 触发:栈上 calldata + 固定缓冲,避免每次事件堆分配
    struct calldata params;
    uint8_t stack[128];
    calldata_init_fixed(&params, stack, sizeof(stack));
    calldata_set_float(&params, "volume", 0.75);
    signal_handler_signal(sh, "volume_changed", &params);

    // 4. 断开并销毁(引用计数归零才真正释放)
    signal_handler_disconnect(sh, "volume_changed", on_volume_changed, NULL);
    signal_handler_destroy(sh);
    return 0;
}

要点回顾:

  • 触发方负责 calldata 的生命周期(栈上 calldata_init_fixed 或堆上 calldata_create),回调方只读;
  • (signal, callback, data) 三元组是连接的唯一键,重复 connect 幂等、重复 disconnect 安全(signal.c);
  • 若 handler 可能被多个模块共享(例如插件注册观察回调后宿主先销毁),应使用 connect_ref/disconnect 配对,让引用计数替代人工时序约束。

源码地图:继续阅读路径

文件 内容
libobs/callback/calldata.h calldata_t 结构体、五类型读写内联实现、call_param_type 枚举
libobs/callback/calldata.c calldata_get_data / calldata_set_data 的扩容与存储实现
libobs/callback/decl.c / decl.h 信号/过程声明字符串的解析器
libobs/callback/signal.c 两级锁、引用计数、延迟移除、全局回调分发
libobs/callback/proc.c 过程注册表与无锁调用
libobs/obs-internal.h 内核 obs_source_dosignal 触发模式
libobs/obs-audio-controls.c 连接 source volume / destroy 信号的实例
docs/sphinx/reference-libobs-callback.rst 本文所依据的 API 参考原文档

小结

libobs 的 callback 模块用约三个轻量对象撑起整个事件体系:calldata_t 是类型安全(以字符串为键、五种类型为值)的参数信封,signal_handler_t 是带引用计数与线程安全延迟移除的事件总线,proc_handler_t 则是按名字寻址的函数注册表。对插件与脚本开发者而言,掌握三元组连接键、"回调内 calldata 只读"的生命周期约定、connect_ref 的引用计数语义这三点,即可安全地在 OBS 内核事件流上挂接自定义逻辑,而无需担心并发触发与对象生命周期竞争。

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