Mirror 的工作原理

SwiftGG翻譯組發表於2018-11-15

作者:Mike Ash,原文連結,原文日期:2018-09-26 譯者:Nemocdz;校對:numbbbbb小鐵匠Linus;定稿:Forelax

儘管 Swift 重心在強調靜態型別上,但它同時支援豐富的後設資料型別。後設資料型別允許程式碼在執行時檢查和操作任意值。這個功能通過 Mirror API 暴露給 Swift 開發者。大家可能會感到困惑,在 Swift 這種如此強調靜態型別的語言裡,Mirror 這樣的特性是怎麼工作的?讓我們一起來通過這篇文章瞭解一下。

事先宣告

這裡介紹的東西都是內部實現的細節。這些程式碼的版本是寫下文章時的版本,程式碼可能會隨著版本改變。後設資料會隨著 ABI 穩定的到來而變得穩定和可靠,但在到來那時也會容易發生變化。大家在寫日常的 Swift 程式碼時,不要依賴這裡講的一切。如果你想做比 Mirror 所提供的方式更復雜的反射,這裡會給你一些思路。但在 ABI 的穩定前,還需要保持相關變化的關注。如果你想使用 Mirror 本身,這篇文章會提供一些好的思路去做接入和適配。不過再次提醒,這些東西可能會隨著版本而改變。

介面

Mirror(reflecting:) 初始化方法可以接受任意值,返回結果是一個提供該值子元素集合 Children 的相關資訊的例項。一個 Child 由可選的標籤和值構成。可以在編譯期且不用知道任何型別資訊情況下,在 Child 的值上用 Mirror 去遍歷整個物件的層級檢視。 Mirror 允許型別用遵循 CustomReflectable 協議的方式提供一個自定義的表示方式。這給那些想表示得比內建形式更友好的型別提供一種有效的方法。 比如 Array 型別遵守 CustomReflectable 協議並且暴露其中的元素為無標籤的 ChildrenDictionary 使用這種方法暴露其中的鍵值對為帶標籤的 Children。 對於其他型別,Mirror 用魔法去返回一個基於其中的實際子元素的 Children 集合。對於結構體和類,Children 為其中儲存的屬性值。對於元組,Children 為元組的子元素。列舉則是列舉的 case 和其關聯的值(如果有的話)。 這些神奇的魔法是怎麼工作的呢?讓我們一起來了解一下。

程式碼結構

反射的 API 有一部分是用 Swift 實現的,另一部分是用 C++ 實現的。Swift 更適合用在實現更 Swift 的介面,並讓很多工變得更簡單。Swift 的執行時的底層是使用 C++ 實現的,但是在 Swift 中不能直接訪問 C++ 的類,所以有一個 C 的連線層。反射的 Swift 實現在 ReflectionMirror.swift,C++ 實現在 ReflectionMirror.mm。 這兩者通過一小組暴露給 Swift 的 C++ 函式進行通訊的。與其使用 Swift 生成的 C 橋接層,不如將這些函式在 Swift 中直接宣告成指定的自定義符號,而這些名字的 C++ 函式則專門實現為可以被 Swift 直接呼叫的方式。這兩部分的程式碼可以在不關心橋接機制會在幕後如何處理傳遞值的情況下互動,但仍需要準確的知道 Swift 應該如何傳遞引數和返回值。除非你在使用需要它的執行時程式碼,否則別輕易嘗試這些。 舉個例子,讓我們看下在 ReflectionMirror.swift 中的 _getChildCount 函式:

@_silgen_name("swift_reflectionMirror_count")
internal func _getChildCount<T>(_: T, type: Any.Type) -> Int
複製程式碼

@_silgen_name 修飾符會通知 Swift 編譯器將這個函式對映成 swift_reflectionMirror_count 符號,而不是 Swift 通常對應到的 _getChildCount 方法名修飾。需要注意的是,最前面的下劃線表示這個修飾符是被保留在標準庫中的。在 C++ 這邊,這個函式是這樣的:

SWIFT_CC(swift) SWIFT_RUNTIME_STDLIB_INTERFACE
intptr_t swift_reflectionMirror_count(OpaqueValue *value,
                                     const Metadata *type,
                                     const Metadata *T) {
複製程式碼

SWIFT_CC(swift) 會告訴編譯器這個函式使用的是 Swift 的呼叫約定,而不是 C/C++ 的。SWIFT_RUNTIME_STDLIB_INTERFACE 標記這是個函式,在 Swift 側的一部分介面中,而且它還有標記為 extern "C" 的作用從而避免 C++ 的方法名修飾,並確保它在 Swift 側會有預期的符號。同時,C++ 的引數會去特意匹配在 Swift 中宣告的函式呼叫。當 Swift 呼叫 _getChildCount 時,C++ 會用包含的 Swift 值指標的 value,包含型別引數的 type,包含型別相應的範型 <T>T 的函式引數來呼叫此函式。 Mirror 的在 Swift 和 C++ 之間的全部介面由以下函式組成:

@_silgen_name("swift_reflectionMirror_normalizedType")
internal func _getNormalizedType<T>(_: T, type: Any.Type) -> Any.Type
@_silgen_name("swift_reflectionMirror_count")
internal func _getChildCount<T>(_: T, type: Any.Type) -> Int
internal typealias NameFreeFunc = @convention(c) (UnsafePointer<CChar>?) -> Void
@_silgen_name("swift_reflectionMirror_subscript")
internal func _getChild<T>(
 of: T,
 type: Any.Type,
 index: Int,
 outName: UnsafeMutablePointer<UnsafePointer<CChar>?>,
 outFreeFunc: UnsafeMutablePointer<NameFreeFunc?>
) -> Any
// Returns 'c' (class), 'e' (enum), 's' (struct), 't' (tuple), or '\0' (none)
@_silgen_name("swift_reflectionMirror_displayStyle")
internal func _getDisplayStyle<T>(_: T) -> CChar
@_silgen_name("swift_reflectionMirror_quickLookObject")
internal func _getQuickLookObject<T>(_: T) -> AnyObject?
@_silgen_name("_swift_stdlib_NSObject_isKindOfClass")
internal func _isImpl(_ object: AnyObject, kindOf: AnyObject) -> Bool
複製程式碼

神奇的動態派發

沒有一種單一、通用的方式去獲取任意型別中我們想要的資訊。元組、結構、類和列舉都需要不同的程式碼去完成這些繁多的任務,比如說查詢子元素的數量。其中還有一些更深、微妙的不同之處,比如對 Swift 和 Objective-C 的類的不同處理。 所有的這些函式因為需要不同型別的檢查而需要派發不同的實現程式碼。這聽起來有點像動態方法派發,除了選擇哪種實現去呼叫比檢查物件型別所使用的方法更復雜之外。這些反射程式碼嘗試去簡化使用包含 C++ 版本資訊的介面的抽象基類,還有一大堆包含各種各樣情況的子類進行 C++ 的動態派發。一個單獨的函式會將一個 Swift 型別對映成一個其中的 C++ 類的例項。在一個例項上呼叫一個方法然後派發合適的實現。 對映的函式叫做 call,宣告是這樣的:

template<typename F>
auto call(OpaqueValue *passedValue, const Metadata *T, const Metadata *passedType,
         const F &f) -> decltype(f(nullptr))
複製程式碼

passedValue 是實際需要傳入的Swift的值的指標。T 是該值得靜態型別,對應 Swift 中的範型引數 <T>passedType 是被顯式傳遞進 Swift 側並且會實際應用在反射過程中的型別(這個型別和在使用 Mirror 作為父類的例項在實際執行時的物件型別不一樣)。最後,f 引數會傳遞這個函式查詢到的會被呼叫的實現的物件引用。然後這個函式會返回當這個 f 引數呼叫時的返回值,可以讓使用者更方便的獲得返回值。 call 的實現並沒有想象中那麼令人激動。主要是一個大型的 switch 宣告和一些額外的程式碼去處理特殊的情況。重要的是它會用一個 ReflectionMirrorImpl 的子類例項去結束呼叫 f,然後會呼叫這個例項上的方法去讓真正的工作完成。 這是 ReflectionMirrorImpl,介面的所有東西都要傳入:

struct ReflectionMirrorImpl {
 const Metadata *type;
 OpaqueValue *value;
  virtual char displayStyle() = 0;
 virtual intptr_t count() = 0;
 virtual AnyReturn subscript(intptr_t index, const char **outName,
                             void (**outFreeFunc)(const char *)) = 0;
 virtual const char *enumCaseName() { return nullptr; }
#if SWIFT_OBJC_INTEROP
 virtual id quickLookObject() { return nil; }
#endif
  virtual ~ReflectionMirrorImpl() {}
};
複製程式碼

作用在 Swift 和 C++ 元件之間的介面函式就會用 call 去呼叫相應的方法。比如,swift_reflectionMirror_count 是這樣的:

SWIFT_CC(swift) SWIFT_RUNTIME_STDLIB_INTERFACE
intptr_t swift_reflectionMirror_count(OpaqueValue *value,
                                     const Metadata *type,
                                     const Metadata *T) {
 return call(value, T, type, [](ReflectionMirrorImpl *impl) {
   return impl->count();
 });
}
複製程式碼

元組的反射

先看看元組的反射,應該是最簡單的一種了,但還是做了不少工作。它一開始會返回 't' 的顯示樣式來表明這是一個元組:

struct TupleImpl : ReflectionMirrorImpl {
 char displayStyle() {
   return 't';
 }
複製程式碼

雖然用硬編碼的常量看起來不是很常見,不過這樣做可以完全在同一個地方給 C++ 和 Swift 這個值的引用,並且他們不需要使用橋接層進行互動,這還算是一個合理的選擇。 接下來是 count 方法。此時我們知道 type 實際上是一個 TupleTypeMetadata 型別的指標而不僅僅是一個 Metadata 型別的指標。TupleTypeMetadata 有一個記錄元組的元素數量的 NumElements 欄位,然後這個方法就完成了:

intptr_t count() {
   auto *Tuple = static_cast<const TupleTypeMetadata *>(type);
   return Tuple->NumElements;
 }
複製程式碼

subscript 方法會做更多一點的工作。它也從一樣的的 static_cast 函式開始:

AnyReturn subscript(intptr_t i, const char **outName,
                     void (**outFreeFunc)(const char *)) {
   auto *Tuple = static_cast<const TupleTypeMetadata *>(type);
複製程式碼

接下來,會有一個邊界檢查避免呼叫者請求了這個元組不存在的索引:

if (i < 0 || (size_t)i > Tuple->NumElements)
     swift::crash("Swift mirror subscript bounds check failure");
複製程式碼

下標有兩個作用:可以檢索元素和對應的名字。對於一個結構體或者類來說,這個名字就是所儲存的屬性名。而對於元組來說,這個名字要麼是該元素的元組標籤,要麼在沒有標籤的情況下就是一個類似 .0 的數值指示器。 標籤以一個用空格做間隔的列表儲存,放在後設資料的 Labels 欄位中。這段程式碼查詢列表中的第 i 個字串:

   // 確定是否有一個標籤
   bool hasLabel = false;
   if (const char *labels = Tuple->Labels) {
     const char *space = strchr(labels, ' ');
     for (intptr_t j = 0; j != i && space; ++j) {
       labels = space + 1;
       space = strchr(labels, ' ');
     }
      // If we have a label, create it.
     if (labels && space && labels != space) {
       *outName = strndup(labels, space - labels);
       hasLabel = true;
     }
   }
複製程式碼

如果在沒有標籤的情況下,建立一個合適的數值指示器作為名字:

   if (!hasLabel) {
     // The name is the stringized element number '.0'.
     char *str;
     asprintf(&str, ".%" PRIdPTR, i);
     *outName = str;
   }
複製程式碼

因為要將 Swift 和 C++ 交叉使用,所以不能享受一些方便的特性比如自動記憶體管理。Swift 有 ARC,C++ 有 RALL, 但是這兩種技術沒辦法相容。outFreeFunc 允許 C++ 的程式碼提供一個函式給呼叫者用來釋放返回的名字。標籤需要使用 free 進行釋放,所以設定給 *outFreeFunc 相應的值如下:

*outFreeFunc = [](const char *str) { free(const_cast<char *>(str)); };
複製程式碼

值得注意的是名字,但令人驚訝的是,這個值檢索起來很簡單。Tuple 後設資料包含了一個可以用索引去獲取元素的相關資訊的返回的函式:

auto &elt = Tuple->getElement(i);
複製程式碼

elt 包含了一個偏移值,可以應用在元組值上,去獲得元素的值指標:

   auto *bytes = reinterpret_cast<const char *>(value);
   auto *eltData = reinterpret_cast<const OpaqueValue *>(bytes + elt.Offset);
複製程式碼

elt 還包含了元素的型別。可以通過型別和值的指標,去構造一個包括這個值新的 Any 物件。這個型別有可以分配記憶體並初始化包含給定型別的值的儲存欄位的函式指標。用這些函式拷貝值為 Any 型別的物件,然後返回 Any 給呼叫者。程式碼是這樣的:

   Any result;
    result.Type = elt.Type;
   auto *opaqueValueAddr = result.Type->allocateBoxForExistentialIn(&result.Buffer);
   result.Type->vw_initializeWithCopy(opaqueValueAddr,
                                      const_cast<OpaqueValue *>(eltData));
    return AnyReturn(result);
 }
};
複製程式碼

這就是元組的做法。

swift_getFieldAt

在結構、類和列舉中查詢元素目前來說相當複雜。造成這麼複雜的主要原因是,這些型別和包含這些型別相關資訊的欄位的欄位描述符之間缺少直接的引用關係。有一個叫 swift_getField 的幫助函式可以查詢給定型別相應的欄位描述符。一但我們新增了那個直接的引用,這整個函式應該就沒啥作用了,但在同一時刻,它提供了執行時程式碼怎麼能做到用語言的後設資料去查詢型別資訊的一個有趣思路。 這個函式原型是這樣的:

void swift::_swift_getFieldAt(
   const Metadata *base, unsigned index,
   std::function<void(llvm::StringRef name, FieldType fieldInfo)>
       callback) {
複製程式碼

它會用型別去檢查,用欄位的索引去查詢,還有一個會被在資訊找到時回撥。 首先就是獲取型別的型別上下文描述,包含著更進一步將會被使用的型別的資訊:

 auto *baseDesc = base->getTypeContextDescriptor();
 if (!baseDesc)
   return;
複製程式碼

這個工作會分為兩個部分。第一步查詢型別的欄位描述符。欄位描述符包括所有有關這個型別的欄位資訊。一旦欄位描述符可用,這個函式可以從描述符中查詢所需要的資訊。 從描述符中查詢資訊被封裝成一個叫 getFieldAt 的幫助方法, 可以讓各種各樣地方的其它程式碼查詢到合適的欄位描述符。讓我們看下這個查詢過程。它從獲取一個用來將符號還原器開始,將符號修飾過的類名還原為實際的型別引用:

 auto dem = getDemanglerForRuntimeTypeResolution();
複製程式碼

會用快取來加快多次的查詢:

 auto &cache = FieldCache.get();
複製程式碼

如果快取中已經有欄位描述符,呼叫 getFieldAt 來獲得:

 if (auto Value = cache.FieldCache.find(base)) {
   getFieldAt(*Value->getDescription());
   return;
 }
複製程式碼

為了讓查詢的程式碼更簡單,有一個可以檢查 FieldDescriptor 是否是被查詢的那一個的幫助方法。如果描述符匹配,那麼描述符放入快取中,呼叫 getFieldAt ,然後返回成功給呼叫者。匹配的過程是複雜的,不過本質上歸納起來就是去匹配符號修飾的名字:

 auto isRequestedDescriptor = [&](const FieldDescriptor &descriptor) {
   assert(descriptor.hasMangledTypeName());
   auto mangledName = descriptor.getMangledTypeName(0);
    if (!_contextDescriptorMatchesMangling(baseDesc,
                                          dem.demangleType(mangledName)))
     return false;
    cache.FieldCache.getOrInsert(base, &descriptor);
   getFieldAt(descriptor);
   return true;
 };
複製程式碼

欄位描述符可用在執行時註冊或在編譯時放進二進位制。這兩個迴圈查詢在匹配中所有已知的的欄位描述符:

 for (auto &section : cache.DynamicSections.snapshot()) {
   for (const auto *descriptor : section) {
     if (isRequestedDescriptor(*descriptor))
       return;
   }
 }
  for (const auto &section : cache.StaticSections.snapshot()) {
   for (auto &descriptor : section) {
     if (isRequestedDescriptor(descriptor))
       return;
   }
 }
複製程式碼

當發現沒有匹配時,記錄一個警告資訊並且在回撥返回一個空元組(僅僅為了給一個回撥):

 auto typeName = swift_getTypeName(base, /*qualified*/ true);
 warning(0, "SWIFT RUNTIME BUG: unable to find field metadata for type '%*s'\n",
            (int)typeName.length, typeName.data);
 callback("unknown",
          FieldType()
            .withType(TypeInfo(&METADATA_SYM(EMPTY_TUPLE_MANGLING), {}))
            .withIndirect(false)
            .withWeak(false));
}
複製程式碼

值得注意的是欄位描述符的查詢過程。getFieldAt 幫助方法將欄位描述符轉化為名字和回撥中返回的欄位型別。開始它會從欄位描述符中請求欄位的引用:

 auto getFieldAt = [&](const FieldDescriptor &descriptor) {
   auto &field = descriptor.getFields()[index];
複製程式碼

名字可以直接獲得在這個引用中訪問到:

   auto name = field.getFieldName(0);
複製程式碼

如果這個欄位實際上是一個列舉,那麼就可能沒有型別。先做這種檢查,並執行回撥:

   if (!field.hasMangledTypeName()) {
     callback(name, FieldType().withIndirect(field.isIndirectCase()));
     return;
   }
複製程式碼

欄位的引用將欄位型別儲存為一個符號修飾的名字。因為回撥預期的是後設資料的指標,所以符號修飾的名字必須被轉化為一個真實的型別。_getTypeByMangledName 函式處理了大部分工作,不過需要呼叫者解決這個型別用的所有範型引數。這個工作需要將這個型別的所有範型的上下文抽離出來:

   std::vector<const ContextDescriptor *> descriptorPath;
   {
     const auto *parent = reinterpret_cast<
                             const ContextDescriptor *>(baseDesc);
     while (parent) {
       if (parent->isGeneric())
         descriptorPath.push_back(parent);
        parent = parent->Parent.get();
     }
   }
複製程式碼

現在獲得了符號修飾的名字和型別,將它們傳入一個 Lambda 表示式來解決範型引數:

   auto typeName = field.getMangledTypeName(0);
    auto typeInfo = _getTypeByMangledName(
       typeName,
       [&](unsigned depth, unsigned index) -> const Metadata * {
複製程式碼

如果請求的深度比描述符的路徑大小還大,那麼就會失敗:

         if (depth >= descriptorPath.size())
           return nullptr;
複製程式碼

除此之外,還有從欄位的型別中獲取範型引數。這需要將索引和深度轉化為單獨的扁平化的索引,通過遍歷描述符的路徑,在每個階段新增範型引數的數量直到達到深度為止:

         unsigned currentDepth = 0;
         unsigned flatIndex = index;
         const ContextDescriptor *currentContext = descriptorPath.back();
          for (const auto *context : llvm::reverse(descriptorPath)) {
           if (currentDepth >= depth)
             break;
            flatIndex += context->getNumGenericParams();
           currentContext = context;
           ++currentDepth;
         }
複製程式碼

如果索引比範型引數可達到的深度大,那麼失敗:

         if (index >= currentContext->getNumGenericParams())
           return nullptr;
複製程式碼

除此之外,從基本型別中獲得合適的範型引數:

         return base->getGenericArgs()[flatIndex];
       });
複製程式碼

像之前那樣,如果不能找到型別,就用空元組:

   if (typeInfo == nullptr) {
     typeInfo = TypeInfo(&METADATA_SYM(EMPTY_TUPLE_MANGLING), {});
     warning(0, "SWIFT RUNTIME BUG: unable to demangle type of field '%*s'. "
                "mangled type name is '%*s'\n",
                (int)name.size(), name.data(),
                (int)typeName.size(), typeName.data());
   }
複製程式碼

然後執行回撥,無論找到了什麼:

   callback(name, FieldType()
                      .withType(typeInfo)
                      .withIndirect(field.isIndirectCase())
                      .withWeak(typeInfo.isWeak()));
  };
複製程式碼

這就是 swift_getFieldAt。我們帶著這個幫助方法看看其他反射的實現。

結構體的反射

結構體的實現也是類似的,但稍微有點複雜。這是因為有些結構體型別不完全支援反射,查詢名字和偏移值要花費更多力氣,而且結構體可能包含需要反射程式碼去提取的弱引用。 首先是一個幫助方法去檢查結構體是否完全支援反射。結構體後設資料裡儲存這樣一個可被訪問的標誌位。跟上面元組的程式碼類似,可以知道 type 實際上是一個 StructMetadata 指標,所以我們可以自由的傳入:

struct StructImpl : ReflectionMirrorImpl {
 bool isReflectable() {
   const auto *Struct = static_cast<const StructMetadata *>(type);
   const auto &Description = Struct->getDescription();
   return Description->getTypeContextDescriptorFlags().isReflectable();
 }
複製程式碼

結構體的顯示樣式是 s :

 char displayStyle() {
   return 's';
 }
複製程式碼

子元素的數量是後設資料給出的欄位的數量,也可能是 0(如果這個型別實際上不能支援反射的話):

 intptr_t count() {
   if (!isReflectable()) {
     return 0;
   }
    auto *Struct = static_cast<const StructMetadata *>(type);
   return Struct->getDescription()->NumFields;
 }
複製程式碼

像之前那樣,subscript 方法是比較複雜的部分。它開始也是類似的,做邊界檢查和查詢偏移值:

 AnyReturn subscript(intptr_t i, const char **outName,
                     void (**outFreeFunc)(const char *)) {
   auto *Struct = static_cast<const StructMetadata *>(type);
    if (i < 0 || (size_t)i > Struct->getDescription()->NumFields)
     swift::crash("Swift mirror subscript bounds check failure");
    // Load the offset from its respective vector.
   auto fieldOffset = Struct->getFieldOffsets()[i];
複製程式碼

從結構體欄位中獲取型別資訊會更復雜一點。這項工作通過 _swift_getFieldAt 幫助方法進行:

   Any result;
    _swift_getFieldAt(type, i, [&](llvm::StringRef name, FieldType fieldInfo) {
複製程式碼

一但它有欄位資訊,一切就會進行得和元組對應部分的程式碼類似。填寫名字和計算欄位儲存的指標:

     *outName = name.data();
     *outFreeFunc = nullptr;
      auto *bytes = reinterpret_cast<char*>(value);
     auto *fieldData = reinterpret_cast<OpaqueValue *>(bytes + fieldOffset);
複製程式碼

這裡有一個額外的步驟去拷貝欄位的值到 Any 型別的返回值來處理弱引用。loadSpecialReferenceStorage 方法處理這種情況。如果值沒有被載入的話那麼那個值用普通的儲存,並且以普通的方式拷貝到返回值:

     bool didLoad = loadSpecialReferenceStorage(fieldData, fieldInfo, &result);
     if (!didLoad) {
       result.Type = fieldInfo.getType();
       auto *opaqueValueAddr = result.Type->allocateBoxForExistentialIn(&result.Buffer);
       result.Type->vw_initializeWithCopy(opaqueValueAddr,
                                          const_cast<OpaqueValue *>(fieldData));
     }
   });
    return AnyReturn(result);
 }
};
複製程式碼

這些就是結構體值得注意的了。

類的反射

類和結構體很類似,在 ClassImpl 裡的程式碼幾乎是相同的。在操作 Objective-C 上有兩點值得注意的不同之處。一個是 quickLookObject 的實現,會調起 Objective-C 的 debugQuickLookObject 方法的:

#if SWIFT_OBJC_INTEROP
id quickLookObject() {
 id object = [*reinterpret_cast<const id *>(value) retain];
 if ([object respondsToSelector:@selector(debugQuickLookObject)]) {
   id quickLookObject = [object debugQuickLookObject];
   [quickLookObject retain];
   [object release];
   return quickLookObject;
 }
  return object;
}
#endif
複製程式碼

另一個是如果該類的父類是 Objective-C 的類,欄位的偏移值需要在 Objective-C 執行時獲得:

 uintptr_t fieldOffset;
 if (usesNativeSwiftReferenceCounting(Clas)) {
   fieldOffset = Clas->getFieldOffsets()[i];
 } else {
#if SWIFT_OBJC_INTEROP
   Ivar *ivars = class_copyIvarList((Class)Clas, nullptr);
   fieldOffset = ivar_getOffset(ivars[i]);
   free(ivars);
#else
   swift::crash("Object appears to be Objective-C, but no runtime.");
#endif
 }
複製程式碼

列舉的反射

列舉有一些不同之處。Mirror 會考慮一個列舉例項最多隻包含一個元素,列舉 case 名字作為標籤,它的關聯值作為值。沒有關聯值的 case 沒有包含的元素。 舉個例子:

enum Foo {
 case bar
 case baz(Int)
 case quux(String, String)
}
複製程式碼

Foo 型別的值使用 mirror 時,mirror 會顯示 Foo.bar 沒有子元素,Foo.baz 有一個 Int 型別的元素,Foo.quux 有一個 (String, String) 型別的元素。相同的子標籤和型別的類和結構體的值有著相同欄位,但同一個型別的不同的列舉 case 不是這樣的。關聯的值也可能是間接的,所以需要一些特殊處理。 enum 的反射需要四部分核心的資訊:case 的名字,tag(表示該值儲存的列舉 case 的數字),payload 的型別,是否是間接的 payload。getInfo 方法獲取這些值:

const char *getInfo(unsigned *tagPtr = nullptr,
                   const Metadata **payloadTypePtr = nullptr,
                   bool *indirectPtr = nullptr) {
複製程式碼

tag 從請求後設資料直接檢索而來:

 unsigned tag = type->vw_getEnumTag(value);
複製程式碼

其它資訊用 _swift_getFieldAt 檢索而來。將 tag 作為欄位索引來呼叫,就會提供合適的資訊:

 const Metadata *payloadType = nullptr;
 bool indirect = false;
  const char *caseName = nullptr;
 _swift_getFieldAt(type, tag, [&](llvm::StringRef name, FieldType info) {
   caseName = name.data();
   payloadType = info.getType();
   indirect = info.isIndirect();
 });
複製程式碼

所有的值會返回給呼叫者:

 if (tagPtr)
   *tagPtr = tag;
 if (payloadTypePtr)
   *payloadTypePtr = payloadType;
 if (indirectPtr)
   *indirectPtr = indirect;
  return caseName;
}
複製程式碼

(你可能會好奇:為什麼只有 case 的名字是直接返回的,而其它的三個資訊用指標返回?為什麼不返回 tag 或者 payload 的型別?答案是:我真的不知道,可能在那個時機看起來是個好主意) count 方法可以用 getInfo 方法去檢索 payload 的型別,並返回 0 或 1 表示 payload 型別是否為 null:

intptr_t count() {
 if (!isReflectable()) {
   return 0;
 }
  const Metadata *payloadType;
 getInfo(nullptr, &payloadType, nullptr);
 return (payloadType != nullptr) ? 1 : 0;
}
複製程式碼

subscript方法開始會獲取所有有關這個值的資訊:

AnyReturn subscript(intptr_t i, const char **outName,
                   void (**outFreeFunc)(const char *)) {
 unsigned tag;
 const Metadata *payloadType;
 bool indirect;
  auto *caseName = getInfo(&tag, &payloadType, &indirect);
複製程式碼

實際的複製值需要更多的工作。為了處理間接的值,整個過程在一個額外的 box 中進行:

 const Metadata *boxType = (indirect ? &METADATA_SYM(Bo).base : payloadType);
 BoxPair pair = swift_allocBox(boxType);
複製程式碼

間接的情況下,真實值要在 box 中取出:

 if (indirect) {
   const HeapObject *owner = *reinterpret_cast<HeapObject * const *>(value);
   value = swift_projectBox(const_cast<HeapObject *>(owner));
 }
複製程式碼

現在一切都準備好了。給 case 名字設定子標籤:

 *outName = caseName;
 *outFreeFunc = nullptr;
複製程式碼

似曾相識的方式被用在將 payload 返回為 Any 型別的物件:

 Any result;
  result.Type = payloadType;
 auto *opaqueValueAddr = result.Type->allocateBoxForExistentialIn(&result.Buffer);
 result.Type->vw_initializeWithCopy(opaqueValueAddr,
                                    const_cast<OpaqueValue *>(value));
  swift_release(pair.object);
 return AnyReturn(result);
}
複製程式碼

其餘種類

檔案中還有三種其他的實現,每種幾乎都沒做什麼事情。ObjCClassImpl 處理 Objective-C 的類。它甚至不去嘗試返回任何子元素,因為 Objective-C 在 ivars 的內容上允許太多種補救方案了。Objective-C 的類允許保持野指標一直存在,並需要單獨的邏輯讓實現不要去碰那個值。因為這樣的值嘗試作為 Mirror 子元素返回,會違反 Swift 的安全性保證。因為沒有辦法可靠地去告知應該如何處理如果值出了問題,所以程式碼避開處理整個這種情況。 MetatypeImpl 處理元型別。如果將 Mirror 用在實際的型別,比如這樣用 Mirror(reflecting:String.self),這時就會用到它。第一反應是,它會在這時提供一些有用的資訊。但實際上它僅僅返回空,甚至沒有去嘗試獲取任何東西。同樣的,OpaqueImpl 處理不透明的型別並返回空。

Swift 側介面

在 Swift 側,Mirror 呼叫在 C++ 側實現的介面函式,去檢索需要的資訊,然後以更友好的方式去展現。這些會在 Mirror 的初始化器中完成:

internal init(internalReflecting subject: Any,
           subjectType: Any.Type? = nil,
           customAncestor: Mirror? = nil)
{
複製程式碼

subjectType 是將要被反射 subject 的值的型別。這通常是值的執行時型別,但如果呼叫者用 superclassMirror 去找到上面的類的層級,它可以是父類。如果呼叫者不傳 入subjectType,程式碼會問 C++ 側的程式碼要 subject 的型別:

 let subjectType = subjectType ?? _getNormalizedType(subject, type: type(of: subject))
複製程式碼

然後它就會獲取子元素的數量,建立一個稍後獲取每個子元素個體的集合來構建構建 children 物件:

 let childCount = _getChildCount(subject, type: subjectType)
 let children = (0 ..< childCount).lazy.map({
   getChild(of: subject, type: subjectType, index: $0)
 })
 self.children = Children(children)
複製程式碼

getChild 函式是 C++ 的 _getChild 函式的簡單封裝,將標籤名字中包含的 C 字串轉換成 Swift 字串。 Mirror 有一個 superclassMirror 屬性,會返回檢查過類的層級結構裡上一層的類的屬性的 Mirror 物件。在內部,它有一個 _makeSuperclassMirror 屬性儲存著一個按需求構建父類的 Mirror 的閉包。閉包一開始會獲取 subjectType 的父類。非類的型別和沒有父類的類沒有父類的 Mirror,所以他們會獲取到 nil:

 self._makeSuperclassMirror = {
   guard let subjectClass = subjectType as? AnyClass,
         let superclass = _getSuperclass(subjectClass) else {
     return nil
   }
複製程式碼

呼叫者可以用一個可作為父類 Mirror 直接返回的 Mirror 例項來指定自定義的祖先的表現:

   if let customAncestor = customAncestor {
     if superclass == customAncestor.subjectType {
       return customAncestor
     }
     if customAncestor._defaultDescendantRepresentation == .suppressed {
       return customAncestor
     }
   }
複製程式碼

除此之外,給相同值返回一個將 superclass 作為 subjectType 的新 Mirror

   return Mirror(internalReflecting: subject,
                 subjectType: superclass,
                 customAncestor: customAncestor)
 }
複製程式碼

最後,它獲取並解析顯示的樣式,並設定 Mirror 的剩下的屬性:

   let rawDisplayStyle = _getDisplayStyle(subject)
   switch UnicodeScalar(Int(rawDisplayStyle)) {
   case "c": self.displayStyle = .class
   case "e": self.displayStyle = .enum
   case "s": self.displayStyle = .struct
   case "t": self.displayStyle = .tuple
   case "\0": self.displayStyle = nil
   default: preconditionFailure("Unknown raw display style '\(rawDisplayStyle)'")
   }
 
   self.subjectType = subjectType
   self._defaultDescendantRepresentation = .generated
 }
複製程式碼

結論

Swift 豐富的後設資料型別大多數在幕後存在,為像協議一致性檢查和泛型型別解決這樣的事提供支援。其中某些通過 Mirror 類 型暴露給使用者,從而允許在執行時檢查任意值。對於靜態型別的 Swift 生態來說,這種方式一開始看起來有點奇怪和神祕,但根據已經存在的資訊來看,它其實是個簡單直接的應用。這個實現的探索旅程應該會幫助大家瞭解神祕之處,並在使用 Mirror 時可以意識到背後正在進行著什麼。

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg