第 9 课:插件是怎么被加载的
# 第 9 课:插件是怎么被加载的
嘿,我是小方。 第 6 课我说"插件填一张表、调
obs_register_source注册自己",留了个尾巴:这个调用到底什么时候、被谁触发? 今天把这条线补上,这也是模块 2 的收官课。 这一课我把每个关键函数的真实代码摊开,一行行讲它在干嘛、用了哪个系统库 —— 看完你能明白引擎"招募"插件的全过程,也能照着写出自己的插件骨架。
# 9.1 插件就是一个个独立的 .dll,靠"动态加载"装进来
📍我们在哪:第 6 课说「插件填表注册自己」,却没说这调用谁触发;这一课收官模块 2——先看插件的物理形态:一个个独立的
.dll,靠动态加载装进来。
第 1 课你就见过 obs-plugins/64bit/ 里那 26 个 .dll。它们是独立编译、独立发布的动态库,不编进主程序:

- 引擎不
#include插件,插件也不#include引擎内部,编译期互不知道对方; - 它们运行时才相遇:引擎启动时扫描目录,把
.dll一个个动态加载进来。
"动态加载一个 .dll"这件事,本身就是个操作系统能力。OBS 把它封装成了 os_dlopen / os_dlsym / os_dlclose 三个跨平台函数,底层在不同系统上接到不同的系统库:

- Windows:
os_dlopen→LoadLibrary,os_dlsym→GetProcAddress,os_dlclose→FreeLibrary(都来自<windows.h>,kernel32)。看libobs/util/platform-windows.c:122,os_dlsym的实现就一行:void *os_dlsym(void *module, const char *func) { return (void *)GetProcAddress(module, func); // 「按函数名」在 dll 里取地址 }1
2
3
4 - Linux / macOS:接到
<dlfcn.h>(libdl)的dlopen/dlsym/dlclose。
记住这两个动作的含义,下面全程要用:os_dlopen = 把 .dll 装进进程;os_dlsym = 按函数名,从装进来的 dll 里把某个函数的地址揪出来。
🗒️ 术语速记:符号(symbol)=库里对外公开的一个函数/变量名;符号表=dll 对外公开的名字清单;句柄(handle)=拿到一个打开资源的"把手";DARRAY / da_*=OBS 自己的"动态数组"宏(能自动扩容的数组)。
# 9.2 接头暗号:OBS_DECLARE_MODULE(附:最小插件骨架)
📍接上一节:知道了引擎会
os_dlopen一个 dll,但它凭什么认定「这是个 OBS 插件」?靠一套双方约定的导出函数名——这一节讲这个接头暗号。
引擎加载一个陌生 .dll,凭什么认定"这是个能用的 OBS 插件"?靠一套双方约定好的导出函数名。每个插件源码里都有这一行:

OBS_DECLARE_MODULE()
它是个宏,展开后生成三个带 EXPORT(导出到 dll 符号表 —— 符号表就是 dll 对外公开的函数名清单)的函数(obs-module.h:76):
#define OBS_DECLARE_MODULE() \
static obs_module_t *obs_module_pointer; \
MODULE_EXPORT void obs_module_set_pointer(obs_module_t *m) \
{ obs_module_pointer = m; } /* 接住引擎给的句柄 */ \
obs_module_t *obs_current_module(void) \
{ return obs_module_pointer; } /* 我是谁 */ \
MODULE_EXPORT uint32_t obs_module_ver(void) \
{ return LIBOBS_API_VER; } /* 返回「编译时的 libobs 版本」*/
2
3
4
5
6
7
8
加上插件自己写的 obs_module_load,一个合格插件至少导出三个名字:obs_module_load、obs_module_set_pointer、obs_module_ver。少一个,引擎用 os_dlsym 找不到,就判定"这不是 OBS 插件",跳过。 这个宏就是接头暗号。
# 你自己写个最小插件,长这样
把暗号和注册凑齐,一个能跑的最小插件其实就这么几行 —— 你完全可以照抄当起点:
#include <obs-module.h>
OBS_DECLARE_MODULE() // ① 暗号:生成 set_pointer / ver 等
OBS_MODULE_USE_DEFAULT_LOCALE("my-plugin", "en-US") // ② 可选:用默认多语言机制
extern struct obs_source_info my_source; // 你的源,在别处填好那张「登记表」
bool obs_module_load(void) // ③ 引擎会调到这里(本课主角)
{
obs_register_source(&my_source); // 把你的源注册进引擎(第 6 课)
return true; // 返回 true = 加载成功
}
2
3
4
5
6
7
8
9
10
11
12
obs_module_load 就是你的"开机入口"——引擎加载完你的 dll,会调它,你在里面把自己提供的源 / 编码器 / 输出注册进去。那"引擎会调它"具体发生在哪?往下看。
特别留意 obs_module_ver 返回的 LIBOBS_API_VER:它在编译你的插件时被烤进 dll,记录"我是拿哪个版本的 libobs 编的"。9.4 你会看到它的大用处。
# 9.3 源码深读:从"启动"到"插件自我注册"的总链
📍为什么现在讲这个:暗号和骨架都有了,这一节先把从「启动」到「你的
obs_module_load被调」的整条链拉一条总览,后面再逐段深读。
引擎启动后,一条链把上面这些串起来,终点正是 obs_register_source:

obs_load_all_modules2() OBSApp.cpp:1882 ← 界面启动时调一次
└─ obs_find_modules2(load_all_callback) obs-module.c:567 扫 obs-plugins/ 找 .dll
└─ load_all_callback(每个文件一次) obs-module.c:493
├─ obs_open_module(&module, path) obs-module.c:140 打开 + 验明正身
└─ obs_init_module(module) obs-module.c:233 点火
└─ module->load() obs-module.c:245 ★ 调你的 obs_module_load()
└─ obs_register_source(&info) ★ 你回头注册(第 6 课)
2
3
4
5
6
7
obs_find_modules2 负责"扫目录、对每个 .dll 调一次 load_all_callback"。重头戏是 load_all_callback 里那两步:打开(open)和点火(init)。下面逐个深读。
# 9.4 obs_open_module:打开,并逐项验明正身
📍接上一节:总链的第一步是「打开」——这一节钻进
obs_open_module,看它怎么装载 dll、找暗号、卡版本、建档。
# 先看"按名字找符号":load_module_exports
打开一个 dll 后,引擎要把暗号里那几个函数地址揪出来。这就是 load_module_exports(obs-module.c:38),代码很直白:
static int load_module_exports(struct obs_module *mod, const char *path)
{
mod->load = os_dlsym(mod->module, "obs_module_load"); // 必有:插件入口
if (!mod->load)
return req_func_not_found("obs_module_load", path); // 找不到 → 失败
mod->set_pointer = os_dlsym(mod->module, "obs_module_set_pointer"); // 必有(宏生成)
if (!mod->set_pointer)
return req_func_not_found("obs_module_set_pointer", path);
mod->ver = os_dlsym(mod->module, "obs_module_ver"); // 必有(宏生成)
if (!mod->ver)
return req_func_not_found("obs_module_ver", path);
/* 下面这些是「可选」的,找不到也不报错,只是置空 */
mod->unload = os_dlsym(mod->module, "obs_module_unload");
mod->post_load = os_dlsym(mod->module, "obs_module_post_load");
mod->set_locale = os_dlsym(mod->module, "obs_module_set_locale");
mod->name = os_dlsym(mod->module, "obs_module_name");
mod->description = os_dlsym(mod->module, "obs_module_description");
...
return MODULE_SUCCESS;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
逐行的意思很清楚:用 os_dlsym(9.1 那个 GetProcAddress/dlsym)按函数名去 dll 里找。前三个是必有(就是 9.2 的暗号),少一个直接返回失败;后面一串是可选回调(卸载、加载后、多语言…),插件没实现就置空,引擎之后判空再决定调不调。所谓"是不是 OBS 插件",判据就是前三个找不找得到。
# 再看 obs_open_module 主体
load_module_exports 是 obs_open_module(obs-module.c:140)里的一环。把主体摊开:

int obs_open_module(obs_module_t **module, const char *path, const char *data_path)
{
struct obs_module mod = {0};
mod.module = os_dlopen(path); // ① 把 .dll 装进进程地址空间
if (!mod.module)
return MODULE_FAILED_TO_OPEN; // 连打开都失败(缺依赖等)
int errorcode = load_module_exports(&mod, path); // ② 按名字找那几个导出符号(上面)
if (errorcode != MODULE_SUCCESS)
return errorcode;
/* ③ 版本闸门 */
uint32_t ver = mod.ver ? mod.ver() & 0xFFFF0000 : 0; // 调插件的 obs_module_ver(),只取 major.minor
if (ver > LIBOBS_API_VER) { // 插件比当前引擎「新」
blog(LOG_WARNING, "Module compiled with newer libobs");
return MODULE_INCOMPATIBLE_VER; // → 拒绝
}
mod.bin_path = bstrdup(path); // ④ 建档:记下路径、文件名、数据目录
mod.mod_name = get_module_name(mod.file);
mod.data_path = bstrdup(data_path);
mod.next = obs->first_module; // 挂进引擎的模块链表
da_init(mod.sources); da_init(mod.encoders); // 初始化「这个模块注册了哪些东西」的数组
da_init(mod.outputs); da_init(mod.services);
*module = bmemdup(&mod, sizeof(mod)); // 把这个模块结构复制到堆上,正式建立
obs->first_module = (*module);
mod.set_pointer(*module); // ⑤ 调插件的 set_pointer,把句柄递给它
if (mod.set_locale) // 可选回调:有就调,设多语言
mod.set_locale(obs->locale);
return MODULE_SUCCESS;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
四个关键步骤,逐个说:
- ①
os_dlopen(path)—— 把这个.dll装进进程。在 Windows 上就是LoadLibrary。装失败(比如它依赖的别的 dll 不在)→MODULE_FAILED_TO_OPEN。 - ②
load_module_exports—— 上面那段,找暗号。找不到 → 直接当"不是 OBS 插件"。 - ③ 版本闸门 —— 调插件的
obs_module_ver()拿到它编译时烤进去的LIBOBS_API_VER,& 0xFFFF0000抹掉低 16 位的补丁号(补丁号兼容,不计较),只比 major.minor。插件比引擎新就拒绝。道理很硬:新版编的插件可能调用引擎里还不存在的函数,放进来迟早崩。 - ④ 建档 —— 把路径、文件名挂好,并
da_init(da_*是 OBS 的动态数组宏)四个数组(sources/encoders/outputs/services)。它们记录"这个模块都注册了哪些东西",将来卸载时好一一撤销。 - ⑤
mod.set_pointer(*module)—— 调插件那个宏生成的obs_module_set_pointer,把"模块句柄"递过去存好。从此插件调obs_current_module()就能拿到"我自己"。
注意:到这里,插件的
obs_module_load()还没被调!obs_open_module只负责"打开 + 验明正身 + 建档"。真正点火是下一步。这也是 9.6 要讲的"分阶段"的伏笔。
顺带回答一个常见疑惑:为什么升级了大版本 OBS,某些老插件"加载失败"?就是栽在③这道闸门 —— 不过反过来,比引擎旧的插件是放行的(
ver > LIBOBS_API_VER才拒,不是!=),因为旧插件用到的函数,新引擎都还在。这就是LIBOBS_API_MAJOR_VER(obs-config.h,当前 32)每次破坏性改动 +1 的意义。
# 9.5 obs_init_module:点火,并记下"谁在注册"
📍接上一节:打开只是验明正身,还没真正跳进插件;这一节看「点火」的
obs_init_module怎么调到你的obs_module_load,闭合第 6 课那条线。
obs_open_module 只是打开。真正调用插件入口的是 obs_init_module(obs-module.c:233),它短得只有几行,却藏着一个精巧机关:

bool obs_init_module(obs_module_t *module)
{
if (!module || !obs) return false;
if (module->loaded) return true; // 已经加载过,别重复
loadingModule = module; // ★ 记下「现在正在加载谁」(全局变量)
module->loaded = module->load(); // ★ 调插件的 obs_module_load() —— 跳进你的代码!
loadingModule = NULL; // 加载完,清空
if (!module->loaded)
blog(LOG_WARNING, "Failed to initialize module '%s'", module->file);
return module->loaded;
}
2
3
4
5
6
7
8
9
10
11
12
13
module->load() 这个函数指针,指向的就是你写的 obs_module_load()(9.2 那个)。这一行,正式跳进插件代码,你在里面调 obs_register_source(&info) —— 第 6 课那条线的起点,终于对上了。
# loadingModule 这个全局,妙在哪
注意调用前后那两句 loadingModule = module / = NULL。它的作用是:让"注册"动作知道当前是哪个插件在注册。
你在 obs_module_load() 里调 obs_register_source 时,引擎一看 loadingModule,就知道"这个源是 win-capture 提供的",记到它名下(回看第 6 课 obs_register_source_s 里那句 da_push_back(loadingModule->sources, ...))。这样引擎才能回答:某个源来自哪个插件、卸载插件时撤哪些源。
它还撑起一道护栏(obs-module.c:894):注册函数会先看 loadingModule 是不是非空,否则报 "Tried to register ... outside of obs_module_load"。注册只能发生在 obs_module_load() 期间 —— 你要是手贱在别处调 obs_register_*,直接被拦。
# 顺便看 load_all_callback 怎么处理各种失败
obs_open_module + obs_init_module 都是被 load_all_callback(obs-module.c:493)串起来调的,它对每个失败码的处理也值得一看:
int code = obs_open_module(&module, info->bin_path, info->data_path);
switch (code) {
case MODULE_MISSING_EXPORTS: // 没那三个暗号 → 根本不是 OBS 插件,静默跳过
return;
case MODULE_FAILED_TO_OPEN: // dll 都打不开(缺依赖)→ 记成「失败模块」
...
goto load_failure;
case MODULE_INCOMPATIBLE_VER: // 版本闸门拦下 → 记成「失败模块」
...
goto load_failure;
}
if (!obs_init_module(module)) { // 打开成功,但插件 obs_module_load() 返回 false
free_module(module); // → 释放,记成「初始化失败」
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
你在 OBS 日志里看到的 "not an OBS plugin" / "incompatible version" / "Failed to initialize",一一对应这里的几个分支。这就是为什么坏插件不会把整个 OBS 拖垮 —— 每个都被单独 try、单独记账。
# 9.6 为什么要分"三阶段"加载
📍收束:单个插件怎么加载讲完了,最后拉高一层——为什么整个加载要分 open/init/post_load 三阶段、各把所有插件走一遍。
最后一个设计。加载不是"一个插件从头走到尾再下一个",而是分三阶段、各自把所有插件走一遍:

- ① open 全部 ——
obs_open_module把每个.dll先打开、验版本(还没调load); - ② init 全部 ——
obs_init_module再挨个调它们的obs_module_load(),各自注册; - ③ post_load 全部 ——
obs_post_load_modules最后挨个调可选的obs_module_post_load()。
为什么不一个插件"开+init+post"一条龙?因为跨插件依赖。 B 插件在 post_load 里可能要用 A 插件注册好的东西;只有保证"② 所有插件都 init 完"再统一进 ③,这种依赖才不会扑空。
界面在启动时依次触发(frontend/OBSApp.cpp:1882):
obs_load_all_modules2(&mfi); // 阶段 ① + ②(open 全部 + init 全部)
...
obs_post_load_modules(); // 阶段 ③
2
3
至此,从"双击 obs64.exe"到"所有插件就位、所有源/编码器/输出都注册好",这条启动主干你算走通了。
# 9.7 本课小结
- 插件 = 独立
.dll,运行时动态加载:os_dlopen/os_dlsym跨平台封装LoadLibrary/GetProcAddress(Windows,<windows.h>)或dlopen/dlsym(Linux/macOS,<dlfcn.h>libdl)。 OBS_DECLARE_MODULE(obs-module.h:76)生成暗号obs_module_set_pointer/obs_current_module/obs_module_ver(返回烤进去的LIBOBS_API_VER);最小插件就是「暗号 +obs_module_load里obs_register_source」几行。- 加载总链:
obs_load_all_modules2(OBSApp.cpp:1882)→ 扫目录 →load_all_callback→obs_open_module→obs_init_module→module->load()= 你的obs_module_load()→obs_register_source。 obs_open_module:①os_dlopen装载 → ②load_module_exports用os_dlsym找暗号(必有三个 + 可选若干)→ ③ 版本闸门(ver & 0xFFFF0000 > LIBOBS_API_VER才拒,旧插件放行)→ ④ 建档(da_init 四个数组)→ ⑤set_pointer把句柄给插件。obs_init_module:module->load()点火;loadingModule全局让注册知道"谁在注册"(归属 + 越界护栏),闭合第 6 课。load_all_callback对每个失败码单独记账,坏插件不拖垮整体。- 三阶段(open 全部 → init 全部 → post_load 全部):为让跨插件依赖在
post_load时都已就绪。
# 9.8 动手 / 观察(本课作业)
- 看加载日志:OBS 日志(帮助 → 日志文件)里有大段 "Loading module" 记录 —— 那是
load_all_callback一个个加载留下的脚印。找找有没有 "incompatible version" 或 "not an OBS plugin" 被跳过的。 - 翻一个插件入口:打开
plugins/obs-x264/obs-x264.c,找到OBS_DECLARE_MODULE()和obs_module_load(),看它在load里注册了什么(是obs_register_encoder吗?)。对照 9.2 的最小骨架。 - 读懂版本闸门:回到
obs-module.c:171,用自己的话讲清楚"为什么是ver > LIBOBS_API_VER才拒绝,而不是ver != LIBOBS_API_VER"。 - (进阶)找暗号:用工具(Linux
nm -D、Windowsdumpbin /EXPORTS)看任意一个插件.dll导出了哪些符号,确认obs_module_load/obs_module_set_pointer/obs_module_ver都在。
# 下一课预告
模块 2「OBS 的世界观」到此全部讲完 —— 一切皆 Source、对象生死、信号通信、插件加载,四把钥匙在手。
从下一课起进入模块 3:画面是怎么来的 —— 从采集第一帧开始,接着是 GPU 与图形子系统、滤镜、场景合成,顺着流水线把视频链路走完。下节课见。
📁 本课配图:
imgs/09-01~imgs/09-07📌 源码锚点:libobs/obs-module.h:76(OBS_DECLARE_MODULE)、libobs/obs-module.c:38(load_module_exports)、:140(obs_open_module)、:160/:171(os_dlopen/版本闸门)、:199(set_pointer)、:233/:245(obs_init_module / module->load)、:493(load_all_callback 错误处理)、:567(find_modules2)、:894(注册护栏)、:956(obs_register_source_s)、libobs/util/platform-windows.c:64/122(os_dlopen/os_dlsym)、frontend/OBSApp.cpp:1882/1886、libobs/obs-config.h(LIBOBS_API_VER)
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言