background

Swift 5 is out, mainly due to ABI stability. According to ABI Dashboard, there are some changes to type metadata to address ABI stability issues. As we all know, HandyJSON, the JSON library of our App, is strongly dependent on the metadata structure. If metadata is changed on a large scale, the library may be completely unusable. In the spirit of early discovery and early treatment, I downloaded Xcode 10.2 beta as soon as possible. A run really did not make up, have no way to begin to solve the problem.

Metadata structure evolution

To make it easier to understand, let’s draw a graph to see the specific structure of metadata. Each block of code has a pointer length. This is the metadata structure in 64-bit system. There are detailed instructions in the official documentation.



Swift before 4.2

Prior to Swift 4.2 (not including 4.2) the structure looked like this:

! [](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
}
Copy the code

The nominal Type Descriptor contains the name of the property and the function that accesses the type information of the property, and the original principle of HandyJSON is to get the type information of the property from the Nominal Type Descriptor and then assign the corresponding value in the JSON string, Since fieldTypeAccessor conforms to C’s calling Convention, a strong cast of the pointer yields type information:

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)}}Copy the code

Swift, 4.2

Swift 4.2 has made changes to the Struct and class structures in Terms of Nominal Type Descriptors. At first glance nothing is missing, but the fieldTypesAccessor function has been modified. It no longer conforms to c’s calling Convention, so it is no longer possible to obtain type information from 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
}
Copy the code

Even though Apple wants us to use Mirror for reflection, the Mirror does not contain the type information of the attribute so far, so Apple has created a temporary swift_getFieldAt interface to help us get the type information:

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

Why is it temporary? Because Swift 5 found that this interface was missing…

Swift, 5.0

When the Swift 5.0, it has already been said that did not have the access type of the interface, so we had to dig out source to find ideas of Swift, find TypeContextDescriptorBuilderBase layout of a class () method:

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

  super::layout();
  asImpl().addName();
  asImpl().addAccessFunction();
  asImpl().addReflectionFieldDescriptor();
  asImpl().addLayoutInfo();
  asImpl().addGenericSignature();
  asImpl().maybeAddResilientSuperclass();
  asImpl().maybeAddMetadataInitialization();
}
Copy the code

According to the source code, the structure of nominal Type Descriptor is as follows:

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
}
Copy the code

FieldTypesAccessor still unable to call, but we found there have one more reflectionFieldDescriptor pointer, intuition told me way should be in this thing, so to see what is under the structure:

void addReflectionFieldDescriptor(a) {... B.addRelativeAddress(IGM.getAddrOfReflectionFieldDescriptor( getType()->getDeclaredType()->getCanonicalType())); }Copy the code

Logic is to get the basic ReflectionFieldDescriptor address, and then put the address to the corresponding memory, it is important to note here is a relative address, RelativePointer wrote in the comments:

// 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.

Relative reference is relative to the current reference pointer address offset, so we have to get ReflectionFieldDescriptor address:

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)
}
Copy the code

So now that we have the address, we also need to know what the structure of the FieldDescriptor looks like, so let’s find the class FieldDescriptor:

// Field descriptors contain a collection of field records for a single
// class, struct or enum declaration.
class FieldDescriptor {
  const FieldRecord *getFieldRecordBuffer(a) 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;

  usingconst_iterator = FieldRecordIterator; . }Copy the code

The structure of the FieldDescriptor contains an array of fieldRecords, which should contain the type information, so let’s go back to the source of the FieldRecord:

class FieldRecord { const FieldRecordFlags Flags; const RelativeDirectPointer<const char> MangledTypeName; const RelativeDirectPointer<const char> FieldName; . }Copy the code

Unfortunately FieldRecord did not save the type information directly, only one MangledTypeName, no big problem, we also have a function called swift_getTypeByMangledNameInContext, The swift_getTypeByMangledName function called behind this function is the same function as getFieldAt, which returns any. Type:

@_silgen_name("swift_getTypeByMangledNameInContext") public func _getTypeByMangledNameInContext( _ name: UnsafePointer<UInt8>, _ nameLength: Int, genericContext: UnsafeRawPointer? , genericArguments: UnsafeRawPointer?) -> Any.Type?Copy the code

At this point we have solved the catastrophic compilation problem caused by Swift 5.0 metadata changes, and the metadata structure has been cleaned up. The code has been submitted to the dev_for_swift5.0 branch.

Afterword.

Mirror is officially supported by the reflection tool, using Metadata in this way is a kind of mainstream approach, but Apple also realized that some of the information in the Mirror can not provide, it is said that there is a bit of technical difficulties so temporarily can not put type information into the Mirror, etc. Metadata has been added to reflect information. ABI Dashboard also says ABI stability takes precedence over full reflection. Metadata will not be changed for the time being. But in the long run, Apple will still fully support reflection in the Mirror.

The relevant data

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