I. Basic syntax of the agreement

1. Definition of the Agreement

Protocols can be used to define declarations of methods, properties, and subscripts. Protocols can be followed by enumerations, structures, and classes (multiple protocols separated by commas).

// Protocol defines methods, attributes, and subscripts
protocol Drawable {
    func draw(a)
    var x: Int { get set }
    var y: Int { get }
    subscript(index: Int) -> Int { get}}protocol Protocol1 {}
protocol Protocol2 {}
protocol Protocol3 {}
// Abide by the agreement
class Person: Protocol1.Protocol2.Protocol3 {}
Copy the code

Methods defined in protocols cannot have default parameter values. By default, all parameters defined in protocols must be implemented. If we don’t want to enforce protocol-compliant type implementations, we can prefix the protocol definition with optional, and prefix protocol and optional with @objc.

@objc protocol Incrementable {
    @objc optional func increment(by: Int)
}
Copy the code

2. Attributes in the protocol

  • The var keyword must be used to define attributes in the protocol.

  • When implementing the protocol, the property permissions must be no less than those defined in the protocol. The protocol defines get and set and uses var to store properties or get and set to calculate properties. The protocol defines GET and can be implemented with any property.

Such as:

protocol Drawable {
    func draw(a)
    var x: Int { get set }
    var y: Int { get }
    subscript(index: Int) -> Int { get}}class Person: Drawable {
    func draw(a) {
        print("Person draw")}var x: Int = 0
    var y: Int = 0
    subscript(index: Int) -> Int {
        set{}
        get{ index }
    }
}
Copy the code

3. Protocol static, class, mutating, and init

  • static

To ensure generality, the protocol must use static to define type methods, type attributes, and type subscripts.

protocol Drawable {
    static func draw(a)
}

class Person: Drawable {
    // class func draw
    static func draw(a) {
        print("Person draw")}}Person.draw()   // Person draw
Copy the code
  • mutating

The implementation of a structure or enumeration is allowed to modify its own memory only if the instance method in the protocol is marked mutating. Classes implement methods without mutating. Enumerations and structures need mutating.

protocol Drawable {
    mutating func draw(a)
}

class Person: Drawable {
    func draw(a) {
        print("Person draw")}}struct Point: Drawable {
    mutating func draw(a) {
        print("Point draw")}}Copy the code
  • init

The protocol can also define an initializer init, which must be required for non-final class implementations.

final class Size: Drawable {
    init(x: Int.y: Int){}}class Point: Drawable {
    required init(x: Int.y: Int){}}Copy the code

If the initializer implemented from the protocol overrides the specified initializer of the parent class, the initializer must also be required and override.

protocol Livable {
    init(name: String)
}

class Person {
    init(name: String){}}class Student: Person.Livable {
    required override init(name: String) {
        super.init(name: name)
    }
}
Copy the code

Init as defined in the protocol? And init! , you can use init, init? And init! To implement the init defined in the protocol, you can use init, init! To achieve them.

protocol Livable {
    init(a)
    init?(age:Int)
    init!(no:Int)
}

class Person: Livable {
    required init(a) {}
    // required init! () {}

    required init?(age: Int) {}
    // required init(age: Int) {}
    // required init! (age: Int) {}

    required init!(no: Int) {}
    // required init(no: Int) {}
    // required init? (no: Int) {}
}
Copy the code

4. Inheritance and combination in protocols

One protocol can inherit from another. Such as:

protocol Runnable {
    func run(a)
}

protocol Livable: Runnable {
    func breath(a)
}

class Person: Livable {
    func breath(a) {}
    func run(a){}}Copy the code

A protocol combination that can contain up to one class type. Let’s look at the following example:

// Receive an instance of Person or its subclasses
func fn0(obj: Person) {}

// Receive instances of compliance to the Livable protocol
func fn1(obj: Livable) {}

// Receive instances that comply with Livable, Runnable protocols
func fn2(obj: Livable & Runnable) {}

// Accept instances of Person or a subclass of Livable, Runnable protocols
func fn3(obj: Person & Livable & Runnable) {}
Copy the code

In fn3, the declaration of the obj protocol combination is too long. We can alias the protocol combination using typeAlias, for example:

typealias RealPerson = Person & Livable & Runnable
// Accept instances of Person or a subclass of Livable, Runnable protocols
func fn3(obj: RealPerson) {}
Copy the code

5. CaseIterable and CustomStringConvertible

  • Making enumerations conform to the CaseIterable protocol allows you to iterate over an enumeration value.
enum Season: CaseIterable {
    case spring, summer, autumn, winter
}

let seasons = Season.allCases
print(seasons.count) / / 4

for season in seasons {
    print(season)
} // spring summer autumn winter
Copy the code
  • Keep CustomStringConvertible, CustomDebugStringConvertible agreement, can be custom print string instance.
class Person: CustomStringConvertible.CustomDebugStringConvertible {
    var age = 0
    var description: String{ "person_\(age)" }
    var debugDescription: String{ "debug_person_\(age)"}}var person = Person(a)print(person)       // person_0
debugPrint(person)  // debug_person_0
Copy the code

  • Print calls description of the CustomStringConvertible protocol.

  • DebugPrint, Po is invoked debugDescription CustomDebugStringConvertible agreement.

6. Class specific protocol

After the protocol, write AnyObject to indicate that only classes are subject to this protocol, and class to indicate that only classes are subject to this protocol.

protocol MyProtocol: AnyObject {}
Copy the code
protocol MyProtocol: class {}
Copy the code

Second, the witness_table

Let’s take a look at the witness_table and what it’s up to.

In the article “Method” we know that class method scheduling is through the virtual function table (VTable) to find the corresponding function to call, and the structure method is directly to get the address of the function to call. What about the method declared in the protocol, if the class or structure complies with the protocol and then implements the protocol method, how does it look up the address of the function to call.

1. Witness_table introduction

We declare a Born protocol, which has a Born (:) method in it. The class -person complies with Born and implements the Born (:) method as follows:

protocol Born {
    func born(_ nickname: String)
}

class Person: Born {
    var nickname: String?
    func born(_ nickname: String) {
        self.nickname = nickname
    }
}

let p = Person()
p.born("Coder_ zhang SAN")
Copy the code

Next we compile the current main.swift file into the main.sil file to see if there is a VTable using sil code. After compiling, find the main function and see the born(:) method called, as shown below:

Note that born(:) is of type class_method in sil, as described in the SIL reference. Methods of type class_method are found in VTable, as shown in figure 1:

Next we see the code at the bottom of the main.sil file, as shown below:

As you can see, born(:) is stored in the VTable, but the witness_table is up to something and has a born(:) in it as well. Next thing I do, I declare the variable p as the Born protocol, as follows:

let p: Born = Person(a)Copy the code

Next, recompile the main.swift file into the main.sil file and look directly at the main function, as shown below:

Witness_method = witness_method = witness_method = witness_method = witness_method

Translation:

Finds the implementation of a protocol method for a generic type variable that is bound by that protocol. The result will be common on the Self prototype of the original protocol, with the witness_method call convention. If the protocol referenced is the @objc protocol, the result type has an objC calling convention.

@protocol Witness for main.born.born (swift.string) -> ()

Notice that it still ends up looking for scheduling methods that comply with the VTable in its class. The only difference between our two tests is whether the type of the variable is Born protocol type, which can also be understood as the way the call is made depends on the static type of my variable.

The summary is as follows:

  • If the static type of the instance object is the determined type, then the protocol method is scheduled through VTalbel.

  • If the static type of the instance object is the protocol type, the protocol method is scheduled through the witness_table protocol method. Then, the protocol method is used to find the Vtables of the compliant classes.

2. Witness_table of the architecture

Witness_table (witness_table) now that we know about the witness_table scheduling for our class, let’s look at the witness_table structure again, using the following sil code:

protocol Born {
    func born(_ nickname: String)
}

struct Person: Born {
    var nickname: String?
    func born(_ nickname: String) {
        self.nickname = nickname
    }
}

let p: Born = Person()
p.born("Coder_ zhang SAN")
Copy the code

Next, recompile the main.swift file into the main.sil file and look directly at the main function, as shown below:

Let’s look at assembly code again, as shown below:

As you can see, the way a structure calls a protocol method is directly a function address call. When I specify the Born protocol for this variable, the implementation of the sil main function is as follows:

Witness_method = witness_method; witness_method = witness_method; witness_method = witness_method

As you can see, it finally finds the address of the structure born(:) method and calls it directly. So that’s a call to the witness_method structure.

3. Provide default implementations of protocol methods in the protocol Extention

If do an extension to a protocol, and implement the protocol method. Classes that comply with the protocol also implement the protocol method. So, when you call a protocol method from this class, you call the protocol method implemented in the class.

The code is as follows:

protocol Born {
    func born(_ nickname: String)
}

extension Born {
    func born(_ nickname: String) {
        print("Born born(:)")}}class Person: Born {
    func born(_ nickname: String) {
        print("Person born(:)")}}let p = Person()
p.born("Coder_ zhang SAN")  // Person born(:)
Copy the code

If the protocol method is not declared in the protocol, but is implemented in extension in the protocol, the class that complies with the protocol also implements the method. So, when the protocol method is called from this class, the method implemented in the class will be called, but if the variable type is specified as the protocol type, the method implemented in the protocol extension will be called.

The code is as follows:

protocol Born {}

extension Born {
    func born(_ nickname: String) {
        print("Born born(:)")}}class Person: Born {
    func born(_ nickname: String) {
        print("Person born(:)")}}let p: Born = Person()
p.born("Coder_ zhang SAN")  // Born born(:)
Copy the code

In the first case, the protocol method calls the same result as the verification process in the first point, so you can compile it into sil code to verify the comparison. Let’s focus on the second case, where we’ll look directly at the main function and the sil_witness_table, as shown below:

As you can see, for the second case, it just calls the function address in extension, and there are no methods in the sil_witness_table.

Sil_witness_table (witness_witness_witness_witness_witness_table) = Person (p = Person) sil_witness_table = Person (p = Person) sil_witness_table = Person (p = Person)

So here’s a summary:

  • First, whether the sil_witness_table has any methods depends on whether the protocol methods are declared in the protocol.

  • If there are no methods in sil_witness_TABLE, then the VTable is scheduled as VTable and the direct function address is called as function address.

  • If the sil_witness_table has methods, whether to call through WITNess_method depends on whether the static type of the current instance is a protocol type. If not, schedule it however you want. If so, schedule the methods through witness_method.

All in all, when the sil_witness_table has methods and is called through witness_method, it’s just one more layer of function calls.

4. Sil_witness_table About the inheritance relationship

  • If a protocol is observed by multiple classes, there is one sil_witness_table in each class.

  • If a class complies with multiple protocols, there will be one sil_witness_table for each protocol. That is, there will be many sil_witness_tables, depending on the number of protocols.

  • If a class complies with a protocol, the class must have a sil_witness_table, so the subclasses and their parent class share one SIL_witness_table.

The above three points can be compared with sil code, interested people can try to verify for themselves. There is no texture here, more trouble.

Witness_table Memory layout and structure

1. Witness_table Location in the memory

Let’s look at some interesting code, like this:

protocol Shape {
    var area: Double { get}}class Circle: Shape {
    var radius: Double

    init(_ radius: Double) {
        self.radius = radius
    }

    var area: Double {
        get {
            return radius * radius * 3.14}}}print("Circle size: \(MemoryLayout<Circle>.size)") // Circle size: 8
print("Shape size: \(MemoryLayout<Shape>.size)")   // Shape size: 40
Copy the code

The Size of the class is equal to 8, which is normal because the class has heap space. This 8 is only the Size of a pointer type. To get the true size of a class, use the class_getInstanceSize function.

Protocol size = 40 protocol size = 40 protocol size = 40 protocol size = 40

let c1: Circle = Circle(10)
let c2: Shape = Circle(20)

print("c1 size: \(MemoryLayout.size(ofValue: c1))") // c1 size: 8
print("c2 size: \(MemoryLayout.size(ofValue: c2))") // c2 size: 40
Copy the code

We find that the same instance of Circle, but when the instance is specified as a protocol type, the size of the instance becomes 40. At this point, it means that c1 and C2 have different memory structures.

C1 stores the address of its heap space instance object. Let’s take a look at its memory layout, as shown in the figure below:

This is the memory layout for C1, and we print out radius values using expr -f float — < memory address > expression.

Let’s look at c2’s memory layout, as shown below:

Now, look at:

  • The first 8 bytes of memory still store the address value of the heap space.

  • We don’t know what the second and third 8 bytes store.

  • The fourth 8-byte stores the address of the heap space metadata.

  • The last 8 bytes are actually the addresses of witness_table.

So how do I know that the last 8 bytes are stored as the witness_table address? Witness_table the last 8-byte memory address is 0x0000000100004028. Let’s start assembly debugging, find the witness_table code after the creation of c2, as shown in the following figure:

So the last 8 bytes are really the addresses of witness_table, as shown in the figure. Through the above analysis, the general structure of c2 type variable can be obtained, and the code is as follows:

struct ProtoclInstaceStruct {
    var heapObject: UnsafeRawPointer
    var unkown1: UnsafeRawPointer
    var unkown2: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeRawPointer
}
Copy the code

2. Witness_table Memory structure

Witness_table = witness_table = witness_table = witness_table = witness_table = witness_table = witness_table = witness_table = witness_table IR syntax and how to compile into IR code are covered in the articles Closures and Their Nature Analysis and Methods.

We will compile the current main.swift file directly into main.ll, using the same code as in the first point, but I have printed out comments for c1 variable and print to avoid interference. After compiling to main.ll, we can directly look at the main function, and the code is as follows:

define i32 @main(i32 %0, i8** %1) #0 {
    entry:
    %2 = bitcast i8** %1 to i8*
    // Get the metadata of Circle
    %3 = call swiftcc %swift.metadata_response @"type metadata accessor for main.Circle"(i64 0) #7
    %4 = extractvalue %swift.metadata_response %3.0
    // %swift.type = type { i64 }
    // %swift.refcounted = type { %swift.type*, i64 }
    // %T4main6CircleC = type <{ %swift.refcounted, %TSd }>
    {% swif.refcounted, %TSd} counted {% swif.refcounted, %TSd} counted
    %5 = call swiftcc %T4main6CircleC* @"main.Circle.__allocating_init(Swift.Double) -> main.Circle"(double 2.000000 e+01.%swift.type* swiftself %4)

    // %T4main5ShapeP = type {[24 x i8], %swift.type*, i8**}, %T4main5ShapeP is essentially a structure
    // Note that getelementptr is the memory address of the i320 structure to get the structure member, and store %4 in the second member variable of the structure
    {[24 x i8], metadata, i8**}
    store %swift.type* %4.%swift.type** getelementptr inbounds (%T4main5ShapeP.%T4main5ShapeP* @"main.c2 : main.Shape", i32 0, i32 1), align 8

    // Get the Witness table and store the witness table on the third member of the %T4main5ShapeP structure (i32 2)
    // The structure of %T4main5ShapeP is {[24 x i8], metadata, witness_table}
    store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"protocol witness table for main.Circle : main.Shape in main", i32 0, i32 0), i8* * * getelementptr inbounds (%T4main5ShapeP.%T4main5ShapeP* @"main.c2 : main.Shape", i32 0, i32 2), align 8

    %T4main5ShapeP = type {[3 x i64], %swift. Type *, i8**}
    {[3 x i64], metadata, witness_table}
    // Then place %5 in the first element of %T4main5ShapeP. {[%T4main6CircleC*, i64, i64], metadata, witness_table},
    store %T4main6CircleC* %5.%T4main6CircleC** bitcast (%T4main5ShapeP* @"main.c2 : main.Shape" to %T4main6CircleC**), align 8
    ret i32 0
}
Copy the code

The interpretation of this code further verifies the memory structure of C2 inferred from point 1. Witness_table’s witness_table is witness_table. The witness_table structure is as follows:

Witness_table has two witness_table members. The witness_table witness_table structure is as follows:

struct TargetWitnessTable{
    var  protocol_conformance_descriptor: UnsafeRawPointer
    var  protocol_witness: UnsafeRawPointer
}
Copy the code

At this point, the structure of a ProtoclInstaceStruct becomes the following:

struct ProtoclInstaceStruct {
    var heapObj: UnsafeRawPointer
    var unkown1: UnsafeRawPointer
    var unkown2: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeMutablePointer<TargetWitnessTable>}Copy the code

3. Witness_table memory structure

Witness_table’s witness_table memory structure is witness_witness_table’s witness_table memory structure. We searched TargetWitnessTable globally and found TargetWitnessTable in the metadata. h file, as shown in the following figure:

Look, the comments in the source code also clear to write that this is a protocol to witness table, and, at this point we know 2 point analysis is a TargetProtocolConformanceDescriptor protocol_conformance_descriptor, Find the definition of this structure and find that it has the following members, as shown:

So let’s look at the Protocol member variable, which is a pointer to a relative type, and the structure of the type is TargetProtocolDescriptor, Relative type pointer in the “metatype and Mirror source code and HandyJson analysis restore enumeration, structure, Metadata class” this article is introduced, and we have put the relative type pointer to restore out, we use the time directly copy over good.

Now we need to restore the structure of the TargetProtocolDescriptor, so the TargetProtocolDescriptor inherits from TargetContextDescriptor, TargetContextDescriptor should be incredibly familiar, as described in the article mentioned above. So, the TargetProtocolDescriptor must have two member variables, Flags and Parent. Let’s see what it has in itself, as shown in the figure below:

At this point, the TargetProtocolDescriptor structure can be restored as follows:

struct TargetProtocolDescriptor {
    var Flags: UInt32
    var Parent: TargetRelativeDirectPointer<UnsafeRawPointer>
    var Name: TargetRelativeDirectPointer<CChar>
    var NumRequirementsInSignature: UInt32
    var NumRequirements: UInt32
    var AssociatedTypeNames: TargetRelativeDirectPointer<CChar>}Copy the code

TargetProtocolDescriptor structure reduction after coming out, we then also restore a TargetProtocolConformanceDescriptor structure, code is as follows:

struct TargetProtocolConformanceDescriptor {
    var `Protocol`: TargetRelativeDirectPointer<TargetProtocolDescriptor>
    var TypeRef: UnsafeRawPointer
    var WitnessTablePattern: UnsafeRawPointer
    var Flags: UInt32
}
Copy the code

4. Verify the memory structure of the witness_table

Witness_table witness_witness_table is witness_witness_witness_table.

The complete code restored is as follows:

struct ProtoclInstaceStruct {
    var heapObj: UnsafeRawPointer
    var unkown1: UnsafeRawPointer
    var unkown2: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeMutablePointer<TargetWitnessTable>}struct TargetWitnessTable {
    var  protocol_conformance_descriptor: UnsafeMutablePointer<TargetProtocolConformanceDescriptor>
    var  protocol_witness: UnsafeRawPointer
}

struct TargetProtocolConformanceDescriptor {
    var `Protocol`: TargetRelativeDirectPointer<TargetProtocolDescriptor>
    var TypeRef: UnsafeRawPointer
    var WitnessTablePattern: UnsafeRawPointer
    var Flags: UInt32
}

struct TargetProtocolDescriptor {
    var Flags: UInt32
    var Parent: TargetRelativeDirectPointer<UnsafeRawPointer>
    var Name: TargetRelativeDirectPointer<CChar>
    var NumRequirementsInSignature: UInt32
    var NumRequirements: UInt32
    var AssociatedTypeNames: TargetRelativeDirectPointer<CChar>}struct TargetRelativeDirectPointer<Pointee> {
    var RelativeOffset: Int32

    mutating func getmeasureRelativeOffset(a) -> UnsafeMutablePointer<Pointee> {let offset = self.RelativeOffset

        return withUnsafePointer(to: &self) { p in
            return UnsafeMutablePointer(mutating: UnsafeRawPointer(p).advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self))}}}Copy the code

Here is my verification code:

var c2: Shape = Circle(20)

withUnsafePointer(to: &c2) { c2_ptr in
    c2_ptr.withMemoryRebound(to: ProtoclInstaceStruct.self, capacity: 1) { pis_ptr in
        print(pis_ptr.pointee)

        let protocolDesPtr = pis_ptr.pointee.witness_table.pointee.protocol_conformance_descriptor.pointee.Protocol.getmeasureRelativeOffset()
        print("Agreement Name:\(String(cString: protocolDesPtr.pointee.Name.getmeasureRelativeOffset()))")
        print("Number of protocol methods:\(protocolDesPtr.pointee.NumRequirements)")
        print("WitnessMethod:\(pis_ptr.pointee.witness_table.pointee.protocol_witness)")}}Copy the code
Print result:ProtoclInstaceStruct(heapObj: 0x000000010732a1c0, unkown1: 0x0000000000000000, unkown2: 0x0000000000000000, metadata: 0x00000001000081f0, witness_table: 0x0000000100004088) Agreement Name:ShapeNumber of protocol methods:1WitnessMethod:0x00000001000021d0
Copy the code

During the IR code, we should have noticed that the protocol_witness of TargetWitnessTable is stored in the witnessMethod. But let’s check it out.

  • Used on terminalsNm - p < executable file > | grep < > memory addressPrint out the symbolic information for this method.
  • Then use theXcrun swift-demangle < symbol information >Restores the symbol information.

As shown in figure:

So, the protocol witness_table is essentially TargetWitnessTable. The first element stores a descriptor, which records some description of the protocol, such as the name and the number of methods. The pointer stored from the second element is the pointer to the function.

Attention! The witness_table variable in ProtoclInstaceStruct is a contiguous memory space, so it could be witness tables for a number of protocols.

If the type of the variable is the protocol combination type, then the witness_table is storing witness tables of all the protocols in the witness_table. If the type of the variable is a single protocol, the witness_table is storing witness tables of all the protocols in the witness_table. So the witness_table is the only witness table for the agreement.

Four, Existential Container

We’ve been looking at the protocol witness_table in the third one, so during our exploration we’ve restored the memory layout of the c2 instance, the ProtoclInstaceStruct structure. What is it? Let’s introduce an interface Container.

Interface Container: A special data type generated by the compiler for managing protocol types that adhere to the same protocol, managed through the current Interface Container because their memory sizes are inconsistent.

  • For small-volume data, it is directly stored in the Value Buffer.

  • For large volumes of data, the address of the heap space is stored through heap allocation.

One thing to make clear is that the restored ProtoclInstaceStruct is actually an Existential Container, which translates to Existential Container. The last two 8-byte stores of the container are fixed, storing the metatype of the instance type and the witness table of the protocol.

What does the first 24 bytes store:

  • If the instance is of reference type, the first 8 bytes store the address value of the instance in the heap space.

  • If the instance is a value type, it is stored directly in 24 bytes of memory that can fully store the value type (that is, the attribute value of the value type). If the number exceeds 24 bytes, it is allocated through the heap, and then the first 8 bytes store the address of the heap space.

So a ProtoclInstaceStruct should look like this:

struct ExistentialContainer {
    var valueBuffer1: UnsafeRawPointer
    var valueBuffer2: UnsafeRawPointer
    var valueBuffer3: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeRawPointer
}
Copy the code

Let’s verify this with a structure. For the sake of testing, let’s change the Circle slightly. The code is as follows:

struct Circle: Shape {
    var radius = 10
    var width: Int
    var height: Int

    init(_ radius: Int) {
        self.radius = radius
        self.width = radius * 2
        self.height = radius * 2
    }

    var area: Double {
        get {
            return Double(radius * radius) * 3.14}}}var c2: Shape = Circle(10)
print("end")
Copy the code

Let’s take a look at its memory layout, as shown below:

The first 24 bytes of the existing container store the RADIUS, width, and height of the Circle respectively. Next I add a property called height1 as follows:

struct Circle: Shape {
    var radius: Int
    var width: Int
    var height: Int
    var height1: Int

    init(_ radius: Int) {
        self.radius = radius
        self.width = radius * 2
        self.height = radius * 2
        self.height1 = radius * 2
    }

    var area: Double {
        get {
            return Double(radius * radius) * 3.14}}var area1: Double {
        get {
            return Double(radius * radius) * 3.14}}}Copy the code

Let’s take a look at its memory layout, as shown below:

As shown in the figure, this verifies the concept and significance of the previous existence of containers.