You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 16:52:58