前言
用 C 语言写程序,现在依然能创造一切,但一切都已经存在了,你无需去创造。刀枪入库,马放南山吧。
不过,如果你还对过去的田园和疆场有所怀恋,无聊的时候,还是可以写写 C 程序。这时,你不是老板,不是员工,甚至也不是学生,而是假期里的一个少年,骑着单车四处游荡。这种心态,很适合写 C 程序。早期的程序员,就是这样写 C 程序的,于是就有了最早的 Unix,后来又有了最早的 Linux。
第一个开始抱怨 C 缺这少那的人,他就不再是少年,也没有假期了,所有对 C 的抱怨,皆因如此。没有人抱怨乐高积木会耽误他们盖摩天大楼,也没有人抱怨自行车会拖慢人类的前进速度,任何对 C 有所抱怨的人,实际上是抱怨自己,没有假期,不再是少年,家里的自行车已被灰尘掩盖。
有个叫朱志文的男人,骑着单车,遍历了整个地球,一大把年纪了,现在依然如此。他实际上挺适合写 C 程序的,而对 C 骂骂咧咧的人,大都以为随便就能买张高铁票就能到自己想去的地方,然而很有可能,他们连自家附近的小山都没爬过。
本文会带着你爬一座叫作 libyaml 的 C 小山,或小 C 山。你要有假期,你要是少年,你要去做一些看上去没用的事。
准备
写 C 程序,最好用 Linux 环境。Windows 和 macOS 早就不是少年,也没假期了。
由于 Ubuntu 用户众多,我用的是它的一个衍生版,Linux Mint。以前我是用 Gentoo 的,可能过段时间我会用回来,像个有假期的少年……由于 Ubuntu 用户众多,安装一个可以爬的 libyaml,只需用以下命令:
$ sudo apt install libyaml-dev
libyaml-dev 包安装完毕后,可用过以下程序验证 libyaml 库是否可用。
/* foo.c */
#include
#include
int main() {
printf("libyaml version: %s\n", yaml_get_version_string());
return 0;
}
假设上述代码保存于 foo.c 文件,使用以下命令编译它,并运行所得程序,结果应该输出 libyaml 的版本号。
$ gcc foo.c -o foo -lyaml
$ ./foo
libyaml version: 0.2.5
还需要做的准备是,你需要懂一些 YAML,因为我们要做的事情是,将一些 C 数据对象输出为 YAML 格式。如果你不懂 YAML,可从「YAML 小传」了解一二。
空流
假设我要向一个名为 foo.yaml 的文件写入一些 YAML 格式的数据,那么首先我应该以写模式打开或创建这个文件:
FILE *yaml_file = fopen("foo.yaml", "w");
if (!yaml_file) {
fprintf(stderr, "failed to open foo.yaml");
exit(EXIT_FAILURE);
}
如果每次打开文件都要写这样的代码,可以表现出自己非常严谨,直到有一天,你厌倦了这种严谨,就干脆定义一个宏吧。
#define YAH_OPEN_FILE(file, name, mode) do { \
file = fopen(name, mode); \
if (!file) { \
fprintf(stderr, "failed to open %s", name); \
exit(EXIT_FAILURE); \
} \
} while (0)
宏的前缀 YAH
表示 youth and holidays. 现在,以写模式打开或创建 yaml_file
的过程可写为
FILE *yaml_file;
YAH_OPEN_FILE(yaml_file, "foo.yaml", "w");
以上为爬山前的热身运动。现在开始着手将什么也没有写入 foo.yaml 文件。什么也没有,就是空。
libyaml 为 C 数据对象所实现的 YAML 格式输出机制,采用了事件模型。无论你要输出什么,都需要准备一个事件,然后发送。发送给谁呢?发送给一个状态机,类型为 yaml_emitter_t
,发送事件之前,需要几行代码初始化这个状态机。
yaml_emitter_t emitter;
if (!yaml_emitter_initialize(&emitter)) {
fprintf(stderr, "failed to initialize yaml emitter!\n");
exit(EXIT_FAILURE);
}
接下来,将 emitter
与 yaml_file
关联起来,即
yaml_emitter_set_output_file(&emitter, yaml_file);
我们也为上述代码做一下封装,但这次是用宏。
#define YAH_YAML_EMITTER_INIT(emitter, file) do { \
if (!yaml_emitter_initialize(emitter)) { \
fprintf(stderr, "failed to initialize yaml emitter!\n"); \
exit(EXIT_FAILURE); \
} \
yaml_emitter_set_output_file(&emitter, file); \
} while (0)
于是 yaml_emitter_t
状态机定义及其初始化过程可写为
yaml_emitter_t emitter;
YAH_YAML_EMITTER_INIT(&emitter, yaml_file);
要向状态机 emitter
发送事件,前提是我们需要定义一个事件:
yaml_event_t event;
若向 emitter
发送事件,需要结合我们的期望,构造事件,然后发送。例如,我们期望向 foo.yaml 文件写入 YAML 流的开始——当然实际上什么也不会写入,只需
if (!yaml_stream_start_event_initialize(&event, YAML_UTF8_ENCODING)) {
fprintf(stderr, "failed to initialize *stream_start* event.\n");
exit(EXIT_FAILURE);
}
if (!yaml_emitter_emit(&emitter, &event)) {
fprintf(stderr, "failed to emit %d because %s\n", event.type, emitter.problem);
exit(EXIT_FAILURE);
}
上述代码构造的 event
是 stream_start
,即流开始事件。不过,我们又遇到了严谨性代码,应该怎么处理呢,宏之!
#define YAH_YAML_EMIT(emitter, event) do { \
if (!yaml_emitter_emit(emitter, event)) { \
fprintf(stderr, \
"failed to emit event %d because %s.\n", \
(event)->type, \
(emitter)->problem); \
exit(EXIT_FAILURE); \
} \
} while (0)
#define YAH_YAML_START(emitter, event, encoding) do { \
if (!yaml_stream_start_event_initialize(event, encoding)) { \
fprintf(stderr, "failed to initialize *stream_start* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
现在,你已经是 C 语言宏定义大师了。当你在调用 YAH_YAML_EMIT
这个宏的时候,大概会有谈笑间,樯橹灰飞烟灭的感觉。这需要感谢 libyaml 作者喜欢为函数取很长的名字。
YAH_YAML_START(&emitter, &event, YAML_UTF8_ENCODING);
上述代码中的 YAML_UTF8_ENCODING
便是 YAH_YAML_EMIT
宏的可变参数。
流开始事件是包含着 YAML 文档编码信息的。YAML_UTF8_ENCODING
表示,向 YAML 文件写入的数据,其编码为 UTF-8。你也可以用其他编码,但 libyaml 提供的支持并不多,除了 UTF-8,就是 UTF-16,在这个时代,你大概不会用后者吧。如果你真的需要 UTF-16,大根端是 YAML_UTF16BE_ENCODING
,小根端是 YAML_UTF16LE_ENCODING
。
有始有终。只要我们构造 stream_end
事件并发送给 emitter
,便意味着向 YAML 文件写入数据的过程彻底结束。
if (!yaml_stream_end_event_initialize(&event)) {
fprintf(stderr, "failed to initialize *stream_end* event.\n");
exit(EXIT_FAILURE);
}
if (!yaml_emitter_emit(&emitter, &event)) {
fprintf(stderr, "failed to emit %d because %s\n", event.type, emitter.problem);
exit(EXIT_FAILURE);
}
又遇到了严谨性代码,继续宏之。
#define YAH_YAML_END(emitter, event) do { \
if (!yaml_stream_end_event_initialize(event)) { \
fprintf(stderr, "failed to initialize *stream_end* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
然后,stream_end
事件的构造和发送过程可简写为
YAH_YAML_END(&emitter, &event);
既然数据写入过程结束了,那么 event
和 emitter
也就没有必要存在了,虽然它们不是指针类型,但他们是结构体对象,成员变量会有一些指针,在它们初始化的时候,会被 libyaml 分配空间。此外,也应当记得关闭 yaml_file
。
yaml_event_delete(&event);
yaml_emitter_delete(&emitter);
fclose(yaml_file);
现在,我们将上述过程汇总为 yah.h 和 foo.c 文件。yah-yaml.h 存放上述所有宏定义,而 foo.c 实现向 YAML 文件写入空的过程。
yah-yaml.h 文件内容如下:
#ifndef YAH_YAML_H
#define YAH_YAML_H
#include
#define YAH_OPEN_FILE(file, name, mode) do { \
file = fopen(name, mode); \
if (!file) { \
fprintf(stderr, "failed to open %s", name); \
exit(EXIT_FAILURE); \
} \
} while (0)
#define YAH_YAML_EMITTER_INIT(emitter, file) do { \
if (!yaml_emitter_initialize(emitter)) { \
fprintf(stderr, "failed to initialize yaml emitter!\n"); \
exit(EXIT_FAILURE); \
} \
yaml_emitter_set_output_file(emitter, file); \
} while (0)
#define YAH_YAML_EMIT(emitter, event) do { \
if (!yaml_emitter_emit(emitter, event)) { \
fprintf(stderr, \
"failed to emit event %d because %s.\n", \
(event)->type, \
(emitter)->problem); \
exit(EXIT_FAILURE); \
} \
} while (0)
#define YAH_YAML_START(emitter, event, encoding) do { \
if (!yaml_stream_start_event_initialize(event, encoding)) { \
fprintf(stderr, "failed to initialize *stream_start* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
#define YAH_YAML_END(emitter, event) do { \
if (!yaml_stream_end_event_initialize(event)) { \
fprintf(stderr, "failed to initialize *stream_end* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
#define YAH_YAML_DOC_START(emitter, event, name, ...) do { \
if (!yaml_##name##_start_event_initialize(event, __VA_ARGS__)) { \
fprintf(stderr, "failed to initialize *%s_start* event.\n", #name); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
#define YAH_YAML_SCALAR(emitter, event, string, type) do { \
if (!yaml_scalar_event_initialize(event, \
NULL, \
NULL, \
string, \
strlen(string), \
1, \
1, \
type)) { \
fprintf(stderr, "failed to initialize *scalar* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
#endif
foo.c 的内容如下:
#include
int main(void) {
FILE *yaml_file;
yaml_emitter_t emitter;
yaml_event_t event;
YAH_OPEN_FILE(yaml_file, "foo.yaml", "w");
YAH_YAML_EMITTER_INIT(&emitter, yaml_file);
YAH_YAML_START(&emitter, &event, YAML_UTF8_ENCODING);
YAH_YAML_END(&emitter, &event);
yaml_event_delete(&event);
yaml_emitter_delete(&emitter);
fclose(yaml_file);
return EXIT_SUCCESS;
}
编译 foo.c 并执行所得程序,会在当前目录下生成 foo.yaml 文件,但其内容为空。在编译的时候,注意要用 -I.
,不然 gcc 找不到 yah-yaml.h。
$ gcc -I. foo.c -o foo -lyaml
$ ./foo
上述代码大概 50 行,可见向 YAML 文件里写入空,并不是什么都不做,我们几乎用了 C 语言的全部知识……好在,main
函数里的代码,逻辑上基本是清晰的,感谢宏!
文档
一个 YAML 流,可包含多个文档。每个文档以 ---
开头,以 ...
结束。虽然 YAML 语言并不严格要求文档必须有起止标记,但是在用 libyaml 的时候,必须要有输出文档起止标记的过程,只是你可以控制文档起止标记显示与否。
文档开始标记的输出过程,我们现在可以直接用宏来表达,如下:
#define YAH_YAML_DOC_START(emitter, event, implicit) do { \
if (!yaml_document_start_event_initialize(event, \
NULL, \
NULL, \
NULL, \
implicit)) { \
fprintf(stderr, "failed to initialize *document_start* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
事情开始变得复杂了起来。只是向 YAML 文件输出个开始标记而已,但是用于构造该事件的函数 yaml_document_start_event_initialize
,这名字又臭又长也就罢了,参数还这么多。这个函数的声明如下:
YAML_DECLARE(int)
yaml_document_start_event_initialize(yaml_event_t *event,
yaml_version_directive_t *version_directive,
yaml_tag_directive_t *tag_directives_start,
yaml_tag_directive_t *tag_directives_end,
int implicit);
第一个参数 event
表达事件,不必多言。第二个参数用于存储要输出的 YAML 版本号,通常没必要,可设为 NULL
。第三和第四个参数,用于设定输出到 YAML 文件首部的标签,通常没必要,可都设为 NULL
。第五个参数,亦即最后一个,用于设定是否隐藏 YAML 文档开始标记,1 表示隐藏,0 表示显示。
YAH_YAML_DOC_START
宏假设你不需要输出 YAML 版本号,也不需要在 YAML 文件首部设置标签,故而第二至四个参数皆为 NULL
,只保留文档开始标记隐藏与否的参数,交由宏调用者确定。这个宏的用法如下:
YAH_YAML_DOC_START(&emitter, &event, 1); /* 隐藏文档开始标记 */
文档结束标记的输出事件,要简单一些,如下:
#define YAH_YAML_DOC_END(emitter, event, implicit) do { \
if (!yaml_document_end_event_initialize(event, implicit)) { \
fprintf(stderr, "failed to initialize *document_end* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
YAH_YAML_DOC_END
的用法如下:
YAH_YAML_DOC_END(&emitter, &event, 1);
将上述宏定义放在 yah-yaml.h 中,现在它们还没有用处,因为 libyaml 不允许输出没有任何内容的 YAML 文档。
标量
YAML 语言里的标量,可对应 C 语言中的字符串常量或对象。以下代码,可将 C 字符串输出为 YAML 普通标量。
const char *hello = "Hello world!";
if (!yaml_scalar_event_initialize(&event,
NULL, /* 锚点 */
NULL, /* 标签 */
(const yaml_char *)hello, /* C 字符串,需强制转换为 libyaml 字符串类型 */
strlen(hello), /* C 字符串长度 */
1, /* 为普通标量设定是否禁止标签显示,1 表示禁止,0 表示不禁止 */
1, /* 为带引号的标量设定是否禁止标签显示,1 表示禁止,0 表示不禁止 */
YAML_PLAIN_SCALAR_STYLE /* 普通标量风格 */)) {
fprintf(stderr, "failed to initialize *scalar* event.\n");
exit(EXIT_FAILURE);
}
if (!yaml_emitter_emit(&emitter, &event)) {
fprintf(stderr,
"failed to emit event %d because %s.\n",
event.type,
emitter.problem);
exit(EXIT_FAILURE);
}
yaml_scalar_event_initialize
函数有 8 个参数,在上述代码里,我已给出了一些注释,但是如果你对 YAML 语言不熟悉,可能会觉得很烦躁。我的建议是,去他的锚点和标签……不用它。如果你愿意接受我的建议,那么我们就可以愉快地定义一个宏,收编上述代码。
#define YAH_YAML_SCALAR(emitter, event, string, style) do { \
if (!yaml_scalar_event_initialize(event, \
NULL, \
NULL, \
(const yaml_char *)(string), \
strlen(string), \
1, \
1, \
style)) { \
fprintf(stderr, "failed to initialize *scalar* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
现在,可以用 YAH_YAML_SCALAR
将 "Hello world!"
输出为 YAML 普通标量,如下:
YAH_YAML_SCALAR(&emitter, &event, "Hello world!", YAML_PLAIN_SCALAR_STYLE);
如果你想输出带引号的标量,例如单引号标量,只需
YAH_YAML_SCALAR(&emitter, &event, "Hello world!", YAML_SINGLE_QUOTED_SCALAR_STYLE);
对于双引号标量,只需
YAH_YAML_SCALAR(&emitter, &event, "Hello world!", YAML_DOUBLE_QUOTED_SCALAR_STYLE);
将上述宏定义放在 yah-yaml.h 中,然后将 foo.c 修改为
#include
int main(void) {
FILE *yaml_file;
yaml_emitter_t emitter;
yaml_event_t event;
YAH_OPEN_FILE(yaml_file, "foo.yaml", "w");
YAH_YAML_EMITTER_INIT(&emitter, yaml_file);
YAH_YAML_START(&emitter, &event, YAML_UTF8_ENCODING);
/* 输出只有一个标量的 YAML 文档 */
YAH_YAML_DOC_START(&emitter, &event, 1);
YAH_YAML_SCALAR(&emitter,
&event,
yah_yaml_scalar("Hello world!"),
YAML_PLAIN_SCALAR_STYLE);
YAH_YAML_DOC_END(&emitter, &event, 1);
YAH_YAML_END(&emitter, &event);
yaml_event_delete(&event);
yaml_emitter_delete(&emitter);
fclose(yaml_file);
return EXIT_SUCCESS;
}
上述程序编译并执行后, foo.yaml 文件会有以下内容:
Hello world!
如果将 YAH_YAML_DOC_START
和 YAH_YAML_DOC_END
的第 2 个参数设为 0,则 foo.yaml 的内容会变为
--- Hello world!
...
它虽然合法,但并非我们想要的。我们想要的是
---
Hello world!
...
然而,libyaml 对此却无能为力,除非你是它的维护者。不过,我们几乎不会只向 YAML 文件输出标量,故而 libyaml 的这个 bug 或 feature 无伤大雅。
如果确实需要,我们可以在禁止 libyaml 显示 YAML 文档起始标记的情况下,自行向 YAML 文件写入 ---
和 ...
,例如
#include
int main(void) {
FILE *yaml_file;
YAH_OPEN_FILE(yaml_file, "foo.yaml", "w");
fprintf(yaml_file, "---\n"); /* 写入 YAML 文档起始标记 */
yaml_emitter_t emitter;
yaml_event_t event;
YAH_YAML_EMITTER_INIT(&emitter, yaml_file);
YAH_YAML_START(&emitter, &event, YAML_UTF8_ENCODING);
/* 输出只有一个标量的 YAML 文档 */
YAH_YAML_DOC_START(&emitter, &event, 1);
YAH_YAML_SCALAR(&emitter,
&event,
yah_yaml_scalar("Hello world!"),
YAML_PLAIN_SCALAR_STYLE);
YAH_YAML_DOC_END(&emitter, &event, 1);
YAH_YAML_END(&emitter, &event);
yaml_event_delete(&event);
yaml_emitter_delete(&emitter);
fprintf(yaml_file, "...\n"); /* 写入 YAML 文档结束标记 */
fclose(yaml_file);
return EXIT_SUCCESS;
}
序列
想向 YAML 文件中输出多个标量,用一个循环,重复调用 YAH_YAML_SCALAR
宏是不行的,例如,你可以向 foo.yaml 文件写入以下内容
a
b
c
但它不是三个标量,而是一个,即 a b c
。因为 YAML 解析器会将其视为一个含有换行符的普通标量,然后解析器会将每个换行符替换为 1 个空格。
在 YAML 文件中,表达多个标量,只能用序列。例如
- a
- b
- c
用 libyaml 输出序列的过程,我们可以用两个宏来表达。
#define YAH_YAML_SEQ_START(emitter, event, style) do { \
if (!yaml_sequence_start_event_initialize(event, \
NULL, \
NULL, \
1, \
style)) { \
fprintf(stderr, "failed to initialize *sequence_start* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
#define YAH_YAML_SEQ_END(emitter, event) do { \
if (!yaml_sequence_end_event_initialize(event)) { \
fprintf(stderr, "failed to initialize *sequence_end* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
yaml_sequence_start_event_initialize
的第二、三个参数分别是锚点和标签,我们可以用 NULL
忽略它们,第四个参数表示隐藏还是显示标签,1 表示禁止,0 表示显示,由于我们不关心标签,故而将其设为 1。第五个参数,也就是最后一个参数,将其作为 YAH_YAML_SEQ_START
的参数,由用户设定,该参数用于设定序列的风格是块风格(block style)还是流风格(flow style)。
以下代码可以输出块风格的序列:
const char *seq[] = {"a", "b", "c"};
size_t n = sizeof(seq) / sizeof(const char *);
YAH_YAML_SEQ_START(&emitter, &event, YAML_BLOCK_SEQUENCE_STYLE);
for (size_t i = 0; i < n; i++) {
YAH_YAML_SCALAR(&emitter,
&event,
seq[i],
YAML_PLAIN_SCALAR_STYLE);
}
YAH_YAML_SEQ_END(&emitter, &event);
输出结果应当为
- a
- b
- c
若将 YAH_YAML_SEQ_START
的第三个参数更换为 YAML_FLOW_SEQUENCE_STYLE
,便可输出流风格的序列,即
[a, b, c]
映射
映射事件的构造和发送,与序列完全一致,将上一节定义的两个宏,更换一下名字即可实现该过程。
#define YAH_YAML_MAP_START(emitter, event, style) do { \
if (!yaml_mapping_start_event_initialize(event, \
NULL, \
NULL, \
1, \
style)) { \
fprintf(stderr, "failed to initialize *mapping_start* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
#define YAH_YAML_MAP_END(emitter, event) do { \
if (!yaml_mapping_end_event_initialize(event)) { \
fprintf(stderr, "failed to initialize *mapping_end* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
如果我们想将一个变量的名字和值以 YAML 格式输出,便可以用映射。例如
/* 要保存的数据 */
int truth = 42;
/* 构造键和值 */
const char *key = "truth";
char value[255];
snprintf(value, sizeof(value), "%d", truth);
/* 输出只含有一个键值对的映射 */
YAH_YAML_MAP_START(&emitter, &event, YAML_BLOCK_MAPPING_STYLE);
YAH_YAML_SCALAR(&emitter, &event, key, YAML_PLAIN_SCALAR_STYLE);
YAH_YAML_SCALAR(&emitter, &event, value, YAML_PLAIN_SCALAR_STYLE);
YAH_YAML_MAP_END(&emitter, &event);
上述代码可输出以下内容:
truth: 42
如果你将上述代码中的映射风格由块风格 YAML_BLOCK_MAPPING_STYLE
换位流风格 YAML_FLOW_MAPPING_STYLE
,则输出内容会变为
{truth: 42}
我知道,你也许已经有些不耐烦了,大概会想,为何不用 printf
或 fprintf
直接向输出上述内容呢?简单的数据可以如此,但是对于复杂一些的数据,例如多层嵌套的结构体,要输出为 YAML 嵌套映射格式,也许你会不想自己去处理块风格中的缩进。不过,要输出这样的结构体,即使我们用宏简化了很多代码,但代码量依然是非常可观,以致于我根本不想举例说明。对于映射,我们还需要再想一些简化方法。
键的输出过程,可以简化为宏:
#define YAH_YAML_KEY(name, style) do { \
const char *key = #name; \
YAH_YAML_SCALAR(&emitter, &event, key, YAML_##style##_SCALAR_STYLE); \
} while (0)
上述宏定义,使用了 C 语言宏的 ##
和 #
语法。符号 ##
可将宏参数与相邻的文本连接起来,例如若 style
的值为 PLAIN
,则 YAML_##style
会被展开为 YAML_PLAIN
。符号 #
可为其后的宏参数添加引号,例如若 name
的值为 true
,则 #true
会被展开为 "truth"
。
YAH_YAML_KEY
的用法如下:
int truth = 42;
char value[255];
snprintf(value, sizeof(value), "%d", truth);
YAH_YAML_MAP_START(&emitter, &event, YAML_BLOCK_MAPPING_STYLE);
YAH_YAML_KEY(truth, PLAIN); /* 输出键 truth */
YAH_YAML_SCALAR(&emitter, &event, value, YAML_PLAIN_SCALAR_STYLE); /* 输出值 */
YAH_YAML_MAP_END(&emitter, &event);
至于键值对里的值,其 YAML 格式输出过程则很难简化,因为输出形式可能是标量,也可能是序列,还可能是映射。如果我们暂且只考虑值为标量的情况。若值在 C 程序中为数字,则可定义一个宏,将其转换为字符串,然后输出为 YAML 标量。
/* 将整型数输出为 YAML 标量 */
/* 整形数转换成的文本,最多只能是 1024 个字节 */
#define YAH_YAML_INT(interger, style) do { \
char value[1024]; \
snprintf(value, sizeof(value), "%d", interger); \
YAH_YAML_SCALAR(&emitter, &event, value, YAML_##style##_SCALAR_STYLE); \
} while (0)
/* 将浮点数(实数)输出为 YAML 标量 */
/* 浮点数转换成的文本,最多只能是 1024 个字节 */
#define YAH_YAML_REAL(real, style) do { \
char value[1024]; \
snprintf(value, sizeof(value), "%f", real); \
YAH_YAML_SCALAR(&emitter, &event, value, YAML_##style##_SCALAR_STYLE); \
} while (0)
/* 将字符串输出为 YAML 标量 */
#define YAH_YAML_STR(string, style) do { \
YAH_YAML_SCALAR(&emitter, &event, string, YAML_##style##_SCALAR_STYLE); \
} while (0)
通过 YAH_YAML_INT
宏,可以将上述的 truth
变量的值输出为 YAML 格式的过程简化为
int truth = 42;
YAH_YAML_MAP_START(&emitter, &event, YAML_BLOCK_MAPPING_STYLE);
YAH_YAML_KEY(truth, PLAIN); /* 输出键 truth */
YAH_YAML_INT(truth, PLAIN); /* 输出值 42 */
YAH_YAML_MAP_END(&emitter, &event);
现在,可以尝试将 C 结构体输出为 YAML 块格式。例如
struct foo {
int a;
double b;
const char *c;
} x = {42, 3.1415926, "Hello world!"};
/* 输出 x:第一层映射 */
YAH_YAML_MAP_START(&emitter, &event, YAML_BLOCK_MAPPING_STYLE);
YAH_YAML_KEY(x, PLAIN); /* 输出键 x */
/* 输出 x 的成员:第二层映射 */
YAH_YAML_MAP_START(&emitter, &event, YAML_BLOCK_MAPPING_STYLE);
/* 输出 x.a */
YAH_YAML_KEY(a, PLAIN); YAH_YAML_INT(x.a, PLAIN);
/* 输出 x.b */
YAH_YAML_KEY(b, PLAIN); YAH_YAML_REAL(x.b, PLAIN);
/* 输出 x.c */
YAH_YAML_KEY(c, PLAIN); YAH_YAML_STR(x.c, PLAIN);
/* 第二层映射结束 */
YAH_YAML_MAP_END(&emitter, &event);
/* 第一层映射结束 */
YAH_YAML_MAP_END(&emitter, &event);
上述代码的输出结果为块风格的嵌套映射:
x:
a: 42
b: 3.141593
c: Hello world!
若将上述代码中的 YAML_BLOCK_MAPPING_STYLE
都替换为 YAML_FLOW_MAPPING_STYLE
,便可输出流风格的嵌套映射:
{x: {a: 42, b: 3.141593, c: Hello world!}}
将上述宏定义都加入到 yah-yaml.h 里,然后,我可以给出一个完整的 foo.c,它实现了上述结构体的输出。
#include
int main(void) {
FILE *yaml_file;
yaml_emitter_t emitter;
yaml_event_t event;
YAH_OPEN_FILE(yaml_file, "foo.yaml", "w")
YAH_YAML_EMITTER_INIT(&emitter, yaml_file);
YAH_YAML_START(&emitter, &event, YAML_UTF8_ENCODING);
YAH_YAML_DOC_START(&emitter, &event, 0);
struct foo {
int a;
double b;
const char *c;
} x = {42, 3.1415926, "Hello world!"};
/* 输出 x */
YAH_YAML_MAP_START(&emitter, &event, YAML_BLOCK_MAPPING_STYLE);
YAH_YAML_KEY(x, PLAIN); /* 输出键 x */
/* 输出 x 的成员 */
YAH_YAML_MAP_START(&emitter, &event, YAML_BLOCK_MAPPING_STYLE);
/* 输出 x.a */
YAH_YAML_KEY(a, PLAIN); YAH_YAML_INT(x.a, PLAIN);
/* 输出 x.b */
YAH_YAML_KEY(b, PLAIN); YAH_YAML_REAL(x.b, PLAIN);
/* 输出 x.c */
YAH_YAML_KEY(c, PLAIN); YAH_YAML_STR(x.c, PLAIN);
YAH_YAML_MAP_END(&emitter, &event);
YAH_YAML_MAP_END(&emitter, &event);
YAH_YAML_DOC_END(&emitter, &event, 0);
YAH_YAML_END(&emitter, &event);
yaml_event_delete(&event);
yaml_emitter_delete(&emitter);
fclose(yaml_file);
return EXIT_SUCCESS;
}
修整
既然我们已经掌握了宏定义中的 ##
符号的用法,那么之前定义的几个宏,它们颇长的参数也可以有所简化,如下:
#define YAH_YAML_START(emitter, event, encoding) do { \
if (!yaml_stream_start_event_initialize(event, YAML_##encoding##_ENCODING)) { \
fprintf(stderr, "failed to initialize *stream_start* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
#define YAH_YAML_SCALAR(emitter, event, string, style) do { \
if (!yaml_scalar_event_initialize(event, \
NULL, \
NULL, \
(const yaml_char_t *)(string), \
strlen(string), \
1, \
1, \
YAML_##style##_SCALAR_STYLE)) { \
fprintf(stderr, "failed to initialize *scalar* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
#define YAH_YAML_SEQ_START(emitter, event, style) do { \
if (!yaml_sequence_start_event_initialize(event, \
NULL, \
NULL, \
1, \
YAML_##style##_SEQUENCE_STYLE)) { \
fprintf(stderr, "failed to initialize *sequence_start* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
#define YAH_YAML_MAP_START(emitter, event, style) do { \
if (!yaml_mapping_start_event_initialize(event, \
NULL, \
NULL, \
1, \
YAML_##style##_MAPPING_STYLE)) { \
fprintf(stderr, "failed to initialize *mapping_start* event.\n"); \
exit(EXIT_FAILURE); \
} \
YAH_YAML_EMIT(emitter, event); \
} while (0)
#define YAH_YAML_INT(interger, style) do { \
char value[1024]; \
snprintf(value, sizeof(value), "%d", interger); \
YAH_YAML_SCALAR(&emitter, &event, value, style); \
} while (0)
#define YAH_YAML_REAL(real, style) do { \
char value[1024]; \
snprintf(value, sizeof(value), "%f", real); \
YAH_YAML_SCALAR(&emitter, &event, value, style); \
} while (0)
#define YAH_YAML_STR(string, style) do { \
YAH_YAML_SCALAR(&emitter, &event, string, style); \
} while (0)
用上述修改的宏定义替换 yah-yaml.h 中相应的宏定义,然后 foo.c 便可以更为清爽一些,如下:
#include
int main(void) {
FILE *yaml_file;
yaml_emitter_t emitter;
yaml_event_t event;
YAH_OPEN_FILE(yaml_file, "foo.yaml", "w");
YAH_YAML_EMITTER_INIT(&emitter, yaml_file);
YAH_YAML_START(&emitter, &event, UTF8);
YAH_YAML_DOC_START(&emitter, &event, 0);
struct foo {
int a;
double b;
const char *c;
} x = {42, 3.1415926, "Hello world!"};
YAH_YAML_MAP_START(&emitter, &event, BLOCK);
YAH_YAML_KEY(x, PLAIN);
YAH_YAML_MAP_START(&emitter, &event, BLOCK);
YAH_YAML_KEY(a, PLAIN); YAH_YAML_INT(x.a, PLAIN);
YAH_YAML_KEY(b, PLAIN); YAH_YAML_REAL(x.b, PLAIN);
YAH_YAML_KEY(c, PLAIN); YAH_YAML_STR(x.c, PLAIN);
YAH_YAML_MAP_END(&emitter, &event);
YAH_YAML_MAP_END(&emitter, &event);
YAH_YAML_DOC_END(&emitter, &event, 0);
YAH_YAML_END(&emitter, &event);
yaml_event_delete(&event);
yaml_emitter_delete(&emitter);
fclose(yaml_file);
return EXIT_SUCCESS;
}
总结
现在,我们已经征服了遍布灌木的 libyaml 山。这是一座并不高的山,但是当我们站在山顶上,终归是能感受到夏日里习习凉风。休息一会便可以下山。下山的途中,你可以思考这样一个问题,能否将要输出的 C 数据依序组织成链表或数组,每个节点或单元存储 C 变量的名字和它的值,再用一些节点或单元表达序列和映射的开始和结束,然后我们遍历这个链表或数组,以 YAML 格式输出每个节点或单元的信息。这个想法有些类似于,我们不必再一步一步走到山下,而是修建一条索道,像飞鸟一样滑到山下。