CLOG为blender提供的日志记录模块,它实现了日志记录的基本功能,同时提供了过滤功能,可通过参数来控制,实现准确记录所关注的记录,来帮助定位问题关键点。
该模块只有二个文件:CLG_log.h,clog.c。
在CLG_log.h文件最后有以下定义:
#define CLOG_INFO(clg_ref, level, ...) \CLOG_AT_SEVERITY(clg_ref, CLG_SEVERITY_INFO, level, __VA_ARGS__)
#define CLOG_WARN(clg_ref, ...) CLOG_AT_SEVERITY(clg_ref, CLG_SEVERITY_WARN, 0, __VA_ARGS__)
#define CLOG_ERROR(clg_ref, ...) CLOG_AT_SEVERITY(clg_ref, CLG_SEVERITY_ERROR, 0, __VA_ARGS__)
#define CLOG_FATAL(clg_ref, ...) CLOG_AT_SEVERITY(clg_ref, CLG_SEVERITY_FATAL, 0, __VA_ARGS__)#define CLOG_STR_INFO(clg_ref, level, str) \CLOG_STR_AT_SEVERITY(clg_ref, CLG_SEVERITY_INFO, level, str)
#define CLOG_STR_WARN(clg_ref, str) CLOG_STR_AT_SEVERITY(clg_ref, CLG_SEVERITY_WARN, 0, str)
#define CLOG_STR_ERROR(clg_ref, str) CLOG_STR_AT_SEVERITY(clg_ref, CLG_SEVERITY_ERROR, 0, str)
#define CLOG_STR_FATAL(clg_ref, str) CLOG_STR_AT_SEVERITY(clg_ref, CLG_SEVERITY_FATAL, 0, str)/* Allocated string which is immediately freed. */
#define CLOG_STR_INFO_N(clg_ref, level, str) \CLOG_STR_AT_SEVERITY_N(clg_ref, CLG_SEVERITY_INFO, level, str)
#define CLOG_STR_WARN_N(clg_ref, str) CLOG_STR_AT_SEVERITY_N(clg_ref, CLG_SEVERITY_WARN, 0, str)
#define CLOG_STR_ERROR_N(clg_ref, str) CLOG_STR_AT_SEVERITY_N(clg_ref, CLG_SEVERITY_ERROR, 0, str)
#define CLOG_STR_FATAL_N(clg_ref, str) CLOG_STR_AT_SEVERITY_N(clg_ref, CLG_SEVERITY_FATAL, 0, str)
总结来说,上面的代码定义了一组宏,方便用于程序中添加日志记录。分为三组,每组按严重程序分为4级,分别对应:INOF,WARN,ERROR,FATAL;
所有宏最终对应定义到CLOG_AT_SEVERITY,CLOG_STR_AT_SEVERITY,CLOG_STR_AT_SEVERITY_N这三个宏。
#define CLOG_AT_SEVERITY(clg_ref, severity, verbose_level, ...) \{ \CLG_LogType *_lg_ty = CLOG_ENSURE(clg_ref); \if (((_lg_ty->flag & CLG_FLAG_USE) && (_lg_ty->level >= verbose_level)) || \(severity >= CLG_SEVERITY_WARN)) { \CLG_logf(_lg_ty, severity, __FILE__ ":" STRINGIFY(__LINE__), __func__, __VA_ARGS__); \} \} \((void)0)#define CLOG_STR_AT_SEVERITY(clg_ref, severity, verbose_level, str) \{ \CLG_LogType *_lg_ty = CLOG_ENSURE(clg_ref); \if (((_lg_ty->flag & CLG_FLAG_USE) && (_lg_ty->level >= verbose_level)) || \(severity >= CLG_SEVERITY_WARN)) { \CLG_log_str(_lg_ty, severity, __FILE__ ":" STRINGIFY(__LINE__), __func__, str); \} \} \((void)0)#define CLOG_STR_AT_SEVERITY_N(clg_ref, severity, verbose_level, str) \{ \CLG_LogType *_lg_ty = CLOG_ENSURE(clg_ref); \if (((_lg_ty->flag & CLG_FLAG_USE) && (_lg_ty->level >= verbose_level)) || \(severity >= CLG_SEVERITY_WARN)) { \const char *_str = str; \CLG_log_str(_lg_ty, severity, __FILE__ ":" STRINGIFY(__LINE__), __func__, _str); \MEM_freeN((void *)_str); \} \} \((void)0)
这三个宏的定义仅有细微差别,CLOG_AT_SEVERITY接受可变参数,最终调用CLOG_logf函数完成日志记录,CLOG_STR_AT_SEVERITY,CLOG_STR_AT_SEVERITY_N这两个宏均接受字符串作为记录内容,调用CLG_log_str函数记录到日志,后一个害在完成日志记录后立即释放字符串占用内存。
这三个宏在记录之前均对记录器类型进行检查,严重性大于或等于CLG_SEVERITY_WARN的准予记录,或者记录器类型标志为CLG_FLAG_USE且level大于或等于verbose_level的才准予记录,主要是为了过滤INFO类型记录过多而设置。
最终这些宏均调用CLG_logf或CLG_log_str来实现日志的记录。
在进一步分析时,来看看日志的几个结构:
1、日志上下文结构
typedef struct CLogContext {CLG_LogType *types; //记录器类型的单链表 CLG_LogRef *refs; //记录器引用的单链表
#ifdef WITH_CLOG_PTHREADSpthread_mutex_t types_lock;
#endifCLG_IDFilter *filters[2]; //记录过滤器,用于包含或排除某些信息的记录bool use_color;bool use_basename;bool use_timestamp;/** Borrowed, not owned. */int output;FILE *output_file;/** For timer (use_timestamp). */uint64_t timestamp_tick_start;/** For new types. */struct {int level;} default_type;//定义回调函数struct {void (*error_fn)(void *file_handle);void (*fatal_fn)(void *file_handle);void (*backtrace_fn)(void *file_handle);} callbacks;
} CLogContext;
在clog.c中定义了g_ctx全局静态变量,在Blender中,只实现了单一的全局上下文。在CLG_init函数中调用CLG_ctx_init函数对g_ctx进行了初始化。代码如下:
/* 我们可以同时支持多个,但现在似乎不需要。 */
static struct CLogContext *g_ctx = NULL;void CLG_init(void)
{g_ctx = CLG_ctx_init();clg_color_table_init(g_ctx->use_color);
}static CLogContext *CLG_ctx_init(void)
{CLogContext *ctx = MEM_callocN(sizeof(*ctx), __func__);
#ifdef WITH_CLOG_PTHREADSpthread_mutex_init(&ctx->types_lock, NULL);
#endifctx->default_type.level = 1;//这个定义了冗余信息显示级别,仅当记录的冗余级别小于或等于该值时才被记录CLG_ctx_output_set(ctx, stdout);return ctx;
}static void CLG_ctx_output_set(CLogContext *ctx, void *file_handle)
{ctx->output_file = file_handle;ctx->output = fileno(ctx->output_file);//output中保存文件描述符
#if defined(__unix__) || defined(__APPLE__)ctx->use_color = isatty(ctx->output);
#elif defined(WIN32)/* 根据windows10构建18298,所有的标准控制台都支持颜色 *就像linux终端一样,但它需要被打开。 *要打开颜色,我们需要通过传递标志ENABLE_VIRTUAL_TERMINAL_PROCESSING到SetConsoleMode来启用虚拟终端处理 *如果系统不支持虚拟终端处理,它将悄然失败和标志 *将不会被设置。*/GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &clg_previous_console_mode);ctx->use_color = 0;if (IsWindows10OrGreater() && isatty(ctx->output)) {DWORD mode = clg_previous_console_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;if (SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), mode)) {ctx->use_color = 1;}}
#endif
}
从上面代码看出,在g_ctx初始化时,仅初始化了default_type和output二个字段,对两个列表并没有初始化。这就要求在使用时对链表进行维护,同时也提供了灵活性(lazyer access)。
全局上下文中设置了日志输出,所有与它相关记录器具有了相同的上下文。从上下文定义可以看出,一个上下文可以有多种记录器类型,多个记录器,它们构成了上下文中的单链表。
再来看具体的记录器和类型:
/* Don't typedef enums. */
enum CLG_LogFlag {CLG_FLAG_USE = (1 << 0),
};
//定义记录严重程序
enum CLG_Severity {CLG_SEVERITY_INFO = 0,CLG_SEVERITY_WARN,CLG_SEVERITY_ERROR,CLG_SEVERITY_FATAL,
};/* Each logger ID has one of these. */
typedef struct CLG_LogType {struct CLG_LogType *next;char identifier[64];/** FILE output. */struct CLogContext *ctx;/** Control behavior. */int level;enum CLG_LogFlag flag;
} CLG_LogType;//记录器
typedef struct CLG_LogRef {const char *identifier; //记录器标识CLG_LogType *type; //记录器类型struct CLG_LogRef *next; //记录器链表指针
} CLG_LogRef;
从代码中可知,记录器和类型结构定义了日志上下文的链表项,记录器项主要有二个字段,标识和类型。类型项包含类型标识、上下文指针、冗余信息控制级及是否可用标志。
Blender由众多模块构成,在调试时为区分信息,在第个模块中定义了各自记录器,给出了标识符。如下面代码位于wm_init_exit.c:
CLG_LOGREF_DECLARE_GLOBAL(WM_LOG_OPERATORS, "wm.operator");
CLG_LOGREF_DECLARE_GLOBAL(WM_LOG_HANDLERS, "wm.handler");
CLG_LOGREF_DECLARE_GLOBAL(WM_LOG_EVENTS, "wm.event");
CLG_LOGREF_DECLARE_GLOBAL(WM_LOG_KEYMAPS, "wm.keymap");
CLG_LOGREF_DECLARE_GLOBAL(WM_LOG_TOOLS, "wm.tool");
CLG_LOGREF_DECLARE_GLOBAL(WM_LOG_MSGBUS_PUB, "wm.msgbus.pub");
CLG_LOGREF_DECLARE_GLOBAL(WM_LOG_MSGBUS_SUB, "wm.msgbus.sub");
上面代码定义了窗口管理的几个日志记录器。它使用了CLG_LOGREF_DECLARE_GLOBAL宏来定义,在定义了static变量同时也定义了一个同名的非静态指针变量。也可直接如action.c:
static CLG_LogRef LOG = {"bke.action"};
在相关模块记录信息时可直接如下使用:
CLOG_INFO(WM_LOG_OPERATORS, 0, "search for unknown operator '%s', '%s'\n", idname_bl, idname);
CLOG_ERROR(&LOG, "Pose copy error, pose to:%p from:%p", (void *)to, (void *)from);
到这里,我们并没有将记录器、类型、上下文进行关联,在哪里使它们组合在一起的呢?
参考上面CLOG_AT_SEVERITY等几个宏的定义,在记录信息时,调用了CLOG_ENSURE宏,它在第一次使用该记录器时会调用CLG_logref_init函数
void CLG_logref_init(CLG_LogRef *clg_ref)
{
#ifdef WITH_CLOG_PTHREADS/* 在大多数情况下,初始化静态类型时仅运行一次 */pthread_mutex_lock(&g_ctx->types_lock);
#endifif (clg_ref->type == NULL) {/* 将记录器添加到上下文链表*/clg_ref->next = g_ctx->refs;g_ctx->refs = clg_ref;//根据记录器标识查找类型,如没找到则添加类型并关联到上下文CLG_LogType *clg_ty = clg_ctx_type_find_by_name(g_ctx, clg_ref->identifier);if (clg_ty == NULL) {clg_ty = clg_ctx_type_register(g_ctx, clg_ref->identifier);}
#ifdef WITH_CLOG_PTHREADSatomic_cas_ptr((void **)&clg_ref->type, clg_ref->type, clg_ty);
#elseclg_ref->type = clg_ty;
#endif}
#ifdef WITH_CLOG_PTHREADSpthread_mutex_unlock(&g_ctx->types_lock);
#endif
}static CLG_LogType *clg_ctx_type_register(CLogContext *ctx, const char *identifier)
{assert(clg_ctx_type_find_by_name(ctx, identifier) == NULL);CLG_LogType *ty = MEM_callocN(sizeof(*ty), __func__);ty->next = ctx->types;ctx->types = ty;strncpy(ty->identifier, identifier, sizeof(ty->identifier) - 1);ty->ctx = ctx;ty->level = ctx->default_type.level;//这里通过过滤器检查确定当前记录器是否记录到日志,设定flagif (clg_ctx_filter_check(ctx, ty->identifier)) {ty->flag |= CLG_FLAG_USE;}return ty;
}
在初始化记录器时,如果是新的记录器,则链接到上下文中,检查是否有相关的记录器类型,没有则创建一个,也链接到上下文。同时检查过滤器是否包含该类型,如果不包含,则设置flag为不使用。
延后处理记录器和类型的好处是灵活多变,可在任何模块中灵活使用。
当我们在代码中调用了相关记录宏,但可能日志并没有真正输出,是因为有二个地方判定不输出导致的,一个就是冗余级别超出了类型设定的级别;另一个就是过滤器排除了它的输出。也就是通过这二个来更精准地控制日志的输出。
最基本的使用如下代码:
static CLG_LogRef LOG = { "CLOG.test" };int main()
{CLG_init();const char* test_filter = "CLOG.*";//必须设置过滤器,默认不显示任何记录//在blender中,可通过启动程序参数来设置需要过滤显示的日志记录,具体代码见creator_args.c中arg_handle_log_set函数CLG_type_filter_include(test_filter, strlen(test_filter));//仅显示level<=1的日志CLOG_INFO(&LOG, 1, "test log!");CLOG_INFO(&LOG, 2, "不会显示!");std::cout << "Hello World!\n";CLG_exit();
}
如果在window环境下控制台不能正确显示中文,可使用下面的代码来设置:
SetConsoleOutputCP(CP_UTF8);