Swift 5 Type Metadata 詳解

chantu發表於2019-02-26

背景

Swift 5 出了,主要是 ABI 穩定了,從 ABI Dashboard 來看為了解決 ABI 穩定問題,對 type metadata 也有不少改動。眾所周知,我們 App 的 JSON 庫 HandyJSON 是強依賴 metadata 結構的,如果 metadata 有大規模的改動可能直接導致這個庫完全不能用,本著早發現早治療的心態我趕快下載了 Xcode 10.2 beta,一跑果然編不過了,沒辦法只好自己著手來解決問題了。

Metadata 的結構演進

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

image.png

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

Swift 4.2 以前

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

![](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 獲取型別資訊。

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 來幫助我們獲取型別資訊:

@_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 的結構如下:

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 指標,直覺告訴我辦法應該在這個東西里面,所以先看下這個東西是什麼結構:

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…

相關文章