回上级页面

中间文件

2023 年 09 月 22 日


问题

假设以下文本为 rzeo 的输入:

以下 Rust 程序

@ hello world #
fn main() {
    println!("Hello world!");
}
@

可在终端打印「Hello world!」。

rzeo 可将其转化为以下 YAML 格式:

- type: 文档片段
  name:
  content: '以下 Rust 程序'
- type: 源码片段
  name: 'hello world'
  content: |-
      'fn main() {
          println!("Hello world!");
      }'
- type: 文档片段
  name:
  content: '可在终端打印「Hello world!」。'

基于 rzeo 的输出,可编写一些程序,将其转化为某些排版软件支持的文档格式,从而实现程序文档的排版。rzeo 不生产程序的文档和源码,只是程序设计思维的搬运工。

该如何解析上述的 YAML 格式呢?在第 6 章「配置文件」中讲述了如何解析 YAML 格式,但所用示例较为简单,不足以令人掌握 Rust 的 Serde 库和 C 的 libcyaml 库的基本用法。可将本章内容视为第 6 章的延续。

若不熟悉上述 YAML 标记,可参考「YAML 语言教程」。

数组反序列化

rzeo 输出的 YAML 文件,本质上是一个数组。在 Rust 语言里,与之对应的数据结构是 Vec<T>,其中 T 在此是结构体类型:

#[derive(Debug, serde::Deserialize)]
struct Node {
    type: String,
    name: String,
    content: String
}

Node 类型的实例,当其 type文档片段 时,其 name 是空字符串。不过上述代码是无法通过编译的,因为 type 是 Rust 关键字,不允许作为变量名使用。rustc 建议给 type 增加前缀 r#,用于指示编译器,所标定的字符串是普通文本,而非关键字。

serde_yaml 库可将 YAML 的数组结构解析为 Vec<T> 实例,例如:

let foo: Vec<Node> = serde_yaml::from_reader(f).unwrap();
for x in foo {
    println!("{:?}", x);
}

下面是完整的 YAML 数组反序列化代码:

use std::fs::File;

#[derive(Debug, serde::Deserialize)]
struct Node {
    r#type: String,
    name: String,
    content: String
}

fn main() -> Result<(), std::io::Error> {
    let f = File::open("foo.yml")?;
    let foo: Vec<Node> = serde_yaml::from_reader(f).unwrap();
    for x in foo {
        println!("{:?}", x);
    }
    return Ok(());
}

输出结果为

Node { type: "文档片段", name: "", content: "以下 Rust 程序" }
Node { type: "源码片段", name: "hello world", 
       content: "'fn main() {\n    println!(\"Hello world!\");\n}'" }
Node { type: "文档片段", name: "", content: "可在终端打印「Hello world!」。" }

省略项

在 YAML 文件中,值为空的数据项(对象),可以省略。例如

- type: 文档片段
  content: '以下 Rust 程序'
- type: 源码片段
  name: 'hello world'
  content: |-
      'fn main() {
          println!("Hello world!");
      }'
- type: 文档片段
  content: '可在终端打印「Hello world!」。'

不过,上一节的程序在解析上述 YAML 文件时会出错,因为 serde 库无法为类型为 文档片段 的结点提供 name 的默认值。规避该错误的办法是,将 Node 的定义修改为

#[derive(Debug, serde::Deserialize)]
struct Node {
    r#type: String,
    #[serde(default)]
    name: String,
    content: String
}

上述修改可使得 serde 库能够为 name 提供默认值——空字符串。

异构数组

上文示例中的 YAML 文件,其中类型为 文档片段 的条目不需要 name,若欲消除数据冗余,可将 YAML 文件可改为

- Doc:
    content: '以下 Rust 程序'
- Code:
    name: 'hello world'
    content: |-
        'fn main() {
            println!("Hello world!");
        }'
- Doc:
    content: '可在终端打印「Hello world!」。'

注意:serde_yaml 库不支持在 YAML 文件中以中文字符作为对象名。

此时,YAML 数组中的元素类型不相同,故而需要使用 enum 类型定义 Vec<T>T,将两种类型的元素统一为一种类型:

#[derive(Debug, serde::Deserialize)]
struct DocFragment {
    content: String
}

#[derive(Debug, serde::Deserialize)]
struct CodeFragment {
    name: String,
    content: String
}

#[derive(Debug, serde::Deserialize)]
enum Node {
    Doc: DocFragment,
    Code: CodeFragment
}

然而,只有使用 YAML 的自定义标签语法方能使得 serde_yaml 库将 YAML 数据项反序列化为 enum 类型的实例,故而上述 YAML 文件内容需修改为

- !Doc
    content: '以下 Rust 程序'
- !Code
    name: 'hello world'
    content: |-
        'fn main() {
            println!("Hello world!");
        }'
- !Doc
    content: '可在终端打印「Hello world!」。'

解析上述 YAML 文件的完整 Rust 代码如下:

use std::fs::File;

#[derive(Debug, serde::Deserialize)]
struct DocFragment {
    content: String
}

#[derive(Debug, serde::Deserialize)]
struct CodeFragment {
    name: String,
    content: String
}

#[derive(Debug, serde::Deserialize)]
enum Node {
    Doc(DocFragment),
    Code(CodeFragment)
}

fn main() -> Result<(), std::io::Error> {
    let f = File::open("foo.yml")?;
    let foo: Vec<Node> = serde_yaml::from_reader(f).unwrap();
    for x in foo {
        println!("{:?}", x);
    }
    return Ok(());
}

C 版本

C 库 libcyaml(1.0 版本)目前不支持 YAML 单纯的数组结构。例如对于以下 YAML 文件:

- type: 文档片段
  content: '以下 Rust 程序'
- type: 源码片段
  name: 'hello world'
  content: |-
      'fn main() {
          println!("Hello world!");
      }'
- type: 文档片段
  content: '可在终端打印「Hello world!」。'

目前无法基于 libcyaml 写出相应的解析代码。也有可能是能够写得出,只是我尚不得其门径。若将上述数组结构修改为 YAML 对象(键值对),例如

nodes:
    - type: 文档片段
      content: '以下 Rust 程序'
    - type: 源码片段
      name: 'hello world'
      content: |-
          'fn main() {
              println!("Hello world!");
          }'
    - type: 文档片段
      content: '可在终端打印「Hello world!」。'

解析代码便可写得出来,例如

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <cyaml/cyaml.h>

typedef struct {
        char *type;
        char *name;
        char *content;
} Node;

typedef struct {
        Node *nodes;
        size_t nodes_count;
} Foo;

static const cyaml_schema_field_t node_field_schema[] = {
        CYAML_FIELD_STRING_PTR("type", CYAML_FLAG_POINTER,
                               Node, type, 0, CYAML_UNLIMITED),
        CYAML_FIELD_STRING_PTR("name", CYAML_FLAG_POINTER | CYAML_FLAG_OPTIONAL,
                               Node, name, 0, CYAML_UNLIMITED),
        CYAML_FIELD_STRING_PTR("content",
                               CYAML_FLAG_POINTER, Node, content, 0, CYAML_UNLIMITED),
        CYAML_FIELD_END
};
static const cyaml_schema_value_t node_schema = {
        CYAML_VALUE_MAPPING(CYAML_FLAG_DEFAULT, Node, node_field_schema)
};
static const cyaml_schema_field_t foo_field_schema[] = {
        CYAML_FIELD_SEQUENCE("nodes", CYAML_FLAG_POINTER,
                             Foo, nodes, &node_schema, 0, CYAML_UNLIMITED),
        CYAML_FIELD_END
};
static const cyaml_schema_value_t top = {
        CYAML_VALUE_MAPPING(CYAML_FLAG_POINTER, Foo, foo_field_schema)
};
/* 解析器设置 */
static const cyaml_config_t config = {
        .log_fn = cyaml_log,
        .mem_fn = cyaml_mem,
        .log_level = CYAML_LOG_WARNING
};
/* 反序列化 */
int main(void) {
        Foo *foo;
        cyaml_err_t err = cyaml_load_file("foo.yml", &config, &top,
                                          (cyaml_data_t **)&foo, NULL);
        if (err != CYAML_OK) {
                fprintf(stderr, "ERROR: %s\n", cyaml_strerror(err));
                return EXIT_FAILURE;
        }
        for (size_t i = 0; i < foo->nodes_count; i++) {
                printf("- type: %s\n", (foo->nodes[i]).type);
                if (foo->nodes[i].name) {
                        printf("  name: %s\n", (foo->nodes[i]).name);
                }
                printf("  content: %s\n", (foo->nodes[i]).content);
        }
        cyaml_free(&config, &top, foo, 0);
        return 0;
}

注意,上述代码中的 CYAML_FLAG_OPTIONAL 相当于 Rust 结构体里的 #[serde(Default)] 标记。

小结

与 Rust 库 serde_cyaml 相比,C 库 libcyaml 库用起来颇为晦涩,且遇到非常规的 YAML 格式,只能使用更为底层的库 libyaml 进行半自动解析。在 C 程序的数据交换文件格式选择方面,我不推荐 YAML,更好的方案是将 Lua 解释器嵌入 C 程序,然后将 Lua 表作为数据交换格式。