1. 思路

前几天我在编写游戏的demo框架时, 需要使用一个输出日志的函数. 如果使用 std::cout 会因为缓冲区刷新不及时导致报错信息输出不及时. 但是为了实现调试时输出日志信息引入spdlog库似乎有点多余, 我并没有时间再学习一个新的库使用. 因此决定自己实现一个简易的日志输出函数.

首先我的思路是使用一个枚举常量来决定日志的类型并决定输出的消息头部:

1
2
3
4
5
6
typedef enum {
MSG_INFO,
MSG_DEBUG,
MSG_WARN,
MSG_ERROR
} msg_type;

然后就是报错信息, 报错信息含有报错信息的提示以及抛出错误的内容, 例如SDL_GetError(), 或者打印其他的变量值, 如果使用 void 指针传入, 就解决不了参数数量不确定的问题, 而且由于 printf 函数不能直接输出 std::string , 又要经过复杂的转换, 最后就只能打算采用可变参数实现.

2. 关于可变参数

常见的如 printf, scanf, 不常见的如sprintf(和printf差不多, 但是输出位置是一个给定的char*而不是stdout),

这些函数都支持可变参数. 在标准库头文件 stdarg.h 中就包含处理可变参数的宏

1
2
3
4
5
6
7
8
va_list;                                     // 可变参数的列表(类似一种数据类型)

void va_start(va_list args, last_named_arg); // 用于获取可变参数列表的首指针
// / \
// 可变参数列表 最后一个确定的参数
type va_arg(va_list args, type);
void va_end(va_list args); // 用于结束可变参数列表的访问
void va_copy(va_list args1, va_list args2); // 用于拷贝可变参数列表

确定函数的声明:

1
void ulog(msg_type type, const char* format, ...);

基于上述资料, 写出函数的大致实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void ulog(msg_type type, const char* format, ...) {
char* msg_content = NULL;
va_list arg_list;
va_start(arg_list, format);

// 计算所需的内存空间大小
int len = vsnprintf(NULL, 0, format, arg_list);
if (len < 0) {
printf("Logger memery calculate error\n");
return;
}

// 分配内存空间
msg_content = (char*)malloc(len + 1);
if (msg_content == NULL) {
printf("Logger memery alloc failure\n");
return;
}

// 使用 vsnprintf 填充 msg_content
va_copy(arg_list, arg_list_raw);
vsnprintf(msg_content, len + 1, format, arg_list);

va_end(arg_list_raw);
va_end(arg_list);

switch(type) {
case MSG_INFO:
printf("[INFO] %s\n", msg_content);
break;
case MSG_DEBUG:
printf("[DEBUG]: %s\n", msg_content);
break;
case MSG_WARN:
printf("[WARN]: %s\n", msg_content);
break;
case MSG_ERROR:
printf("[ERROR]: %s\n", msg_content);
break;
default:
printf("[ERROR_LOG]: %s\n", msg_content);
}

free(msg_content);
}

3. 特殊改动

上述第一版的函数并不能正常的运行, 我在第一次调试的时候就出现结果异常和严重的段错误:

1
2
3
4
5
6
7
8
9
10
11
12
// 调试代码:
ulog(MSG_INFO, "114514%s %d %lf", "哼, 哼, 啊啊啊啊啊!!!", 1919810, (double)3.1415926);
ulog(MSG_WARN, "114514%s %d %lf", "哼, 哼, 啊啊啊啊啊!!!", 1919810, (double)3.1415926);
ulog(MSG_DEBUG, "114514%s %d %lf", "哼, 哼, 啊啊啊啊啊!!!", 1919810, (double)3.1415926);
ulog(MSG_ERROR, "114514%s %d %lf", "哼, 哼, 啊啊啊啊啊!!!", 1919810, (double)3.1415926);
int a = 114514;
int b = 1919810;
const char* str = "jsjkjdask";
ulog(MSG_INFO, "djsajdskla");
ulog(MSG_WARN, "%d + %d = %d", a, b, a + b);
ulog(MSG_DEBUG,"%s", str);
ulog(MSG_ERROR,"28918028");

输出错误

这样的错误使用gdb也难以进行排查, 因为 va_list 是一个封装的相当好的结构体, 在查阅大量资料之后, 我得知 va_list引用完就会抛弃的特性, 即引用一个变量就清除一个.

我发现在第一次使用 vsnprintf 函数计算字符串长度时已经使用了 arg_list , 得益于库的特性, arg_list被清空了 , 导致后面访问的全部是非法内存, 结果可想而知.

经过改进, 最终的函数长这样, 里面出现反复拷贝 arg_list 的行为, 防止计算字符串长度的时候丢失参数列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void ulog(msg_type type, const char* format, ...) {
char* msg_content = NULL;

va_list arg_list_raw;
va_start(arg_list_raw, format);
va_list arg_list;
// 拷贝一份arg_list
va_copy(arg_list, arg_list_raw);

// 计算所需的内存空间大小
int len = vsnprintf(NULL, 0, format, arg_list);
if (len < 0) {
printf("Logger memery calculate error\n");
return;
}

// 分配内存空间
msg_content = (char*)malloc(len + 1);
if (msg_content == NULL) {
printf("Logger memery alloc failure\n");
return;
}

// 将原始的arg_list填充回去
va_copy(arg_list, arg_list_raw);
// 使用 vsnprintf 填充 msg_content
vsnprintf(msg_content, len + 1, format, arg_list);

va_end(arg_list_raw);
va_end(arg_list);

switch(type) {
case MSG_INFO:
printf("[INFO] %s\n", msg_content);
break;
case MSG_DEBUG:
printf("[DEBUG]: %s\n", msg_content);
break;
case MSG_WARN:
printf("[WARN]: %s\n", msg_content);
break;
case MSG_ERROR:
printf("[ERROR]: %s\n", msg_content);
break;
default:
printf("[ERROR_LOG]: %s\n", msg_content);
}

// 释放内存
free(msg_content);
}

现在能够正常的输出日志了:

成功输出日志

4.颜色输出

虽说已经能够正常的输出日志信息, 但是总感觉差点意思, 现在就是为日志输出函数加上颜色输出了.

这个日志输出函数底层还是调用了printg函数, 因此可以直接使用几个宏定义为输出加上颜色(记得输出完之后需要把颜色恢复成NONE, 不然会一直保持上一个颜色)

这是最终的函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void ulog(msg_type type, const char* format, ...) {
#define COLOR_NONE "\033[0m"
#define RED "\033[1;31m"
#define BLUE "\033[1;34m"
#define GREEN "\033[1;32m"
#define YELLOW "\033[1;33m"

char* msg_content = NULL;
va_list arg_list_raw;
va_start(arg_list_raw, format);
va_list arg_list;
va_copy(arg_list, arg_list_raw);

// 计算所需的内存空间大小
int len = vsnprintf(NULL, 0, format, arg_list);
if (len < 0) {
printf("Logger memery calculate error\n");
return;
}

// 分配内存空间
msg_content = (char*)malloc(len + 1);
if (msg_content == NULL) {
printf("Logger memery alloc failure\n");
return;
}

// 使用 vsnprintf 填充 msg_content
va_copy(arg_list, arg_list_raw);
vsnprintf(msg_content, len + 1, format, arg_list);

va_end(arg_list_raw);
va_end(arg_list);

switch(type) {
case MSG_INFO:
printf(GREEN "[INFO] %s\n" COLOR_NONE, msg_content);
break;
case MSG_DEBUG:
printf(BLUE "[DEBUG]: %s\n" COLOR_NONE, msg_content);
break;
case MSG_WARN:
printf(YELLOW "[WARN]: %s\n" COLOR_NONE, msg_content);
break;
case MSG_ERROR:
printf(RED "[ERROR]: %s\n" COLOR_NONE, msg_content);
break;
default:
printf("[ERROR_LOG]: %s\n", msg_content);
}

// 释放内存
free(msg_content);

#undef COLOR_NONE
#undef RED
#undef BLUE
#undef GREEN
#undef YELLOW
}

这是最后测试代码输出的结果

最终结果