Swift 5 Type Metadata 詳解

背景

Swift 5 出了,主要是 ABI 穩定了,從 ABI Dashboard 來看爲了解決 ABI 穩定問題,對 type metadata 也有很多改動。衆所周知,咱們 App 的 JSON 庫 HandyJSON 是強依賴 metadata 結構的,若是 metadata 有大規模的改動可能直接致使這個庫徹底不能用,本着早發現早治療的心態我趕快下載了 Xcode 10.2 beta,一跑果真編不過了,沒辦法只好本身着手來解決問題了。git

Metadata 的結構演進

爲了便於理解,先畫個圖看一下 metadata 的具體結構,每一格代碼一個指針長度,這是 64 位系統下的 metadata 結構,32 位系統下 nominal type descriptor 的偏移在 11 個指針長度的位置,官方文檔裏有詳細的說明。
github

image.png

Swift type metadata 的結構其實並無明顯的變化,而其中的 nominal type descriptor 結構卻經歷了一系列的變化。

Swift 4.2 之前

在 Swift 4.2 (不包括 4.2)之前的結構是這樣的:swift

![](https://user-gold-cdn.xitu.io/2019/2/26/16929595b0015018?w=490&h=734&f=png&s=61393)
struct _NominalTypeDescriptor {
    var mangledName: Int32
    var numberOfFields: Int32
    var fieldOffsetVector: Int32
    var fieldNames: Int32
    var fieldTypesAccessor: Int32
}
複製代碼

nominal type descriptor 包含了屬性的名字和訪問屬性的類型信息的函數,HandyJSON 最初的原理就是從 nominal type descriptor 中取得屬性的類型信息而後把 JSON 字串裏的相應值賦進去,因爲 fieldTypeAccessor 符合 c 的 calling convention,把指針強轉一下就能得到類型信息:數組

var fieldTypes: [Any.Type]? {
    guard let nominalTypeDescriptor = self.nominalTypeDescriptor else {
        return nil
    }
    guard let function = nominalTypeDescriptor.fieldTypesAccessor else { return nil }
    return (0..<nominalTypeDescriptor.numberOfFields).map {
        return unsafeBitCast(function(UnsafePointer<Int>(pointer)).advanced(by: $0).pointee, to: Any.Type.self)
    }
}
複製代碼

Swift 4.2

Swift 4.2 對 nominal type descriptor 作了調整,struct 和 class 結構變得有所不一樣,乍看沒有少什麼東西,其實對 fieldTypesAccessor 這個函數作了修改,再也不符合 c 的 calling convention,所以不能夠再從 nominal type descriptor 獲取類型信息。bash

struct _StructContextDescriptor: _ContextDescriptorProtocol {
    var flags: Int32
    var parent: Int32
    var mangledName: Int32
    var fieldTypesAccessor: Int32
    var numberOfFields: Int32
    var fieldOffsetVector: Int32
}

struct _ClassContextDescriptor: _ContextDescriptorProtocol {
    var flags: Int32
    var parent: Int32
    var mangledName: Int32
    var fieldTypesAccessor: Int32
    var superClsRef: Int32
    var reservedWord1: Int32
    var reservedWord2: Int32
    var numImmediateMembers: Int32
    var numberOfFields: Int32
    var fieldOffsetVector: Int32
}
複製代碼

儘管蘋果但願咱們用 Mirror 來作反射,可是其實 Mirror 至今爲止都不包含屬性的類型的信息,所以蘋果留了一個臨時接口 swift_getFieldAt 來幫助咱們獲取類型信息:app

@_silgen_name("swift_getFieldAt")
func _getFieldAt( _ type: Any.Type, _ index: Int, _ callback: @convention(c) (UnsafePointer<CChar>, UnsafeRawPointer, UnsafeMutableRawPointer) -> Void,
    _ ctx: UnsafeMutableRawPointer
)
複製代碼

爲何說是臨時的呢,由於 Swift 5 的時候就發現這個接口沒了。。。。函數

Swift 5.0

到了 Swift 5.0 的時候,前面已經說過了獲取類型的那個接口沒了,那麼咱們只好翻出 Swift 的源碼來找找思路了,
找到 TypeContextDescriptorBuilderBase 類的 layout() 方法:工具

void layout() {
  asImpl().computeIdentity();

  super::layout();
  asImpl().addName();
  asImpl().addAccessFunction();
  asImpl().addReflectionFieldDescriptor();
  asImpl().addLayoutInfo();
  asImpl().addGenericSignature();
  asImpl().maybeAddResilientSuperclass();
  asImpl().maybeAddMetadataInitialization();
}
複製代碼

按源碼寫出 nominal type descriptor 的結構以下:ui

struct _StructContextDescriptor: _ContextDescriptorProtocol {
    var flags: Int32
    var parent: Int32
    var mangledNameOffset: Int32
    var fieldTypesAccessor: Int32
    var reflectionFieldDescriptor: Int32
    var numberOfFields: Int32
    var fieldOffsetVector: Int32
}

struct _ClassContextDescriptor: _ContextDescriptorProtocol {
    var flags: Int32
    var parent: Int32
    var mangledNameOffset: Int32
    var fieldTypesAccessor: Int32
    var reflectionFieldDescriptor: Int32
    var superClsRef: Int32
    var metadataNegativeSizeInWords: Int32
    var metadataPositiveSizeInWords: Int32
    var numImmediateMembers: Int32
    var numberOfFields: Int32
    var fieldOffsetVector: Int32
}
複製代碼

雖然 fieldTypesAccessor 仍是沒法調用,可是咱們發現這裏多了一個 reflectionFieldDescriptor 指針,直覺告訴我辦法應該在這個東西里面,因此先看下這個東西是什麼結構:this

void addReflectionFieldDescriptor() {
  ....
    
  B.addRelativeAddress(IGM.getAddrOfReflectionFieldDescriptor(
    getType()->getDeclaredType()->getCanonicalType()));
}
複製代碼

邏輯基本就是拿到 ReflectionFieldDescriptor 的地址,而後把地址放到相應的內存裏,須要注意的是這裏放的是一個相對的地址,RelativePointer 的註釋中寫道:

// A reference can be absolute or relative: // //   - An absolute reference is a pointer to the object. // //   - A relative reference is a (signed) offset from the address of the //     reference to the address of its direct referent.

相對引用指的是相對當前引用指針地址的偏移量,因而咱們有了獲取 ReflectionFieldDescriptor 地址的方法:

var reflectionFieldDescriptor: FieldDescriptor? {
    guard let contextDescriptor = self.contextDescriptor else {
        return nil
    }
    let pointer = UnsafePointer<Int>(self.pointer)
    let base = pointer.advanced(by: contextDescriptorOffsetLocation)
    let offset = contextDescriptor.reflectionFieldDescriptor
    let address = base.pointee + 4 * 4 // (4 properties in front) * (sizeof Int32)
    guard let fieldDescriptorPtr = UnsafePointer<_FieldDescriptor>(bitPattern: address + offset) else {
        return nil
    }
    return FieldDescriptor(pointer: fieldDescriptorPtr)
}
複製代碼

拿到了地址,咱們還須要知道 FieldDescriptor 這個結構是什麼樣子的,咱們找到 FieldDescriptor 這個類:

// Field descriptors contain a collection of field records for a single
// class, struct or enum declaration.
class FieldDescriptor {
  const FieldRecord *getFieldRecordBuffer() const {
    return reinterpret_cast<const FieldRecord *>(this + 1);
  }

  const RelativeDirectPointer<const char> MangledTypeName;
  const RelativeDirectPointer<const char> Superclass;

public:
  FieldDescriptor() = delete;

  const FieldDescriptorKind Kind;
  const uint16_t FieldRecordSize;
  const uint32_t NumFields;

  using const_iterator = FieldRecordIterator;
  
  ....
}
複製代碼

FieldDescriptor 的結構裏有一個 FieldRecord 的數組,從名字看裏面應該保存了類型信息,咱們再翻出 FieldRecord 的源碼:

class FieldRecord {
  const FieldRecordFlags Flags;
  const RelativeDirectPointer<const char> MangledTypeName;
  const RelativeDirectPointer<const char> FieldName;
  ....
}
複製代碼

很遺憾 FieldRecord 並無直接保存類型信息,只有一個 MangledTypeName ,問題不大,咱們還有一個叫 swift_getTypeByMangledNameInContext 的函數,這個函數背後調用的 swift_getTypeByMangledName 函數與以前的 getFieldAt 內部調用的是同一個函數,返回是 Any.Type:

@_silgen_name("swift_getTypeByMangledNameInContext")
public func _getTypeByMangledNameInContext(
    _ name: UnsafePointer<UInt8>,
    _ nameLength: Int,
    genericContext: UnsafeRawPointer?,
    genericArguments: UnsafeRawPointer?)
    -> Any.Type?
複製代碼

至此咱們解決了由 Swift 5.0 metadata 變更致使的災難性編譯問題,順便把 metadata 結構梳理了一下,代碼已經提交到了 dev_for_swift5.0 分支。

後記

Mirror 是官方支持的反射工具,使用 Metadata 這種辦法算是一種非主流的作法,可是蘋果也意識到 Mirror 裏面有部分信息沒法提供,聽說是技術上有一點困難因此暫時無法把類型信息等放到 Mirror 裏面,因此纔在 Metadata 裏增長了用於反射的信息,ABI Dashboard 裏也說 ABI 穩定的優先級高於完整的反射功能,可見 Metadata 這一部分的結構暫時不會大改了,可是遠期來看蘋果仍是會在 Mirror 裏面完整支持反射功能。

相關資料

swift.org/abi-stabili…
github.com/apple/swift…
github.com/apple/swift…
github.com/alibaba/Han…

相關文章
相關標籤/搜索