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 使用基于非侵入式 traits 的设计。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 是基于 traits 的,如何将您的数据转换为 YAML 的选择是基于您的数据类型。但是在 C++ 类型匹配中,typedefs 不会生成唯一的类型名称。这意味着如果您有两个 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 转换而来。创建这些唯一类型的目的是,您现在可以为它们指定 traits 以获得不同的 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++ 中另一个常见的数据结构是一个字段,其中每个位都有唯一的含义。这通常用于“flags”字段。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(在写入时)将测试位集 trait 中的每个值是否与 flags 字段匹配,并且每个匹配的值都会导致相应的字符串添加到流式序列中。读取时执行相反的操作,任何未知的字符串值都将导致错误。使用上述模式,相同的有效 YAML 文档是

name:    Tom
flags:   [ pointy, flat ]

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

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(在写入时)会将枚举掩码应用于 flags 字段,并比较结果和来自位集的值。与常规位集的情况一样,每个匹配的值都会导致相应的字符串添加到流式序列中。

自定义标量

有时为了可读性,标量需要以自定义方式格式化。例如,您的内部数据结构可能使用整数表示时间(自某个纪元以来的秒数),但在 YAML 中,以某种时间格式(例如 2012 年 5 月 4 日晚上 10:30)表示该整数会更好。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();
  }
};

映射

要转换为 YAML 映射或从 YAML 映射转换类型 T,您必须在 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);
  ...
  }
};

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

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() 方法负责规范化和反规范化。在原生数据结构不需要规范化的简单情况下,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 的栈分配实例,它由提供的 polar 对象构造,该对象初始化其 x 和 y 字段。然后 mapRequired() 方法将 x 和 y 值作为键/值对写出。

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

在某些情况下,规范化类可能是原生类型的子类,并且可以由 denormalize() 方法返回,除非临时的规范化实例是栈分配的。在这些情况下,可以改用实用程序模板 MappingNormalizationHeap<>。它与 MappingNormalization<> 完全相同,只是它在读取 YAML 时堆分配规范化对象。它永远不会销毁规范化对象。denormalize() 方法可以返回 “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 时,在写入 YAML 键/值之前,会调用 validate() 方法。输出期间的任何错误都将触发 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 对象配置进行换行。

序列

要转换为 YAML 序列或从 YAML 序列转换类型 T,您必须在 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 模式中的顶级节点将是映射或序列。对于这些情况,不需要以下内容。但是对于您确实想要多个文档的情况,您可以为您的文档列表类型指定一个 trait。该 trait 具有与 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() 方法中设置上下文中的值,并在内部映射的 mapping() 方法中检索这些值。

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

输出

llvm::yaml::Output 类用于使用在您的数据类型上定义的 traits 从您的内存数据结构生成 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;