一个适用于嵌入式的强类型、格式化、模板化的C++打印/日志库

陪她去流浪 桃子 2024年03月21日 编辑 阅读次数:1057

作为一个长期使用 Go/C++ 语言的我来说,总是会抱怨 C++ 里面为什么就是没有 Go 语言里面那样简单明了、易用的各种库。 以致于,我经常会用 Go 的模式来写各种 C++ 库以满足日常使用。

今天所介绍的 printf/log 库就是其中之一。然而看其名字就知道,这不就是一个语句打印/日志输出库? 确实如此,但是哪个写 C++ 的人又没有造过自己的 String 类呢?没有。正是因为标准库的匮乏,所以人们总是不停地造各种自己的轮子🛞,乐此不疲。

可以直接跳转到后面查看示例

std::printfstd::iostream

首先,为什么不使用 std::iostream

  • 仅仅 #include <iostream> 就会使目标程序编译出来的目标文件增加 200KB 左右。这在嵌入式系统/单片机程序上是完全无法接受的。 要知道,我测试的平台 CH32V003 仅有 16KB 的空间(Flash),增加 200KB 的大小是绝对无法接受的。
  • 使用 << 来输出其实非常难受:包括格式设置、包括优先级考量。

其次,为什么不用 std::printf

  • 历史非常古早了,其实现方式也非常 hack。
    • 一不小心就会崩溃给你看。
  • 参数不支持类型检查,也存在隐式转换。
    • 比如把 -1 输出为 255
  • 格式化修饰符需要与参数严格对应,否则输出结果会出错。
    • 比如:%d%u%ld%lu%lld……
    • 再比如 inttypes.h 里面的一堆:PRIdN PRIdLEASTN PRIdFASTN PRIdMAX PRIdPTR PRIiN PRIiLEASTN PRIiFASTN PRIiMAX PRIiPTR 纯纯地恶心人……
    • 关键是,不同的平台(8位/16位/32位机)上,int 的大小还不一样,真的我哭死。
  • 实现很复杂/成熟,为目标体积增加不少
    • 使用 std::printf 的时候,用 objsize 查看,经常会发现,它是占用最大的函数
    • 然而在嵌入式设备上,通常不需要这么复杂/完整/成熟的实现,所以市面上出现了各种 printf 的替代实现,比如:mini_printf。

总之,用户需要时时刻刻记住格式化修饰符需要与参数的类型对应。心智负担很大。

当然,以上我只是稍微列举了其中几点我认为非常难受的地方,还有很多很多……

Go 语言是如何做的?

Go 里面的 fmt 包大大简化了 C/C++ 的格式控制字符的使用:

  • 所有的整数数值类型均可以 %d 来输出:
    • 不需要区分有符号/无符号
    • 不需要区分数据的字长:字节、单字、双字、四字
  • 所有的类型均可以用 %v 来输出:
    • 会寻找值的默认类型来输出
    • 比如整数就用 %d,字符就用 %c,字符串就用 %s
  • 可以用 %t 输出布尔值:
    • 你正在使用的 C 语言编译器里面可能没有 bool/_Bool 类型(气死你)
    • 直接输出 true/false
  • 直接用 %c 输出字符:
    • 支持任何 Unicode 码位的字符输出
    • 而在 C 里面,只支持单字节字符的输出
  • 支持所有实现 Stringer 接口的类型
    • 这就对自定义类型非常友好
    • 不只是适用于普通数据类型(POD)的输出

我的实现

我结合了上述两者的优点来实现了我自己的 printf/log 库。

虽然看起来内部实现大量运用了 C++ 的模板以及元编程,但实际上, 这个库只导出了一个函数供用户使用:

1
2
3
4
5
6
7
8
/**
 * @brief 唯一对外暴露的模板函数。
*/
template<typename... Args>
int printf(const char* fmt, Args&&... args) {
	int n = _printf_t(fmt, std::forward<Args&&>(args)...);
	return n + _outputStr(fmt);
}

剩余的看起来像 printf 的东东,几乎全部是重载(多达 30+)。

支持的类型

基础类型

  • bool
  • char, unsigned char, char32_t
  • int, unsigned int
  • int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t
  • const char*
  • 任何指针(输出地址)
  • float, double

扩展类型

  • const char* toString() 方法的结构体或类
  • std::pair 输出形如 K:V。字符串不会自动加引号,原样输出。
  • std::map 输出形如 JSON 对象。
  • std::vector 输出形如 JSON 数组。
  • std::unordered_map 输出形如 JSON 对象。

格式支持

  • 常规:

    • %% 字面值 %
    • %v 值的默认格式
  • 布尔(%v = %t):

    • %t 布尔值:true / false
  • 字符(%v = %c):

    • %c 对应的 Unicode 符号。
  • 整数(%v = %d):

    • %b 整数的二进制
    • %o 整数的八进制
    • %d 整数的十进制
    • %x 十六进制(小写)
    • %X 十六进制(大写)
    • %c Unicode 码点值
  • 浮点数:

    • %f
  • 字符串(%v = %s):

    • %s 字符串
    • %X
    • %x
  • 指针(%v = %p):

    • %p 0x 表示的十六进制,固定显示为指针长度,大写字母。
  • std::pair

    输出格式为:T1:T2

  • std::vector

    类似 JSON 的数组:[1,2,3]

  • std::map<K,V> / std::unordered_map<K,V>

    类似 JSON 的对象:{k1:v1,k2:v2}

样式支持

  • 颜色:黑色、白色、红色、绿色、黄色、蓝色、紫色、青色。
  • 字体:加粗、斜体、下划线、闪烁。

Bugs

可能是 Bugs,也可能是设计如此。

  • 不支持类似 %[0]s 的表示(根据 [] 内的数字引用第 N 个参数)。
    • 因为实现方式使用了 C++ 的模板展开,无法提前和延迟获取到顺序不一致的参数。
    • 无解,但是还好,很少被人知道/使用,并且也能非常轻易地绕过限制。
  • 多次 printf 调用之间的样式无法嵌套。否则需要递归加状态,实现意义似乎不是很大。
  • fmt 字符串不能是 std::string(参数可以),有意设计成这样。

日志库

日志库只是基于 printf 增加了一些预定义的样式,从而定义了一些形如:logInfologErrlogDebug 的函数,其使用方式与 printf 并无异。

一些示例

基础类型的输出示例:

 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
#include <stuff/base/log.hpp>

using namespace stuff::base;

int main() {
	log::printf("Literal: %%, %c\n", '%');

	// 布尔值
	log::printf("%t %t\n", true, false);
	
	// 整数
	{
		int a = 1;
		unsigned b = 2;
		int8_t c = 3;
		uint8_t d = 4;
		int16_t e = 5;
		uint16_t f = 6;
		int32_t g = 7;
		uint32_t h = 8;
		int64_t i = 9;
		uint64_t j = 10;

		log::printf("%b %b %b %b %b %b %b %b %b %b\n", a, b, c, d, e, f, g, h, i, j);
		log::printf("%o %o %o %o %o %o %o %o %o %o\n", a, b, c, d, e, f, g, h, i, j);
		log::printf("%d %d %d %d %d %d %d %d %d %d\n", a, b, c, d, e, f, g, h, i, j);
		log::printf("%x %x %x %x %x %x %x %x %x %x\n", a, b, c, d, e, f, g, h, i, j);
		log::printf("%X %X %X %X %X %X %X %X %X %X\n", a, b, c, d, e, f, g, h, i, j);
		log::printf("%o\n", '\e');
	}

	log::printf("hello stuff %s\n");

	log::printf("Unicode: %c %c %c\n", U'桃', U'🍑', U'🍌');
	log::printf("Unicode: %c\n", 25105);
	log::printf("Unicode: %c (yes, a question mark)\n", 0xFFFFFFFF);
	log::printf("%v %v\n", 'A', U'🍑');

	return 0;
}

输出如下:

Literal: %, %
true false
1 10 11 100 101 110 111 1000 1001 1010
1 2 3 4 5 6 7 10 11 12
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 a
1 2 3 4 5 6 7 8 9 A
33
hello stuff %s
Unicode: 桃 🍑 🍌
Unicode: 我
Unicode: � (yes, a question mark)
A 🍑

标准库容器类型/复杂类型作为日志输出:

 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
#include <stuff/base/log.hpp>

#include <string>
#include <vector>
#include <map>
#include <unordered_map>
#include <set>
#include <unordered_set>
#include <array>

using namespace stuff::base::log;

int main() {
	logDebug("Debug");
	logInfo("INFO", log::Underline(log::Bold("ABC")));
	logInfo("INFO: %c", 'A', log::Red(true));
	logRed("Red: %s", "str");
	logWarn("Warn: %s", "str");
	logInfo("Info: %s, %d, %c, end.",
		log::Black("111"),
		log::Yellow(1),
		log::Black('A'),
		log::Bold(log::Underline(true))
	);
	logErr("Error: %v end", log::Blink(log::Green(true)));
	
	std::string s("ABC");
	logWarn("Str: %s", log::Italic(s));
	
	std::vector arr{std::vector{1,2,3}, std::vector{4,5,6}};
	logFatal("Array: %v %v", arr, std::vector<int>{});
	
	std::map<int, const char*> m;
	m[0] = "X000";
	m[1] = "X111";
	logFatal("Map: %v", m);
	
	std::map<const char*, std::vector<int>> si;
	si["A"] = {1,2,3};
	si["B"] = {4,5,6};
	logFatal("Map+Vector: %v", si);
	
	std::pair<int, int> p = {1, 2};
	logFatal("Pair: %v", p);
	
	std::unordered_map<int, int> um;
	um[0] = 1;
	logCyan("unordered_map: %v", um);
	
	std::set<int> set1{1,2,3,4,5,6};
	logPurple("set: %v", set1);
	
	std::array<int,3> a1{1,2,3};
	logYellow("array: %v, %v", a1, &a1);
}

看起来很复杂,实则一点也不简单:

Debug
INFOABC
INFO: Atrue
Red: str
Warn: str
Info: 111, 1, A, end.true
Error: true end
Str: ABC
Array: [[1,2,3],[4,5,6]] []
Map: {0:X000,1:X111}
Map+Vector: {A:[1,2,3],B:[4,5,6]}
Pair: 1:2
unordered_map: {0:1}
set: [1,2,3,4,5,6]
array: [1,2,3], 0x000000016F182C68

注:以上 HTML 由 Aes2Htm 命令从终端输出生成:./main | ./aes2htm --html > log.html

结语

目前几个月下来的使用体验远比 std::printf 来得舒服。

但是这个库并没有独立出来,而是作为我的嵌入式基础代码玩意儿库的一部分存在于 libstuff/base/log at main · movsb/libstuff 中。 可以一同享用。🤪🤪🤪

标签:C++ · 嵌入式 · 日志