如何在C语言中实现支持printf格式的异步日志器?
C语言异步日志器的简便实现方案
核心思路
绕开直接存储va_list(其底层实现依赖编译器和平台,可移植性差且难以序列化),转而在调用线程中提前解析格式字符串,将所有参数转换为可安全存储的统一类型后存入队列。后台线程只需从队列取出预解析数据,完成最终格式化与输出即可。
具体实现步骤
1. 定义队列存储的日志结构体
结构体需包含日志级别、格式字符串副本,以及序列化后的参数集合:
typedef enum { LOG_INFO, LOG_WARN, LOG_ERROR } LogLevel; typedef union { int i; long l; unsigned int u; unsigned long ul; double d; void* ptr; char* str; // 存储复制后的字符串 } LogArg; typedef struct { LogLevel level; char* fmt; // 格式字符串副本 size_t arg_count; LogArg args[]; // 柔性数组存储参数 } LogEntry;
2. 调用线程解析格式并序列化参数
在log_info这类接口中,遍历格式字符串的%占位符,识别参数类型并对应处理:
- 基本类型(int、long、指针等):直接拷贝值到
LogArg - 字符串
%s:调用strdup复制原字符串,将副本指针存入LogArg - 浮点数
%f/%g:以double类型存储(可变参数会自动将float提升为double)
示例代码框架:
#define log_info(fmt, ...) _log_entry(LOG_INFO, fmt, __VA_ARGS__) // 统计格式字符串中的参数数量 static size_t _count_fmt_args(const char* fmt) { size_t count = 0; while (*fmt) { if (*fmt == '%') { fmt++; // 跳过宽度、精度等修饰符 while (isdigit(*fmt) || *fmt == '.' || *fmt == '-') fmt++; // 跳过转义的% if (*fmt == '%') continue; count++; } fmt++; } return count; } static void _log_entry(LogLevel level, const char* fmt, ...) { va_list ap; va_start(ap, fmt); size_t arg_count = _count_fmt_args(fmt); size_t entry_size = sizeof(LogEntry) + arg_count * sizeof(LogArg); LogEntry* entry = malloc(entry_size); if (!entry) goto cleanup; entry->fmt = strdup(fmt); if (!entry->fmt) { free(entry); goto cleanup; } entry->level = level; entry->arg_count = arg_count; va_list ap_copy; va_copy(ap_copy, ap); const char* p = fmt; size_t idx = 0; while (*p) { if (*p == '%') { p++; // 跳过修饰符 while (isdigit(*p) || *p == '.' || *p == '-') p++; if (*p == '%') { p++; continue; } // 匹配参数类型并序列化 switch (*p) { case 'd': case 'i': entry->args[idx].i = va_arg(ap_copy, int); break; case 'u': case 'x': case 'X': entry->args[idx].u = va_arg(ap_copy, unsigned int); break; case 'l': p++; if (*p == 'd' || *p == 'i') entry->args[idx].l = va_arg(ap_copy, long); else if (*p == 'u') entry->args[idx].ul = va_arg(ap_copy, unsigned long); break; case 'f': case 'g': case 'e': entry->args[idx].d = va_arg(ap_copy, double); break; case 's': { const char* str = va_arg(ap_copy, const char*); entry->args[idx].str = str ? strdup(str) : strdup("(null)"); break; } case 'p': entry->args[idx].ptr = va_arg(ap_copy, void*); break; default: break; } idx++; } p++; } va_end(ap_copy); // 将日志条目存入线程安全队列 _enqueue_log_entry(entry); cleanup: va_end(ap); }
3. 后台线程格式化并输出日志
后台线程从队列取出LogEntry,根据格式字符串和预序列化参数生成最终日志,完成后释放资源:
static void* log_thread(void* arg) { while (!_should_exit) { LogEntry* entry = _dequeue_log_entry(); if (!entry) { usleep(10000); continue; } // 自定义格式化函数,替代vsprintf char buf[4096]; _custom_format(buf, sizeof(buf), entry->fmt, entry->arg_count, entry->args); // 输出到目标(文件/控制台等) printf("[%s] %s\n", _level_to_str(entry->level), buf); // 释放资源 free(entry->fmt); for (size_t i = 0; i < entry->arg_count; i++) { // 识别字符串参数并释放副本 const char* p = entry->fmt; size_t arg_idx = 0; while (*p && arg_idx <= i) { if (*p == '%') { p++; while (isdigit(*p) || *p == '.' || *p == '-') p++; if (*p == '%') { p++; continue; } if (arg_idx == i && *p == 's') { free(entry->args[i].str); break; } arg_idx++; } p++; } } free(entry); } return NULL; }
4. 关键技巧说明
- 放弃va_list存储:通过预解析格式字符串,将参数转换为统一的
LogArg联合体,彻底解决va_list序列化的跨平台问题。 - 字符串即时复制:在调用线程完成字符串副本创建,避免原字符串失效导致的野指针问题,后台线程负责释放副本。
- 线程安全队列:使用带互斥锁的环形缓冲区或无锁队列,确保多线程下的日志条目存取安全。
注意事项
- 格式解析需覆盖常用修饰符(如宽度、精度、
l前缀等),可根据需求扩展。 - 内存管理需严谨,所有
malloc/strdup的内存必须在后台线程释放,避免泄漏。 - 可通过宏封装不同级别的日志接口,简化调用。
内容的提问来源于stack exchange,提问作者Kevin Meier
相关产品推荐
相关产品推荐

