C 对象 -> YAML

前言

用 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);
}

接下来,将 emitteryaml_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);
}

上述代码构造的 eventstream_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);

既然数据写入过程结束了,那么 eventemitter 也就没有必要存在了,虽然它们不是指针类型,但他们是结构体对象,成员变量会有一些指针,在它们初始化的时候,会被 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_STARTYAH_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}

我知道,你也许已经有些不耐烦了,大概会想,为何不用 printffprintf 直接向输出上述内容呢?简单的数据可以如此,但是对于复杂一些的数据,例如多层嵌套的结构体,要输出为 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 格式输出每个节点或单元的信息。这个想法有些类似于,我们不必再一步一步走到山下,而是修建一条索道,像飞鸟一样滑到山下。

你可能感兴趣的:(yamlc)