跨平台日志宏 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)
- Android →
- 可变参数兼容:统一使用
##__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中提供LOGI与LOGE;如需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 库:CMake 加 target_link_libraries(xxx log);ndk-build 加 LOCAL_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,避免不同平台类型差异。
时间获取(毫秒)
- Windows:
GetLocalTime()直接返回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 分支的 LOGI 把 printf(...) 换成 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;
}
评论区