blender学习系列之--CLOG

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_PTHREADS
  pthread_mutex_t types_lock;
#endif

  CLG_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_PTHREADS
  pthread_mutex_init(&ctx->types_lock, NULL);
#endif
  ctx->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);
#endif
  if (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_PTHREADS
    atomic_cas_ptr((void **)&clg_ref->type, clg_ref->type, clg_ty);
#else
    clg_ref->type = clg_ty;
#endif
  }
#ifdef WITH_CLOG_PTHREADS
  pthread_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;

  //这里通过过滤器检查确定当前记录器是否记录到日志,设定flag
  if (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);

posted @ 2024-12-21 14:20  平凡人  阅读(12)  评论(0编辑  收藏  举报