侧边栏壁纸
博主头像
银河驿站 博主等级

行动起来,活在当下

  • 累计撰写 97 篇文章
  • 累计创建 17 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

跨平台日志宏 LOGI

Administrator
2026-07-31 / 0 评论 / 0 点赞 / 1 阅读 / 0 字

跨平台日志宏 LOGI

一个单头文件、零依赖的 C 语言日志宏,自动打印 毫秒级时间 + 线程号 + 标签(tag) + 消息
并针对不同平台自动分发到各自的原生日志系统。

支持平台:Android / 鸿蒙(OpenHarmony) / Linux / Windows / macOS


目录


特性

  • 单头文件即用:只需 #include "log.h",无需编译静态库。
  • 自动上下文信息:每条日志自带毫秒级时间戳、线程号、tag,便于排查并发与时序问题。
  • 跨平台原生输出
    • Android → logcat(使用 __android_log_print
    • 鸿蒙 → HiLog(使用 OH_LOG_Print
    • Linux / macOS / Windows → 标准输出(stdout
  • 可变参数兼容:统一使用 ##__VA_ARGS__,GCC/Clang 原生支持,MSVC 2015+ 扩展支持(含空参数场景)。

文件清单

文件 说明
log.h 核心实现:平台检测、线程号/时间获取、LOGI / LOGE
main.c 使用示例,可在 Linux/macOS/Windows 直接编译运行

快速开始

#include "log.h"

int main(void) {
    LOGI("MyApp", "hello world, count=%d", 1);
    LOGI("Network", "connect to %s:%d, rtt=%.2fms", "127.0.0.1", 8080, 3.14);
    LOGI("Lifecycle", "app started");   /* 无附加参数也支持 */
    LOGE("DB", "query failed: %s", "timeout");
    return 0;
}

编译

# Linux / macOS
gcc -o demo main.c

# Windows (MSVC)
cl /utf-8 main.c

# Windows (MinGW)
gcc -o demo main.c

Android / 鸿蒙:在对应 NDK 工程中 #include "log.h" 即可,无需单独编译(链接要求见下)。


接口说明

LOGI(tag, fmt, ...)

信息级别日志。

参数 说明
tag 日志标签字符串(C 字符串字面量或变量均可)
fmt printf 风格格式串
... 可变参数,数量与 fmt 中占位符匹配;可省略(零附加参数)

LOGE(tag, fmt, ...)

错误级别日志,输出到 stderr(其余行为与 LOGI 一致,消息前缀带 [E])。

默认仅在 log.h 中提供 LOGILOGE;如需 LOGD / LOGW 等级别,可按 LOGE 模式扩展。


输出格式

[YYYY-MM-DD HH:MM:SS.mmm][T:tid][tag] message
  • 时间:本地时区,YYYY-MM-DD HH:MM:SS.mmm(毫秒为 3 位补零)。
  • 线程号 tid:各平台含义见实现细节
  • tag:调用时传入的标签。

平台差异说明:

  • Android / 鸿蒙tag 作为原生日志系统(logcat / HiLog)的 tag 单独传递,消息体格式为
    [时间][T:tid] 消息(不在消息体内重复 [tag],因为原生系统已按 tag 归类)。
  • Linux / macOS / Windows:消息体格式为 [时间][T:tid][tag] 消息

各平台编译与链接

平台 所需头文件 / 库 链接说明
Android `` 链接 log 库:CMaketarget_link_libraries(xxx log)ndk-buildLOCAL_LDLIBS := -llog
鸿蒙 (OpenHarmony) `` 链接 libhilog_ndk.z.so;需用 LOG_DOMAIN 宏指定 domain(默认 0,可在 include 前覆盖)
Linux gcc -o demo main.c;极老 glibc(< 2.17)需额外链接 -lrt 才能使用 clock_gettime
macOS clang -o demo main.c(macOS 10.12+ 原生提供 clock_gettime
Windows `` 无需额外库;cl main.c 即可

鸿蒙 LOG_DOMAIN

OH_LOG_Print 要求一个 domain 标识(取值范围 0 ~ 0xDFFF)。默认在 log.h 中定义为 0

#define LOG_DOMAIN 1234   /* 在 #include "log.h" 之前定义 */
#include "log.h"

实现细节

线程号获取

平台 API 说明
Linux / Android syscall(SYS_gettid) 内核真实 tid,可与 top/ps -T 对应
鸿蒙 gettid() 内核 tid
Windows GetCurrentThreadId() 系统线程 ID
macOS pthread_mach_thread_np(pthread_self()) Mach 线程端口
其他(回退) (uintptr_t)pthread_self() pthread_t 本身

统一返回 unsigned long long,避免不同平台类型差异。

时间获取(毫秒)

  • WindowsGetLocalTime() 直接返回 SYSTEMTIME,其中 wMilliseconds 即为毫秒字段,用 snprintf 手工格式化 %03d
  • POSIX(Linux / macOS / Android / 鸿蒙)
    • clock_gettime(CLOCK_REALTIME, &ts) 取当前秒 + 纳秒;
    • localtime_r() 将秒转换为本地时区struct tm
    • strftime 格式化到 YYYY-MM-DD HH:MM:SS
    • 再追加 .%03d,毫秒 = ts.tv_nsec / 1000000

注意事项与 FAQ

Q:MSVC 编译出现 C4819「文件包含不能在当前代码页表示的字符」?
A:源文件含中文注释。编译时加 /utf-8(如 cl /utf-8 main.c)即可消除警告,不影响功能。

Q:需要兼容 VS2013 及更早的 MSVC 吗?
A:将宏中的 , ##__VA_ARGS__ 改为 , __VA_ARGS__,并保证每次调用都至少带一个可变参数。

Q:缓冲区够用吗?
A:内部 _log_t_buf[32],毫秒格式最长约 24 字符(含结尾 \0),安全。

Q:想输出到 stderr 或文件而不是 stdout?
A:log.h 中 Linux/macOS/Windows 分支的 LOGIprintf(...) 换成 fprintf(stderr, ...) 或已 fopen 的文件指针即可。

Q:如何扩展更多日志级别?
A:参考 LOGE 的实现,新增 LOGD / LOGW 等宏,区别仅在于级别标识或输出目标。


示例输出

main.c 在 Linux / macOS / Windows 下的典型输出(时间戳随运行变化):

[2026-07-31 16:17:55.331][T:1708][MyApp] hello world, count=1
[2026-07-31 16:17:55.331][T:1708][Network] connect to 127.0.0.1:8080, rtt=3.14ms
[2026-07-31 16:17:55.331][T:1708][Lifecycle] app started
[2026-07-31 16:17:55.331][T:1708][DB][E] query failed: timeout

最后一行由 LOGE 输出到 stderr,前缀带 [E]
多线程环境下,[T:tid] 会随线程不同而变化,便于区分各线程日志。


附录:完整源码

以下为项目源文件完整内容,可直接复制到工程中使用。

log.h

#ifndef LOG_H
#define LOG_H

/*
 * ============================================================================
 *  跨平台日志宏  LOGI(tag, fmt, ...)
 *  支持: Android / 鸿蒙(OpenHarmony) / Linux / Windows / macOS
 *  输出格式: [YYYY-MM-DD HH:MM:SS.mmm][T:tid][tag] message
 *
 *  说明:
 *   - 统一使用 ##__VA_ARGS__ 处理可变参数。GCC/Clang 原生支持;
 *     MSVC 自 VS2015 起将其作为扩展支持(默认传统预处理器即可,
 *     VS2019 16.5+ 在 /Zc:preprocessor 下完全符合标准)。
 *   - 若需兼容 VS2013 及更早版本,请把 ", ##__VA_ARGS__" 改为 ", __VA_ARGS__"
 *     并保证每次调用都至少带一个可变参数。
 * ============================================================================
 */

#include <stdio.h>
#include <string.h>
#include <time.h>

/* ============================================================================
 *  1. 平台检测与对应头文件
 * ========================================================================== */

#if defined(__ANDROID__)
    /* Android NDK 原生日志,链接时加 -llog */
    #include <android/log.h>
    #define LOG_PLATFORM_ANDROID  1

#elif defined(__OHOS__) || defined(__HARMONYOS__)
    /* 鸿蒙 Hilog,链接 libhilog_ndk.z.so */
    #include <hilog/log.h>
    #ifndef LOG_DOMAIN
        #define LOG_DOMAIN 0          /* 鸿蒙要求的 domain,可被外部覆盖 */
    #endif
    #define LOG_PLATFORM_OHOS     1

#elif defined(_WIN32) || defined(_WIN64)
    #include <windows.h>
    #define LOG_PLATFORM_WINDOWS  1

#elif defined(__APPLE__)
    /* macOS / iOS */
    #include <pthread.h>
    #include <mach/mach.h>
    #include <mach/thread_act.h>
    #define LOG_PLATFORM_MAC      1

#elif defined(__linux__)
    #include <pthread.h>
    #include <sys/syscall.h>
    #include <unistd.h>
    #define LOG_PLATFORM_LINUX    1

#else
    /* 未知平台,回退到 POSIX */
    #include <pthread.h>
    #define LOG_PLATFORM_UNKNOWN  1
#endif

/* ============================================================================
 *  2. 获取当前线程 ID(统一返回 unsigned long long)
 * ========================================================================== */

static inline unsigned long long _log_get_tid(void)
{
#if defined(LOG_PLATFORM_LINUX) || defined(LOG_PLATFORM_ANDROID)
    /* 内核真实 tid */
    return (unsigned long long)(long)syscall(SYS_gettid);

#elif defined(LOG_PLATFORM_OHOS)
    return (unsigned long long)gettid();

#elif defined(LOG_PLATFORM_WINDOWS)
    return (unsigned long long)GetCurrentThreadId();

#elif defined(LOG_PLATFORM_MAC)
    /* macOS 用 mach thread port */
    return (unsigned long long)pthread_mach_thread_np(pthread_self());

#else
    /* 回退:pthread_t 本身 */
    return (unsigned long long)(uintptr_t)pthread_self();
#endif
}

/* ============================================================================
 *  3. 获取可读时间字符串  YYYY-MM-DD HH:MM:SS.mmm(毫秒)
 * ========================================================================== */

static inline void _log_get_time_str(char *buf, size_t size)
{
#if defined(LOG_PLATFORM_WINDOWS)
    /* Windows: GetLocalTime 直接提供毫秒字段 */
    SYSTEMTIME st;
    GetLocalTime(&st);
    snprintf(buf, size, "%04d-%02d-%02d %02d:%02d:%02d.%03d",
             st.wYear, st.wMonth, st.wDay,
             st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
#else
    /* POSIX: clock_gettime 取纳秒 -> 毫秒;localtime_r 转本地时区 */
    struct timespec ts;
    struct tm tm_info;
    clock_gettime(CLOCK_REALTIME, &ts);
    localtime_r(&ts.tv_sec, &tm_info);
    strftime(buf, size, "%Y-%m-%d %H:%M:%S", &tm_info);
    size_t len = strlen(buf);
    snprintf(buf + len, size - len, ".%03d", (int)(ts.tv_nsec / 1000000));
#endif
}

/* ============================================================================
 *  4. LOGI 宏 —— 按平台分发
 * ========================================================================== */

#if defined(LOG_PLATFORM_ANDROID)

    /*
     * Android: 用 __android_log_print 输出到 logcat。
     * tag 直接作为 logcat 的 tag,时间/tid 由 logcat 自带,这里仍补一份便于自查。
     * 链接: -llog
     */
    #define LOGI(tag, fmt, ...) \
        do { \
            char _log_t_buf[32]; \
            _log_get_time_str(_log_t_buf, sizeof(_log_t_buf)); \
            __android_log_print(ANDROID_LOG_INFO, (tag), \
                                "[%s][T:%llu] " fmt, \
                                _log_t_buf, _log_get_tid(), ##__VA_ARGS__); \
        } while (0)

#elif defined(LOG_PLATFORM_OHOS)

    /*
     * 鸿蒙: 用 OH_LOG_Print 输出到 HiLog。
     * 链接: libhilog_ndk.z.so
     */
    #define LOGI(tag, fmt, ...) \
        do { \
            char _log_t_buf[32]; \
            _log_get_time_str(_log_t_buf, sizeof(_log_t_buf)); \
            OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, (tag), \
                         "[%s][T:%llu] " fmt, \
                         _log_t_buf, _log_get_tid(), ##__VA_ARGS__); \
        } while (0)

#else

    /*
     * Linux / macOS / Windows / 其他: 输出到 stdout。
     * 如需输出到 stderr,把 printf 换成 fprintf(stderr, ...) 即可。
     */
    #define LOGI(tag, fmt, ...) \
        do { \
            char _log_t_buf[32]; \
            _log_get_time_str(_log_t_buf, sizeof(_log_t_buf)); \
            printf("[%s][T:%llu][%s] " fmt "\n", \
                   _log_t_buf, _log_get_tid(), (tag), ##__VA_ARGS__); \
        } while (0)

#endif

/* ============================================================================
 *  可选: 错误级别日志(输出到 stderr)
 * ========================================================================== */
#ifndef LOGE
#define LOGE(tag, fmt, ...) \
    do { \
        char _log_t_buf[32]; \
        _log_get_time_str(_log_t_buf, sizeof(_log_t_buf)); \
        fprintf(stderr, "[%s][T:%llu][%s][E] " fmt "\n", \
                _log_t_buf, _log_get_tid(), (tag), ##__VA_ARGS__); \
    } while (0)
#endif

#endif /* LOG_H */

main.c

/*
 * LOGI 宏使用示例
 * 编译:
 *   Linux/macOS : gcc -o demo main.c
 *   Windows     : cl main.c   或   gcc -o demo main.c
 * Android/鸿蒙: 在对应 NDK 工程中 include "log.h" 即可
 */

#include "log.h"

int main(void)
{
    /* 基本用法 */
    LOGI("MyApp", "hello world, count=%d", 1);

    /* 带字符串/浮点 */
    LOGI("Network", "connect to %s:%d, rtt=%.2fms", "127.0.0.1", 8080, 3.14);

    /* 无附加参数(验证 ##__VA_ARGS__ 兼容性) */
    LOGI("Lifecycle", "app started");

    /* 错误级别示例 */
    LOGE("DB", "query failed: %s", "timeout");

    return 0;
}
0

评论区