Understanding method dispatch in Swift

In its most general sense, the word dispatch describes the act of sending something somewhere for a particular purpose. In computer science, this term is used to indicate the same concept in different contexts, like to dispatch a call to a function, dispatch an event to a listener, dispatch an interrupt to a handler, or dispatch a process to the CPU.

In this article, we’ll examine this term in the object-oriented (OO) world— in other words, dispatching a call to a method. Specifically, we’ll look at class method dispatch and protocol-based dispatch.

We’ll begin with object-oriented programming and class method dispatch. As we all know, Swift can support many different programming styles. These styles include functional, procedural, object-oriented, structural, and protocol -oriented.

Swift 1.0 provided full support for UIKit at the time of its release. UIKit is a fully mature and powerful Objective C framework for creating user interfaces.

It does this by letting us create custom classes, data, and operations, where we can selectively expose publicly-available components and private components. Classes are said to have an outside and an inside. Object-oriented programming also lets us define a base class API that we can then refine in sub-classes. This powerful feature is referred to as polymorphism, and it lets us substitute types at runtime and substitute new behavior instead of exiting on the fly.

Imagine we have a base class of type Animal which defines a method makeNoise() and two concrete types Dog and Cat that inherit from the base class and override makeNoise() to give a specific implementation. What will happen if we have an array of Animals and then we call makeNoise() on each one of them?

Each object in the array refers to one of the two sub-classes mentioned above.

Each concrete class has one V-table that gets internally passed to perform this task. Hence, in this case when makeNoise() is called on a dog object it will print “Bark” and when when makeNoise() is called on a cat object it will print “Mew”.

Swift not only supports dynamic dispatch, but it also supports two more types of dispatch. In all, the language supports the following three types of dispatch:

  • Static Dispatch
  • Message Dispatch
  • Dynamic Dispatch

Static dispatch, also called as early binding, is determined at the compile time and is very fast compared to the other two. The compiler knows at compile time the function to execute, and the decision is not left until runtime to pick an implementation opposite of dynamic dispatch. This means no call overheads.

As discussed earlier, Swift uses dynamic dispatch to dispatch methods at runtime using a V-table. However, Objective-C runtime uses message dispatch to perform similar activity. Message dispatch is the most flexible of the three, and it also enables swizzling new implementations at runtime.

Dispatch in action

In the diagram above, we mentioned Animal and it’s subclasses. Let’s have a look at the code and see dynamic dispatch in action:

class Animal {
    init() {
        print("Animal Created")
    }

    func makeNoise() {
        fatalError("Must Override to get specific Noise")
    }
}

class Cat : Animal {
    override init() {
        print("Cat created")
    }

    override func makeNoise() {
        print("Mews")
    }
}

let cat = Cat()
cat.makeNoise()

We have an Animal base class and a concrete Cat class that inherits from Animal.

The base class has a makeNoise() method, but since the type of animal isn’t known with the base class, this method throws a fatalError with a message.

To check the lifecycle, we have provided explicit initializers to both the classes with print statements, and finally we created a Cat instance and invoked makeNoise() on it.

Executing the code, we get the following output on the console:

As you can see, both initializers were called and finally “Mews” was printed on the console instead of fatalError. The reason for this behavior is dynamic dispatch—i.e. at runtime the compiler decides to pick the concrete class implementation of makeNoise().

Extensions, V-tables and Method Dispatch

As Swift developers we know it isn’t possible to override a method written in a Swift extension. For example, let’s take the example of the following class:

The reason for such a behavior is that a method written in an extension isn’t added to the V-table of the class.

Fix: As a workaround, we add @objc to the method inside the extension. Adding @objc to the method provides a full ObjectiveC method dispatch to the method, because of which the compiler knows what to do in case this method is called at runtime. As a result, it allows us to override the method.

Dispatch for Protocols

In this section, we’ll see protocol-oriented programming (POP) in action and work with dispatch for protocols.

It works fine to a certain degree, but the design becomes awkward as we get deeper with multi-level inheritance.

Protocols provide a blueprint of specific functionality that can be adopted by a type. This lets us add interface and implementation to types that we don’t own in code. Unlike inheritance, we can adopt any number of protocols and provide default implementation to functions if need be, and this can be done for Classes, Structures, and Enumerations.

Let’s see how POP affects programming principles that we’ve worked with so far. In the previous section, we had a base class Shape. With POP, Shape no longer needs to be a class. Instead, it can now be a protocol, as shown below:

protocol Shape {
    var length: Float { get set }
    var breadth: Float { get }
    func area() -> Float 
}

extension Shape {
    func area() -> Float {
        return length * breadth
    }
}

Now, Shape doesn’t put any memory layout requirements on how these properties are stored, and if we forgot any one of the required properties, we’ll get an error at compile time and not at runtime.

Protocol functions can’t have bodies, but we can implement the functions in extensions, as shown in the code. If we add another function—for example, parameter()to the extension, every type that confirms to this protocol will get access to that method by default and will be statically dispatched when used.

Problem with protocol types:

Let’s explore protocol dispatch with help of the following example:

protocol Container {
    func getValue()
}

// Value: 3 words in size
struct ContainerOne : Container {
    var stringValue: String
    func getValue() {}
}

// Value: 1 word in size
struct ContainerTwo : Container {
    var intValue: Int
    func getValue() {}
}

// How do we represent this in memory?
var testContainer: Container

We have a protocol Container and two structs: ContainerOne and ContainerTwo, conforming to Container type. ContainerOne holds onto a String type, which makes it three words in memory, and ContainerTwo holds an Int value, which is one word in memory.

Now, if we create a variable testContainer, how will Swift represent this in memory, given that it can either contain an instance of ContainerOne or ContainerTwo? In other words, how can an arbitrary variable be stored in a variable of known size?

The same question has a very simple answer for Objective-C, since only Classes are allowed to conform to protocols in Objective-C. From our previous understanding, we know that Classes are reference types, and only a reference is required to be stored in a given variable.

However, this can’t be used in Swift because structs can conform to protocols, and structs aren’t exposed to the Objective-C runtime.

Swift’s solution to protocol types

Swift solves the problem by using existential container types.

Each existential container type is composed of the following three things:

  1. A Value buffer that contains the value of the model or a pointer if the value is greater than three words. It stores the actual instance. For example, if we had a one word long instance, then it would be stored directly inside the Value buffer, and for anything bigger than one word, it would allocate the memory on the heap and store the reference to that memory itself.
  2. Value Witness table (VWT): A V-table used only for creating, copying, and destroying values. It stores metadata for the type of given instance that’s stored in the Value buffer. As a part of the metadata, it also stores a pointer to the VWT. Value witness tables store pointers to the given function in order to manage the memory for the given Value buffer.
  3. Protocol Witness table (PWT): Acts similar to a V-table and is used to dispatch the correct method. It’s a series of pointers to the function implementations of protocol requirements for a given type. Each type that conforms to a given protocol has it’s own PWT. So if a type conforms to more than one protocol, then the existential container simply adds a reference to another PWT for the other protocol, as shown by step 4 in the image above.

This works fine because the existential container still remains the same size, and when the compiler notices multiple protocol conformance, it’s able to derive the size of the existential container for that type. We can check this by having a look at the following code:

// How do we represent this in memory?
var testContainer: Container

protocol BiggerContainer {
    func getBiggerValue()
}

var testBigAndSmallContainer: (Container & BiggerContainer)

// Getting memory layout for both the variables
print("Size of Container =", MemoryLayout<Container>.size)
print("Size of Big and Small Container =", MemoryLayout<Container & BiggerContainer>.size)

The result of executing the above code prints the following to the console:

As you can see, conformance to one more protocol increases the size of the existential container type by 8bytes (i.e. 64 bits), which means I’m working with a 64-bit machine.

From the above discussion, we now know that we have a separate PWT per type, per protocol conformance, and this is the mechanism used by protocols to achieve dynamic dispatch. When the compiler sees a method call on a given existential container, it dispatches to the corresponding methods in the respective PWT.

Hence, by using concrete types over abstract types, we can avoid excessive overhead by not just avoiding the use of existential containers, but also by giving a stronger type to variables that the compiler is working with.

For other updates you can follow me on Twitter on my twitter handle @NavRudraSambyal

Thanks for reading, please share it if you found it useful 🙂

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *

wix banner square