首页
/ OBS Studio 的 libobs/util/base.h 日志子系统:blog 日志级别、自定义处理函数与崩溃处理机制

OBS Studio 的 libobs/util/base.h 日志子系统:blog 日志级别、自定义处理函数与崩溃处理机制

2026-09-05 16:39:41作者:魏献源Searcher

本文基于 OBS Studio 文档目录中的参考文档 reference-libobs-util-base.rst,系统讲解 libobs 基础工具层中的日志与崩溃处理接口:blog/blogva 函数、四个日志级别(LOG_ERRORLOG_DEBUG)、可替换的日志处理函数(log_handler_t)以及“只能设置一次”的崩溃处理函数。读完本文,你能理解 OBS 全代码库统一的日志调用方式、前端如何将日志写入文件并驱动日志查看窗口,以及插件和脚本开发者应遵循的日志级别选择规范。

一、模块定位:util/base 是 libobs 的日志与崩溃地基

参考文档开头给出的使用方式是:

#include <util/base.h>

对应源码位于 base.h,文件头注释明确说明其职责范围:“Just contains logging/crash related stuff”(只包含日志/崩溃相关内容)。其实现文件 base.c 非常精简,核心就是两组静态函数指针(日志处理函数与崩溃处理函数)加两个默认实现。从源码结构看,libobs 内部几乎所有 C 模块(内存管理 bmem、动态数组 darray、平台代码 platform-*.c 等)都直接依赖这一套接口输出日志或触发受控崩溃,因此它是理解 OBS 日志体系的第一块拼图。

头文件中还附带了几个与日志输出直接相关的宏(见 base.h):

#define STRINGIFY(x) #x
#define STRINGIFY_(x) STRINGIFY(x)
#define S__LINE__ STRINGIFY_(__LINE__)

#define INT_CUR_LINE __LINE__
#define FILE_LINE __FILE__ " (" S__LINE__ "): "

#define OBS_COUNTOF(x) (sizeof(x) / sizeof(x[0]))

其中 FILE_LINE 会把 __FILE__ 与行号拼接成 "path (123): " 这样的前缀字符串,供调用方拼进 blog 的格式串中,方便在日志里直接定位到出问题的源码位置。

二、四个日志级别:取值、语义与使用规范

参考文档“Logging Levels”一节定义了四个级别,与 base.h 中匿名枚举的注释完全一致:

常量 数值 文档规定的用途
LOG_ERROR 100 出现了可能影响程序的问题,但尚未严重到必须终止程序。应使用在创建函数与核心子系统函数中——即“绝对不能失败”的地方
LOG_WARNING 200 出现了不影响程序且可恢复的问题。适用于失败并非完全意外、可以被安全处理的场景
LOG_INFO 300 输出到日志中的信息性消息
LOG_DEBUG 400 调试消息,主要供开发者使用

头文件中的注释原文(base.h):

enum {
	/**
	 * Use if there's a problem that can potentially affect the program,
	 * but isn't enough to require termination of the program.
	 *
	 * Use in creation functions and core subsystem functions.  Places that
	 * should definitely not fail.
	 */
	LOG_ERROR = 100,

	/**
	 * Use if a problem occurs that doesn't affect the program and is
	 * recoverable.
	 *
	 * Use in places where failure isn't entirely unexpected, and can
	 * be handled safely.
	 */
	LOG_WARNING = 200,

	/**
	 * Informative message to be displayed in the log.
	 */
	LOG_INFO = 300,

	/**
	 * Debug message to be used mostly by developers.
	 */
	LOG_DEBUG = 400
};

注意数值是 100/200/300/400 的等差分布而非 0 起,且数值越大越“安静”——这一点在前端的输出策略中有直接体现(见第五节:LOG_DEBUG 默认不写入日志文件,需要 verbose 模式才输出)。

三、日志处理函数模型:log_handler_t 与 set/get 接口

参考文档定义的类型:

void (*log_handler_t)(int lvl, const char *msg, va_list args, void *p)

即一个接收“日志级别 + printf 风格格式串 + 可变参数列表 + 用户参数”的回调。对应声明在 base.h

typedef void (*log_handler_t)(int lvl, const char *msg, va_list args, void *p);

EXPORT void base_get_log_handler(log_handler_t *handler, void **param);
EXPORT void base_set_log_handler(log_handler_t handler, void *param);

EXPORT void base_set_crash_handler(void (*handler)(const char *, va_list, void *), void *param);

EXPORT void blogva(int log_level, const char *format, va_list args);

两个接口语义:

  • base_set_log_handler(handler, param):替换全局日志处理函数,param 是后续回调中原样传回 p 的用户数据指针;
  • base_get_log_handler(&handler, &param):读出当前处理函数与用户参数,便于在替换前保存旧实现以便链式调用。

base.c 中的一个关键细节:传入 NULL 会回退到内置默认处理器:

void base_set_log_handler(log_handler_t handler, void *param)
{
	if (!handler)
		handler = def_log_handler;

	log_param = param;
	log_handler = handler;
}

默认处理器 def_log_handlerbase.c)的行为很朴素:用 vsnprintf 把格式串渲染到 8192 字节缓冲,再按级别加前缀输出——LOG_DEBUG/LOG_INFO/LOG_WARNINGstdoutLOG_ERRORstderr,每次都 fflush 保证即时可见:

static void def_log_handler(int log_level, const char *format, va_list args, void *param)
{
	char out[8192];
	vsnprintf(out, sizeof(out), format, args);

	switch (log_level) {
	case LOG_DEBUG:
		fprintf(stdout, "debug: %s\n", out);
		fflush(stdout);
		break;
	// LOG_INFO / LOG_WARNING 同理输出到 stdout,
	// LOG_ERROR 输出 "error: %s\n" 到 stderr
	}
}

四、blog 与 blogva:printf 风格输出及编译期格式检查

参考文档中的两个核心函数:

void blogva(int log_level, const char *format, va_list args)   // 使用 va_list 的日志函数
void blog(int log_level, const char *format, ...)               // 变参日志函数

实现(base.c)就是标准的 va_list 转发链:blog 展开变参后调用 blogvablogva 直接调用当前 log_handler

void blogva(int log_level, const char *format, va_list args)
{
	log_handler(log_level, format, args, log_param);
}

void blog(int log_level, const char *format, ...)
{
	va_list args;

	va_start(args, format);
	blogva(log_level, format, args);
	va_end(args);
}

base.h 还为它们附加了编译期格式串检查,让 GCC/Clang 像检查 printf 一样检查 blog 的格式参数:

#if !defined(_MSC_VER) && !defined(SWIG)
#define PRINTFATTR(f, a) __attribute__((__format__(__printf__, f, a)))
#else
#define PRINTFATTR(f, a)
#endif

PRINTFATTR(2, 3)
EXPORT void blog(int log_level, const char *format, ...);
PRINTFATTR(1, 2)
#ifndef SWIG
OBS_NORETURN
#endif
EXPORT void bcrash(const char *format, ...);

这里有两个值得注意的条件编译:MSVC 与 SWIG 绑定场景下关闭该属性;而 bcrash 在非 SWIG 场景下被标记 OBS_NORETURN(不会返回,编译器据此优化)。

五、bcrash 与崩溃处理函数:只能设置一次,且防重入

参考文档对崩溃接口的描述:

void base_set_crash_handler(void (*handler)(const char *, va_list, void *), void *param)
// Sets the current crash handler. This may only be set once.

void bcrash(const char *format, ...)
// Crash function.

“只能设置一次”这一约束在 base.c 中用原子交换实现——第二次调用只会得到一条 LOG_WARNING 并保留第一个处理函数:

void base_set_crash_handler(void (*handler)(const char *, va_list, void *), void *param)
{
	static bool non_default_handler_set = false;

	if (os_atomic_exchange_bool(&non_default_handler_set, true)) {
		blog(LOG_WARNING, "Tried to set a crash handler when one already exists.");
		return;
	}

	crash_param = param;
	crash_handler = handler;
}

bcrash 本体(base.c)带有一个静态的 crashing 重入保护:如果崩溃处理函数内部再次触发 bcrash,直接向 stderr 输出 "Crashed in the crash handler" 并以退出码 2 终止;否则置位标志、调用用户处理函数,并在处理函数返回后 exit(0) 兜底:

OBS_NORETURN void bcrash(const char *format, ...)
{
	va_list args;

	if (crashing) {
		fputs("Crashed in the crash handler", stderr);
		exit(2);
	}

	crashing = 1;
	va_start(args, format);
	crash_handler(format, args, crash_param);
	va_end(args);
	exit(0);
}

默认崩溃处理器 def_crash_handlerbase.c)则把格式串原样打印到 stderr 后 exit(0)

库内部的 bcrash 调用点(源码证据)

从源码结构看,bcrash 主要被用于“代码逻辑错误”与“内存分配失败”这类不可恢复场景,例如:

  • bmem.c:分配 0 字节("Allocating 0 bytes is broken behavior, please fix your code!")与内存耗尽时直接 bcrash
  • darray.hdarray.hdarray_move_item/darray_swap 内存不足时 bcrash
  • 平台代码如 platform-nix.c(无法取得 $HOME)、platform-cocoa.m、[platform-windows.c](https://gitcode.com/GitHub_Trending/ob/obs-studio/blob/ecebe28a1ea87c30f652b5d06fcaf893c787108d/libobs/util/platform-windows.c?utm_source=gitcode_repo_files#L364-L366 附近的 UUID 失败路径) 也遵循同一模式。

六、真实集成:前端如何用这套接口写日志文件

libobs 提供的默认处理器只写标准输出,而 OBS Studio 前端(obs-main.cpp)注册了一个功能完整的 do_logobs-main.cpp),在启动时接管全局日志处理:

	if (logFile.is_open()) {
		delete_oldest_file(false, "obs-studio/logs");
		base_set_log_handler(do_log, &logFile);
	} else {
		blog(LOG_ERROR, "Failed to open log file");
	}

(见 obs-main.cppparam 传入的 &logFile 会在每次回调中经 *static_cast<fstream *>(param) 还原为文件流。)该 do_log 演示了生产级日志处理器的典型做法:

  1. 多路输出:非 Windows 平台用 va_copy 复制参数列表,既写文件又转发默认处理器到控制台(obs-main.cppobs-main.cpp);Windows 上检测到调试器时额外通过 OutputDebugStringW 输出;
  2. 级别门控if (log_level <= LOG_INFO || log_verbose) —— 由于 LOG_DEBUG = 400 大于 LOG_INFO,调试日志默认不写文件,只有开启 verbose 日志(命令行开关)才落盘。这解释了第二节中“数值越大越安静”的设计;
  3. 重复行抑制too_many_repeated_entriesobs-main.cpp)比较当前消息指针与字符和,同一消息连续重复超过 MAX_REPEATED_LINES(30 行)且字符差异小于 MAX_CHAR_VARIATION(255×3)时折叠,并补写一行 "Last log entry repeated for N more lines",避免高频错误日志把文件刷爆;
  4. 时间戳与 UI 联动LogStringChunk/LogStringobs-main.cpp)为每条日志加 HH:MM:SS.mmm 前缀(由 CurrentTimeString 生成),逐行写入文件后,通过 QMetaObject::invokeMethod(App(), &OBSApp::addLogLine, Qt::QueuedConnection, ...) 把日志行以队列方式推给 Qt 主线程,驱动界面中的日志查看窗口;写文件过程由 logfile_mutex 保护。

Windows 崩溃处理函数的前端实现

对应 base_set_crash_handler 的“只能设置一次”约束,前端只在 Windows 分支注册一次 main_crash_handlerobs-main.cpp)。该处理函数([obs-main.cpp](https://gitcode.com/GitHub_Trending/ob/obs-studio/blob/ecebe28a1ea87c30f652b5d06fcaf893c787108d/frontend/obs-main.cpp?utm_source=gitcode_repo_files#L715-L735 附近))把崩溃信息渲染进固定大小缓冲,并用如下文案提示用户复制崩溃日志到剪贴板、同时告知日志保存路径:

#define CRASH_MESSAGE                                                      \
	"Woops, OBS has crashed!\n\nWould you like to copy the crash log " \
	"to the clipboard? The crash log will still be saved to:\n\n%s"

而在 Windows 崩溃捕获链路的另一端,obs-win-crash-handler.c 在收集完崩溃报告后调用 bcrash("%s", data.str.array) 进入统一的崩溃出口——这正好验证了 bcrash 作为“全库唯一受控崩溃点”的定位。

七、测试与脚本绑定中的用法

  • 测试代码:Windows 平台测试入口 test/win/test.cpp 在创建测试窗口前先注册自定义处理函数 base_set_log_handler(do_log, nullptr),演示了“库外程序接管 libobs 日志”的标准姿势;
  • 脚本绑定:SWIG 接口文件 obslua.iobspython.i 中显式声明 %ignore blog; %ignore blogva; %ignore bcrash; %ignore base_set_crash_handler;,即 Lua/Python 脚本 API 不直接暴露这些底层日志与崩溃函数(头文件中 OBS_NORETURN 等条件编译对 SWIG 宏的特判也与此配套)。

八、调用速查表

接口 声明位置 关键行为(实现位置)
blog(level, fmt, ...) base.h 变参日志,带 printf 格式检查;转发到当前 log_handlerbase.c
blogva(level, fmt, va_args) base.h va_list 版本,供已持有参数列表的函数使用(base.c
base_set_log_handler(h, p) base.h 替换全局处理器;NULL 恢复默认输出到 stdout/stderr(base.c
base_get_log_handler(&h, &p) base.h 读取当前处理器与用户参数,便于链式替换(base.c
base_set_crash_handler(h, p) base.h 仅允许设置一次,重复设置得到 LOG_WARNINGbase.c
bcrash(fmt, ...) base.h 受控崩溃出口,防重入,OBS_NORETURNbase.c

实践建议(依据上文文档规范与源码行为):

  • 在“不应失败”的创建函数与核心子系统函数中用 blog(LOG_ERROR, ...);失败可恢复、不预期的地方用 LOG_WARNING;常规状态输出用 LOG_INFO;仅开发者可见的细节用 LOG_DEBUG(默认不会进入日志文件,除非 verbose 模式);
  • 插件/程序入口若要接管日志,先 base_get_log_handler 保存旧处理器,在自定义处理器中链式调用旧实现,程序退出前可像 obs-main.cpp 那样 base_set_log_handler(nullptr, nullptr) 恢复默认;
  • 崩溃处理函数全局只注册一次(通常是主程序在最早阶段注册);不可恢复错误用 bcrash 退出而不是自行 exit,以保证 Windows 等平台能走统一的崩溃报告路径。

以上所有结论均可在当前仓库中按对应文件路径直接查证:接口定义与语义见 libobs/util/base.hlibobs/util/base.c,前端集成见 frontend/obs-main.cpp,文档原始描述见 docs/sphinx/reference-libobs-util-base.rst

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

项目优选

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