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 相互轉換。建立這些獨特類型的目的是,您現在可以針對它們指定特徵,以獲得不同的 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;
};

為了支援此列舉的讀取和寫入,您可以定義 CPUs 上的 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 中,以某種時間格式(例如 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::enumerationenumInput 函數。範例

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 的堆疊分配實例,由提供的極座標物件建構,該物件初始化其 x 和 y 欄位。然後 mapRequired() 方法將 x 和 y 值作為鍵/值對寫出。

讀取 YAML 時,本機變數 "keys" 將是 NormalizedPolar 的堆疊分配實例,由空建構子建構。mapRequired 方法將在 YAML 文件中找到匹配的鍵,並填寫 NormalizedPolar 物件鍵的 x 和 y 欄位。在 mapping() 方法結束時,當本機鍵變數超出範圍時,將自動呼叫 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,並且旗標是特定於 cpu 的,您可以根據 cpu 以程式設計方式切換如何將旗標轉換為 YAML 和從 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() 方法。您的 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 物件時,它們的建構子會採用可選的「上下文」參數。這是指向您可能需要的任何狀態資訊的指標。

例如,在先前的範例中,我們展示了如何根據映射中另一個欄位的值在執行階段確定旗標欄位的轉換類型。但是,如果內部映射需要知道外部映射的某些欄位值怎麼辦?這就是「上下文」參數的用武之地。您可以在外部映射的 mapping() 方法中設定上下文中的值,並在內部映射的 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;