10 / 32 Source、对象、插件与渲染

第 9 课:插件是怎么被加载的

# 第 9 课:插件是怎么被加载的

嘿,我是小方。 第 6 课我说"插件填一张表、调 obs_register_source 注册自己",留了个尾巴:这个调用到底什么时候、被谁触发? 今天把这条线补上,这也是模块 2 的收官课。 这一课我把每个关键函数的真实代码摊开,一行行讲它在干嘛、用了哪个系统库 —— 看完你能明白引擎"招募"插件的全过程,也能照着写出自己的插件骨架。


# 9.1 插件就是一个个独立的 .dll,靠"动态加载"装进来

📍我们在哪:第 6 课说「插件填表注册自己」,却没说这调用谁触发;这一课收官模块 2——先看插件的物理形态:一个个独立的 .dll,靠动态加载装进来。

第 1 课你就见过 obs-plugins/64bit/ 里那 26 个 .dll。它们是独立编译、独立发布的动态库,不编进主程序:

插件就是一个个 .dll

  • 引擎不 #include 插件,插件也不 #include 引擎内部,编译期互不知道对方;
  • 它们运行时才相遇:引擎启动时扫描目录,把 .dll 一个个动态加载进来。

"动态加载一个 .dll"这件事,本身就是个操作系统能力。OBS 把它封装成了 os_dlopen / os_dlsym / os_dlclose 三个跨平台函数,底层在不同系统上接到不同的系统库:

os_dlopen 的跨平台映射

  • Windows:os_dlopenLoadLibrary,os_dlsymGetProcAddress,os_dlcloseFreeLibrary(都来自 <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 宏

OBS_DECLARE_MODULE()
1

它是个宏,展开后生成三个带 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 版本」*/
1
2
3
4
5
6
7
8

加上插件自己写的 obs_module_load,一个合格插件至少导出三个名字:obs_module_loadobs_module_set_pointerobs_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 = 加载成功
}
1
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(&amp;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(&amp;info)               ★ 你回头注册(第 6 课)
1
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;
}
1
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_exportsobs_open_module(obs-module.c:140)里的一环。把主体摊开:

obs_open_module 深读

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;
}
1
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),它短得只有几行,却藏着一个精巧机关:

obs_init_module 与 loadingModule

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;
}
1
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);        //   → 释放,记成「初始化失败」
	...
}
1
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 三阶段、各把所有插件走一遍。

最后一个设计。加载不是"一个插件从头走到尾再下一个",而是分三阶段、各自把所有插件走一遍:

三阶段加载

  1. ① open 全部 —— obs_open_module 把每个 .dll 先打开、验版本(还没调 load);
  2. ② init 全部 —— obs_init_module 再挨个调它们的 obs_module_load(),各自注册;
  3. ③ 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();        // 阶段 ③
1
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_loadobs_register_source」几行。
  • 加载总链:obs_load_all_modules2(OBSApp.cpp:1882)→ 扫目录 → load_all_callbackobs_open_moduleobs_init_modulemodule->load() = 你的 obs_module_load()obs_register_source
  • obs_open_module:① os_dlopen 装载 → ② load_module_exportsos_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 动手 / 观察(本课作业)

  1. 看加载日志:OBS 日志(帮助 → 日志文件)里有大段 "Loading module" 记录 —— 那是 load_all_callback 一个个加载留下的脚印。找找有没有 "incompatible version" 或 "not an OBS plugin" 被跳过的。
  2. 翻一个插件入口:打开 plugins/obs-x264/obs-x264.c,找到 OBS_DECLARE_MODULE()obs_module_load(),看它在 load 里注册了什么(是 obs_register_encoder 吗?)。对照 9.2 的最小骨架。
  3. 读懂版本闸门:回到 obs-module.c:171,用自己的话讲清楚"为什么是 ver > LIBOBS_API_VER 才拒绝,而不是 ver != LIBOBS_API_VER"。
  4. (进阶)找暗号:用工具(Linux nm -D、Windows dumpbin /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/1886libobs/obs-config.h(LIBOBS_API_VER)

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>