YAML I/O

YAML 简介

YAML 是一种人类可读的数据序列化语言。完整的 YAML 语言规范可以在 yaml.org 上找到。YAML 最简单的形式只是“标量”、“映射”和“序列”。标量可以是任何数字或字符串。井号/散列符号 (#) 用于开始注释行。映射是一组键值对,其中键以冒号结尾。例如

# a mapping
name:      Tom
hat-size:  7

序列是一系列项目,每个项目以一个前导短划线 ('-') 开头。例如

# a sequence
- x86
- x86_64
- PowerPC

可以通过缩进将映射和序列组合起来。例如,一个映射序列,其中一个映射值本身是一个序列

# a sequence of mappings with one key's value being a sequence
- name:      Tom
  cpus:
   - x86
   - x86_64
- name:      Bob
  cpus:
   - x86
- name:      Dan
  cpus:
   - PowerPC
   - x86

有时,序列已知很短,并且每行一个条目过于冗长,因此 YAML 为序列提供了一种替代语法,称为“流序列”,其中您将逗号分隔的序列元素放入方括号中。上面的示例可以简化为

# a sequence of mappings with one key's value being a flow sequence
- name:      Tom
  cpus:      [ x86, x86_64 ]
- name:      Bob
  cpus:      [ x86 ]
- name:      Dan
  cpus:      [ PowerPC, x86 ]

YAML I/O 简介

使用缩进使 YAML 易于人类阅读和理解,但是让程序读取和写入 YAML 涉及许多繁琐的细节。YAML I/O 库构建并简化了读取和写入 YAML 文档的过程。

YAML I/O 假设您有一些“原生”数据结构,您希望能够将其转储为 YAML 并从 YAML 中重新创建。第一步是尝试为您的数据结构编写示例 YAML。您可能会发现,在查看可能的 YAML 表示形式后,您的数据结构到 YAML 的直接映射不可读。通常,字段的顺序与人类认为可读的顺序不一致。或者相同的信息在多个位置重复,这使得人类难以正确编写此类 YAML。

在关系数据库理论中,有一个称为规范化的设计步骤,您可以在其中重新组织字段和表。相同的考虑因素需要纳入 YAML 编码的设计中。但是,您可能不想更改现有的原生数据结构。因此,在写出 YAML 时,可能会有一个规范化步骤,而在读取 YAML 时,将会有一个相应的反规范化步骤。

YAML I/O 使用非侵入性的、基于特征的设计。YAML I/O 定义了一些抽象基模板。您在您的数据类型上专门化这些模板。例如,如果您有一个枚举类型 FooBar,您可以在该类型上专门化 ScalarEnumerationTraits 并定义 enumeration() 方法

using llvm::yaml::ScalarEnumerationTraits;
using llvm::yaml::IO;

template <>
struct ScalarEnumerationTraits<FooBar> {
  static void enumeration(IO &io, FooBar &value) {
  ...
  }
};

与所有 YAML I/O 模板专门化一样,ScalarEnumerationTraits 用于读取和写入 YAML。也就是说,内存中枚举值与 YAML 字符串表示之间的映射仅在一个位置。这确保了 YAML 写入和解析代码保持同步。

要指定 YAML 映射,您需要在 llvm::yaml::MappingTraits 上定义一个专门化。如果您的原生数据结构碰巧是一个已经规范化的结构,那么专门化就很简单。例如

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

template <>
struct MappingTraits<Person> {
  static void mapping(IO &io, Person &info) {
    io.mapRequired("name",         info.name);
    io.mapOptional("hat-size",     info.hatSize);
  }
};

如果您的数据类型具有 begin()/end() 迭代器和 push_back() 方法,则会自动推断 YAML 序列。因此,任何 STL 容器(例如 std::vector<>)都将自动转换为 YAML 序列。

一旦您为数据类型定义了专门化,就可以以编程方式使用 YAML I/O 来写入 YAML 文档

using llvm::yaml::Output;

Person tom;
tom.name = "Tom";
tom.hatSize = 8;
Person dan;
dan.name = "Dan";
dan.hatSize = 7;
std::vector<Person> persons;
persons.push_back(tom);
persons.push_back(dan);

Output yout(llvm::outs());
yout << persons;

这将写入以下内容

- name:      Tom
  hat-size:  8
- name:      Dan
  hat-size:  7

您还可以使用以下代码读取此类 YAML 文档

using llvm::yaml::Input;

typedef std::vector<Person> PersonList;
std::vector<PersonList> docs;

Input yin(document.getBuffer());
yin >> docs;

if ( yin.error() )
  return;

// Process read document
for ( PersonList &pl : docs ) {
  for ( Person &person : pl ) {
    cout << "name=" << person.name;
  }
}

YAML 的另一个特性是能够在一个文件中定义多个文档。这就是读取 YAML 会生成文档类型向量的缘故。

错误处理

在解析 YAML 文档时,如果输入与您的模式(如您的 XxxTraits<> 专门化中表达的那样)不匹配。YAML I/O 将打印错误消息,并且 Input 对象的 error() 方法将返回 true。例如,以下文档

- name:      Tom
  shoe-size: 12
- name:      Dan
  hat-size:  7

有一个在模式中未定义的键 (shoe-size)。YAML I/O 将自动生成此错误

YAML:2:2: error: unknown key 'shoe-size'
  shoe-size:       12
  ^~~~~~~~~

对于不符合模式的其他输入,也会生成类似的错误。

标量

YAML 标量只是字符串(即不是序列或映射)。YAML I/O 库提供支持,用于在 YAML 标量和特定的 C++ 类型之间进行转换。

内置类型

以下类型在 YAML I/O 中具有内置支持

  • bool

  • float

  • double

  • StringRef

  • std::string

  • int64_t

  • int32_t

  • int16_t

  • int8_t

  • uint64_t

  • uint32_t

  • uint16_t

  • uint8_t

也就是说,您可以在 MappingTraits 的字段中或作为序列中的元素类型使用这些类型。在读取时,YAML I/O 将验证找到的字符串是否可转换为该类型,如果不可转换则会报错。

唯一类型

鉴于 YAML I/O 是基于特征的,如何将数据转换为 YAML 的选择是基于数据类型的。但在 C++ 类型匹配中,typedef 不会生成唯一的类型名称。这意味着如果您有两个 unsigned int 的 typedef,对于 YAML I/O 来说,这两种类型看起来都完全像 unsigned int。为了促进创建唯一的类型名称,YAML I/O 提供了一个宏,该宏像 typedef 一样用于内置类型,但扩展为创建一个具有到基类型和从基类型转换运算符的类。例如

LLVM_YAML_STRONG_TYPEDEF(uint32_t, MyFooFlags)
LLVM_YAML_STRONG_TYPEDEF(uint32_t, MyBarFlags)

这将生成两个类 MyFooFlags 和 MyBarFlags,您可以在原生数据结构中使用它们来代替 uint32_t。它们会隐式地转换为 uint32_t 和从 uint32_t 转换。创建这些唯一类型的目的是,您现在可以为它们指定特征以获得不同的 YAML 转换。

十六进制类型

唯一类型的一个示例用法是,YAML I/O 提供固定大小的无符号整数,这些整数以 YAML I/O 作为十六进制写入,而不是内置整数类型使用的十进制格式

  • Hex64

  • Hex32

  • Hex16

  • Hex8

您可以使用 llvm::yaml::Hex32 代替 uint32_t,唯一的区别是当 YAML I/O 写出该类型时,它将以十六进制格式化。

ScalarEnumerationTraits

YAML I/O 支持在内存中枚举和 YAML 文档中的一组字符串值之间进行转换。这是通过在您的枚举类型上专门化 ScalarEnumerationTraits<> 并定义一个 enumeration() 方法来完成的。例如,假设您有一个 CPU 的枚举和一个以它作为字段的结构

enum CPUs {
  cpu_x86_64  = 5,
  cpu_x86     = 7,
  cpu_PowerPC = 8
};

struct Info {
  CPUs      cpu;
  uint32_t  flags;
};

要支持此枚举的读取和写入,您可以在 CPU 上定义 ScalarEnumerationTraits 专门化,然后将其用作字段类型

using llvm::yaml::ScalarEnumerationTraits;
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

template <>
struct ScalarEnumerationTraits<CPUs> {
  static void enumeration(IO &io, CPUs &value) {
    io.enumCase(value, "x86_64",  cpu_x86_64);
    io.enumCase(value, "x86",     cpu_x86);
    io.enumCase(value, "PowerPC", cpu_PowerPC);
  }
};

template <>
struct MappingTraits<Info> {
  static void mapping(IO &io, Info &info) {
    io.mapRequired("cpu",       info.cpu);
    io.mapOptional("flags",     info.flags, 0);
  }
};

在读取 YAML 时,如果找到的字符串与 enumCase() 方法指定的任何字符串都不匹配,则会自动生成错误。在写入 YAML 时,如果要写入的值与 enumCase() 方法指定的任何值都不匹配,则会触发运行时断言。

BitValue

C++ 中另一种常见的数据结构是一个字段,其中每个位都有唯一的含义。这通常用于“标志”字段中。YAML I/O 支持将此类字段转换为流序列。例如,假设您定义了以下位标志

enum {
  flagsPointy = 1
  flagsHollow = 2
  flagsFlat   = 4
  flagsRound  = 8
};

LLVM_YAML_STRONG_TYPEDEF(uint32_t, MyFlags)

要支持 MyFlags 的读取和写入,您需要在 MyFlags 上专门化 ScalarBitSetTraits<> 并提供位值及其名称。

using llvm::yaml::ScalarBitSetTraits;
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

template <>
struct ScalarBitSetTraits<MyFlags> {
  static void bitset(IO &io, MyFlags &value) {
    io.bitSetCase(value, "hollow",  flagHollow);
    io.bitSetCase(value, "flat",    flagFlat);
    io.bitSetCase(value, "round",   flagRound);
    io.bitSetCase(value, "pointy",  flagPointy);
  }
};

struct Info {
  StringRef   name;
  MyFlags     flags;
};

template <>
struct MappingTraits<Info> {
  static void mapping(IO &io, Info& info) {
    io.mapRequired("name",  info.name);
    io.mapRequired("flags", info.flags);
   }
};

使用上述方法,YAML I/O(在写入时)将测试位集特征中每个值与标志字段的掩码,并且每个匹配的位将导致相应的字符串添加到流序列中。读取时执行相反的操作,并且任何未知的字符串值都将导致错误。使用上述模式,一个有效的 YAML 文档是

name:    Tom
flags:   [ pointy, flat ]

有时,“标志”字段可能包含由位掩码定义的枚举部分。

enum {
  flagsFeatureA = 1,
  flagsFeatureB = 2,
  flagsFeatureC = 4,

  flagsCPUMask = 24,

  flagsCPU1 = 8,
  flagsCPU2 = 16
};

要支持此类字段的读取和写入,您需要使用 maskedBitSet() 方法并提供位值、其名称和枚举掩码。

template <>
struct ScalarBitSetTraits<MyFlags> {
  static void bitset(IO &io, MyFlags &value) {
    io.bitSetCase(value, "featureA",  flagsFeatureA);
    io.bitSetCase(value, "featureB",  flagsFeatureB);
    io.bitSetCase(value, "featureC",  flagsFeatureC);
    io.maskedBitSetCase(value, "CPU1",  flagsCPU1, flagsCPUMask);
    io.maskedBitSetCase(value, "CPU2",  flagsCPU2, flagsCPUMask);
  }
};

YAML I/O(在写入时)将枚举掩码应用于标志字段,并将结果与位集中的值进行比较。与常规位集一样,每个匹配的位将导致相应的字符串添加到流序列中。

自定义标量

有时为了可读性,需要以自定义方式格式化标量。例如,您的内部数据结构可能使用整数表示时间(自某个纪元以来的秒数),但在 YAML 中,以某种时间格式(例如 4-May-2012 10:30pm)表示该整数会更好。YAML I/O 有一种方法可以通过在您的数据类型上专门化 ScalarTraits<> 来支持标量类型的自定义格式化和解析。在写入时,YAML I/O 将提供原生类型,并且您的专门化必须创建一个临时的 llvm::StringRef。在读取时,YAML I/O 将提供标量的 llvm::StringRef,并且您的专门化必须将其转换为您的原生数据类型。自定义标量类型的概述如下所示

using llvm::yaml::ScalarTraits;
using llvm::yaml::IO;

template <>
struct ScalarTraits<MyCustomType> {
  static void output(const MyCustomType &value, void*,
                     llvm::raw_ostream &out) {
    out << value;  // do custom formatting here
  }
  static StringRef input(StringRef scalar, void*, MyCustomType &value) {
    // do custom parsing here.  Return the empty string on success,
    // or an error message on failure.
    return StringRef();
  }
  // Determine if this scalar needs quotes.
  static QuotingType mustQuote(StringRef) { return QuotingType::Single; }
};

块标量

YAML 块标量是字符串文字,它们在 YAML 中使用文字块表示法表示,就像下面显示的示例一样

text: |
  First line
  Second line

YAML I/O 库通过允许您专门针对您的数据类型专门化 BlockScalarTraits<>,从而为 YAML 块标量和特定 C++ 类型之间的转换提供支持。该库没有为像 std::string 和 llvm::StringRef 这样的类型提供任何内置的块标量 I/O 支持,因为它们已经由 YAML I/O 支持,并且默认使用普通的标量表示法。

BlockScalarTraits 的专门化与 ScalarTraits 的专门化非常相似 - YAML I/O 将提供本机类型,您的专门化必须在写入时创建一个临时的 llvm::StringRef,它还将提供一个包含该块标量值的 llvm::StringRef,并且您的专门化必须在读取时将其转换为您的本机数据类型。下面显示了一个具有 BlockScalarTraits 适当专门化的自定义类型的示例

using llvm::yaml::BlockScalarTraits;
using llvm::yaml::IO;

struct MyStringType {
  std::string Str;
};

template <>
struct BlockScalarTraits<MyStringType> {
  static void output(const MyStringType &Value, void *Ctxt,
                     llvm::raw_ostream &OS) {
    OS << Value.Str;
  }

  static StringRef input(StringRef Scalar, void *Ctxt,
                         MyStringType &Value) {
    Value.Str = Scalar.str();
    return StringRef();
  }
};

映射

要转换为或从您的类型 T 的 YAML 映射转换,您必须专门针对 T 专门化 llvm::yaml::MappingTraits 并实现“void mapping(IO &io, T&)”方法。如果您的本机数据结构在任何地方都使用指向类的指针,则可以在类指针上进行专门化。示例

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

// Example of struct Foo which is used by value
template <>
struct MappingTraits<Foo> {
  static void mapping(IO &io, Foo &foo) {
    io.mapOptional("size",      foo.size);
  ...
  }
};

// Example of struct Bar which is natively always a pointer
template <>
struct MappingTraits<Bar*> {
  static void mapping(IO &io, Bar *&bar) {
    io.mapOptional("size",    bar->size);
  ...
  }
};

在某些情况下,我们希望允许将整个映射读取为枚举。例如,假设某些配置选项最初是作为枚举开始的。然后它变得更复杂,所以现在它是一个映射。但有必要支持旧的配置文件。在这种情况下,添加一个函数 enumInput,就像 ScalarEnumerationTraits::enumeration 一样。示例

struct FooBarEnum {
  int Foo;
  int Bar;
  bool operator==(const FooBarEnum &R) const {
    return Foo == R.Foo && Bar == R.Bar;
  }
};

template <> struct MappingTraits<FooBarEnum> {
  static void enumInput(IO &io, FooBarEnum &Val) {
    io.enumCase(Val, "OnlyFoo", FooBarEnum({1, 0}));
    io.enumCase(Val, "OnlyBar", FooBarEnum({0, 1}));
  }
  static void mapping(IO &io, FooBarEnum &Val) {
    io.mapOptional("Foo", Val.Foo);
    io.mapOptional("Bar", Val.Bar);
  }
};

无规范化

如果需要,mapping() 方法负责规范化和反规范化。在一个简单的案例中,如果本机数据结构不需要规范化,则映射方法只需使用 mapOptional() 或 mapRequired() 将结构的字段绑定到 YAML 键名。例如

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

template <>
struct MappingTraits<Person> {
  static void mapping(IO &io, Person &info) {
    io.mapRequired("name",         info.name);
    io.mapOptional("hat-size",     info.hatSize);
  }
};

规范化

当需要 [反] 规范化时,mapping() 方法需要一种方法来访问规范化的值作为字段。为了帮助解决这个问题,有一个模板 MappingNormalization<>,您可以使用它来自动执行规范化和反规范化。该模板用于在您的 mapping() 方法中创建一个局部变量,其中包含规范化的键。

假设您有本机数据类型 Polar,它指定极坐标系中的位置(距离、角度)

struct Polar {
  float distance;
  float angle;
};

但是您已决定规范化的 YAML 应使用 x、y 坐标。也就是说,您希望 yaml 看起来像

x:   10.3
y:   -4.7

您可以通过定义一个 MappingTraits 来支持这一点,该 MappingTraits 在写入 YAML 时将极坐标规范化为 x、y 坐标,并在读取 YAML 时将 x、y 坐标反规范化为极坐标。

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

template <>
struct MappingTraits<Polar> {

  class NormalizedPolar {
  public:
    NormalizedPolar(IO &io)
      : x(0.0), y(0.0) {
    }
    NormalizedPolar(IO &, Polar &polar)
      : x(polar.distance * cos(polar.angle)),
        y(polar.distance * sin(polar.angle)) {
    }
    Polar denormalize(IO &) {
      return Polar(sqrt(x*x+y*y), arctan(x,y));
    }

    float        x;
    float        y;
  };

  static void mapping(IO &io, Polar &polar) {
    MappingNormalization<NormalizedPolar, Polar> keys(io, polar);

    io.mapRequired("x",    keys->x);
    io.mapRequired("y",    keys->y);
  }
};

在写入 YAML 时,局部变量“keys”将是 NormalizedPolar 的一个栈分配实例,由提供的极坐标对象构造,该对象初始化其 x 和 y 字段。然后,mapRequired() 方法将 x 和 y 值作为键值对写入。

在读取 YAML 时,局部变量“keys”将是 NormalizedPolar 的一个栈分配实例,由空构造函数构造。mapRequired 方法将在 YAML 文档中找到匹配的键,并填充 NormalizedPolar 对象 keys 的 x 和 y 字段。在 mapping() 方法结束时,当局部变量 keys 超出范围时,将自动调用反规范化() 方法将读取的值转换回极坐标,然后分配回 mapping() 的第二个参数。

在某些情况下,规范化类可能是本机类型的子类,并且可以通过反规范化() 方法返回,除了临时规范化实例是栈分配的。在这些情况下,可以使用实用程序模板 MappingNormalizationHeap<>。它与 MappingNormalization<> 类似,只是它在读取 YAML 时堆分配规范化对象。它永远不会销毁规范化对象。反规范化() 方法可以返回“this”。

默认值

mapping() 方法中,对 io.mapRequired() 的调用意味着在解析 YAML 文档时该键必须存在,否则 YAML I/O 将发出错误。

另一方面,使用 io.mapOptional() 注册的键允许在读取的 YAML 文档中不存在。那么这些可选键的字段中将输入什么值?填充这些可选字段的方法有两个步骤。首先,mapping() 方法的第二个参数是对本机类的引用。该本机类必须具有默认构造函数。默认构造函数最初为可选字段设置的任何值都将是该字段的值。其次,mapOptional() 方法有一个可选的第三个参数。如果提供,则它是 mapOptional() 应该设置该字段的值,如果 YAML 文档中没有该键。

这两种方法(默认构造函数和 mapOptional 的第三个参数)之间存在一个重要区别。当 YAML I/O 生成 YAML 文档时,如果使用了 mapOptional() 的第三个参数,如果正在写入的实际值与(使用 ==)默认值相同,则不会写入该键值对。

键的顺序

在写出 YAML 文档时,键的写入顺序与在 mapping() 方法中对 mapRequired()/mapOptional() 的调用顺序相同。这使您有机会以 YAML 文档的人类读者认为自然的顺序来写入字段。这可能与本机类中字段的顺序不同。

在读取 YAML 文档时,文档中的键可以按任何顺序排列,但它们是按照在 mapping() 方法中对 mapRequired()/mapOptional() 的调用顺序进行处理的。这使得一些有趣的功能成为可能。例如,如果绑定的第一个字段是 cpu,绑定的第二个字段是 flags,并且 flags 是特定于 cpu 的,则可以根据 cpu 以编程方式切换 flags 如何转换为和从 YAML 转换。这适用于读取和写入。例如

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

struct Info {
  CPUs        cpu;
  uint32_t    flags;
};

template <>
struct MappingTraits<Info> {
  static void mapping(IO &io, Info &info) {
    io.mapRequired("cpu",       info.cpu);
    // flags must come after cpu for this to work when reading yaml
    if ( info.cpu == cpu_x86_64 )
      io.mapRequired("flags",  *(My86_64Flags*)info.flags);
    else
      io.mapRequired("flags",  *(My86Flags*)info.flags);
 }
};

标签

YAML 语法支持标签作为在解析节点之前指定节点类型的一种方法。这允许节点的动态类型。但是 YAML I/O 模型使用静态类型,因此您在 YAML I/O 模型中使用标签的方式受到限制。最近,我们在 YAML I/O 中添加了对检查/设置映射上可选标签的支持。使用此功能,甚至可以支持不同的映射,只要它们是可转换的。

要检查标签,在您的 mapping() 方法中,您可以使用 io.mapTag() 指定标签应该是什么。这也会在写入 yaml 时添加该标签。

验证

有时在 YAML 映射中,每个键值对都是有效的,但组合无效。这类似于某些东西没有语法错误,但仍然有语义错误。为了支持语义级别检查,YAML I/O 允许在 MappingTraits 模板专门化中使用可选的 validate() 方法。

在解析 YAML 时,validate() 方法在处理映射中的所有键值对之后调用。在输入期间 validate() 方法返回的任何错误消息都将像打印语法错误一样打印。在写入 YAML 时,validate() 方法在 YAML 键值对写入之前调用。输出期间的任何错误都将触发 assert(),因为拥有无效的结构值是一个编程错误。

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

struct Stuff {
  ...
};

template <>
struct MappingTraits<Stuff> {
  static void mapping(IO &io, Stuff &stuff) {
  ...
  }
  static std::string validate(IO &io, Stuff &stuff) {
    // Look at all fields in 'stuff' and if there
    // are any bad values return a string describing
    // the error.  Otherwise return an empty string.
    return std::string{};
  }
};

流映射

YAML“流映射”是指在写入 YAML 时使用内联表示法(例如 { x: 1, y: 0 })的映射。要指定类型应使用流映射写入 YAML,您的 MappingTraits 专门化应添加“static const bool flow = true;”。例如

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

struct Stuff {
  ...
};

template <>
struct MappingTraits<Stuff> {
  static void mapping(IO &io, Stuff &stuff) {
    ...
  }

  static const bool flow = true;
}

流映射会根据 Output 对象配置进行换行。

序列

要转换为或从您的类型 T 的 YAML 序列转换,您必须专门针对 T 专门化 llvm::yaml::SequenceTraits 并实现两个方法:size_t size(IO &io, T&)T::value_type& element(IO &io, T&, size_t indx)。例如

template <>
struct SequenceTraits<MySeq> {
  static size_t size(IO &io, MySeq &list) { ... }
  static MySeqEl &element(IO &io, MySeq &list, size_t index) { ... }
};

size() 方法返回序列中当前有多少个元素。element() 方法返回对序列中第 i 个元素的引用。在解析 YAML 时,element() 方法可能会使用比当前大小大 1 的索引调用。您的 element() 方法应为一个或多个元素分配空间(如果元素是 C++ 对象,则使用默认构造函数),并返回对该新分配空间的引用。

流序列

YAML“流序列”是指在写入 YAML 时使用内联表示法(例如 [ foo, bar ])的序列。要指定序列类型应作为流序列写入 YAML,您的 SequenceTraits 专门化应添加“static const bool flow = true;”。例如

template <>
struct SequenceTraits<MyList> {
  static size_t size(IO &io, MyList &list) { ... }
  static MyListEl &element(IO &io, MyList &list, size_t index) { ... }

  // The existence of this member causes YAML I/O to use a flow sequence
  static const bool flow = true;
};

使用上述方法,如果您在您的本机数据结构中使用 MyList 作为数据类型,那么在转换为 YAML 时,将使用整数的流序列(例如 [ 10, -3, 4 ])。

流序列会根据 Output 对象配置进行换行。

实用程序宏

由于序列的常见来源是 std::vector<>,因此 YAML I/O 提供了宏:LLVM_YAML_IS_SEQUENCE_VECTOR() 和 LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(),它们可用于轻松地为 std::vector 类型指定 SequenceTraits<>。YAML I/O 不对 std::vector<> 进行部分专门化 SequenceTraits,因为这将强制所有向量都成为序列。宏的使用示例

std::vector<MyType1>;
std::vector<MyType2>;
LLVM_YAML_IS_SEQUENCE_VECTOR(MyType1)
LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(MyType2)

文档列表

YAML 允许您在一个 YAML 文件中定义多个“文档”。每个新文档都以左对齐的“—”标记开头。所有文档的结尾用左对齐的“…”标记表示。许多 YAML 用户永远不需要多个文档。他们 YAML 架构中的顶级节点将是映射或序列。对于这些情况,不需要以下内容。但是对于确实需要多个文档的情况,您可以为文档列表类型指定一个特性。该特性与 SequenceTraits 具有相同的方法,但名为 DocumentListTraits。例如

template <>
struct DocumentListTraits<MyDocList> {
  static size_t size(IO &io, MyDocList &list) { ... }
  static MyDocType element(IO &io, MyDocList &list, size_t index) { ... }
};

用户上下文数据

当创建 llvm::yaml::Input 或 llvm::yaml::Output 对象时,它们的构造函数会接受一个可选的“context”参数。这是一个指向您可能需要的任何状态信息的指针。

例如,在前面的示例中,我们展示了如何根据映射中另一个字段的值在运行时确定 flags 字段的转换类型。但是,如果内部映射需要知道外部映射的一些字段值怎么办?这就是“context”参数的作用。您可以在外部映射的 mapping() 方法中设置 context 中的值,并在内部映射的 mapping() 方法中检索这些值。

上下文值只是一个 void*。所有使用上下文并在您的原生数据类型上操作的特性都需要约定上下文值到底是什么。它可以是指向对象或结构体的指针,您的各种特性使用它来共享上下文敏感信息。

输出

llvm::yaml::Output 类用于根据您内存中的数据结构生成 YAML 文档,使用您数据类型上定义的特性。要实例化 Output 对象,您需要一个 llvm::raw_ostream、一个可选的上下文指针和一个可选的换行列。

class Output : public IO {
public:
  Output(llvm::raw_ostream &, void *context = NULL, int WrapColumn = 70);

获得 Output 对象后,您可以使用 C++ 流运算符在其上写入您的原生数据作为 YAML。需要注意的是,YAML 文件可以包含多个“文档”。如果作为 YAML 流式传输的顶级数据结构是映射、标量或序列,则 Output 假定您正在生成一个文档,并使用“---”和尾随的“...”包装映射输出。

WrapColumn 参数将导致流映射和序列在超过提供的列时换行。传递 0 以完全抑制换行。

using llvm::yaml::Output;

void dumpMyMapDoc(const MyMapType &info) {
  Output yout(llvm::outs());
  yout << info;
}

以上可能产生如下输出:

---
name:      Tom
hat-size:  7
...

另一方面,如果作为 YAML 流式传输的顶级数据结构具有 DocumentListTraits 特化,则 Output 将遍历您 DocumentList 的每个元素,并在每个元素的开始之前生成“—”并以“…”结尾。

using llvm::yaml::Output;

void dumpMyMapDoc(const MyDocListType &docList) {
  Output yout(llvm::outs());
  yout << docList;
}

以上可能产生如下输出:

---
name:      Tom
hat-size:  7
---
name:      Tom
shoe-size:  11
...

输入

llvm::yaml::Input 类用于将 YAML 文档解析到您的原生数据结构中。要实例化 Input 对象,您需要一个指向整个 YAML 文件的 StringRef,以及一个可选的上下文指针。

class Input : public IO {
public:
  Input(StringRef inputContent, void *context=NULL);

获得 Input 对象后,您可以使用 C++ 流运算符读取文档。如果您预计在一个文件中可能有多个 YAML 文档,则需要在文档类型的列表上特化 DocumentListTraits 并以该文档列表类型进行流式传输。否则,您可以直接流式传输文档类型。此外,您可以通过在 Input 对象上调用 error() 方法来检查 YAML 中是否存在任何语法错误。例如:

// Reading a single document
using llvm::yaml::Input;

Input yin(mb.getBuffer());

// Parse the YAML file
MyDocType theDoc;
yin >> theDoc;

// Check for error
if ( yin.error() )
  return;
// Reading multiple documents in one file
using llvm::yaml::Input;

LLVM_YAML_IS_DOCUMENT_LIST_VECTOR(MyDocType)

Input yin(mb.getBuffer());

// Parse the YAML file
std::vector<MyDocType> theDocList;
yin >> theDocList;

// Check for error
if ( yin.error() )
  return;