Is Swift a Floor Wax or a Dessert Topping?

I’m going to go into some of the details of how Swift does method dispatch. It’s going to involve some assembly language, but don’t panic – I’ll talk you through it.

Let’s start off with plain swift code:

public class Foo {
    public func doSomething() {
        print("not interesting")
    }
}
 
let x = Foo()
x.doSomething()

What we’re interested in is how x.doSomething gets invoked. What you look for in an assembly language is an instruction that changes the control flow to some other place with the intention of coming back and picking up where you left off. In many processors it’s something like “jsr” (Jump to SubRoutine) or “bsr” (Branch to Subroutine) or in the case of X64, “call”. This instruction usually takes one argument, which is the address of the routine you want to go to. If doSomething were a top level function, not a method, this would be straight forward since the compiler knows the name of the routine it wants to call. For example:

call  _someGlobalFunc

With that in mind, here is the code that gets generated for the last two lines:

call       __T010unitHelper3FooCMa
mov        r13, rax
call       __T010unitHelper3FooCACycfC
mov        qword [__T010unitHelper1xAA3FooCvp], rax
mov        rax, qword [__T010unitHelper1xAA3FooCvp]
mov        rsi, qword [rax]
mov        r13, rax
call       qword [rsi+0x50]

So what’s going on here? The first call to the lovely name ‘__T010unitHelper3FooCMa’ is a call to get the type metadata for the type Foo. This is more or less the ISA value for the type. Every constructor takes one of these (and in anything but generic types it gets completely ignored). The result of that ends up in rax, which gets stored in a global variable named x (__T010unitHelper1xAA3FooCvp). This represents the let statement. Next we copy x (__T010unitHelper1xAA3FooCvp) into rax and read the first pointer value from the instance (the ISA) into rsi. Then we move rax into r13. This step is because in the swift ABI, all instance methods are called with their instance value in r13. This was different in versions of swift earlier than 4.0, but that’s where we are now. Finally, there’s the call instruction – but instead of a global address, it’s using [rsi+0x50]. That means the we’re looking at an address that is 0x50 bytes offset from rsi (the ISA pointer, remember?) and reading an address from there to jump to.
Why? Well, the function that’s getting called can be overridden in a subclass and unless we’re sure that x is really a Foo and not a subclass of Foo, we can’t just call Foo.doSomething directly. Instead, we look up the address we need in a table that’s in the ISA pointer. This is called vtable dispatch and is what pretty much every OOP language uses because it’s reasonably efficient.

Where things get interesting is if you declare the type to be @objc, which means that swift should obey ObjC calling conventions:

import Foundation
 
@objc
public class Bar : NSObject {
    @objc public func doIt() { }
}
 
let b = Bar()
b.doIt()

This is more or less the same code, but now we’re looking at an ObjC type. The generated code looks like this:

call       __T010unitHelper3BarCMa
mov        r13, rax
call       __T010unitHelper3BarCACycfC
lea        rsi, qword [_swift_isaMask]
mov        qword [__T010unitHelper1bAA3BarCvp], rax
mov        rax, qword [__T010unitHelper1bAA3BarCvp]
mov        r13, qword [rax]
and        r13, qword [rsi]
mov        qword [rbp+var_20], r13
mov        r13, rax
mov        rax, qword [rbp+var_20]
call       qword [rax+0x50]

Again, we start with a call to get the class metadata and we move it to r13 and then call the constructor, __T010unitHelper3BarCACycfC. Then we get the address of a global variable named _swift_isaMask and hold that in rsi. It gets used in a bit. Just remember that. Next we store the instance which is in rax in a global variable named __T010unitHelper1bAA3BarCvp, and because this is not optimized code, we read it right back into rax. Next step is reading the ISA pointer from the instance and putting that in r13 and then we mask off some of the bits. This is different. There are some bits in the ISA pointer that get set aside to be used to identify this ISA as really ObjC or swift. If we treat the ISA pointer as a pointer without the mask, we’ll read from the wrong location. It turns out that r13 and rax need to get swapped. That happens through a local variable named [rbp+var_20]. This is abysmal code, by the way. There are other better ways of doing the same thing, but again, this is not optimized so I’m not surprised. Finally, we call doIt by looking it up in a vtable. Wait. What? This is weird because we declared that this was @objc – how come swift isn’t using ObjC’s name dispatch? Does it even have it?
The answer to the latter question is yes. If we poke around in the binaries, we find this routine:

                     -[_TtC10unitHelper3Bar doIt]:
push       rbp
mov        rbp, rsp
push       r13
sub        rsp, 0x18
mov        qword [rbp+var_10], rdi
mov        qword [rbp+var_18], rsi
call       imp___stubs__objc_retain
mov        r13, qword [rbp+var_10]
mov        qword [rbp+var_20], rax
call       __T010unitHelper3BarC4doItyyF
mov        rdi, qword [rbp+var_10]
call       imp___stubs__objc_release
add        rsp, 0x18
pop        r13
pop        rbp
ret

The instance comes in in rdi and gets stored in local var_10. rsi is stored in var_18, but is never used again. Then we call objc_retain on the instance (this is automatic reference counting at work). Then the instance gets read back into r13 and we store rax (the return from retain) into var20. Then we call __T010unitHelper3BarC4doItyyF, which is the mangled name for doIt. So here is the ObjC implementation which is just an adapter to call into the swift implementation. Tada.

So it is possible to call Bar from ObjC and it ends up going right into the existing Swift code.

There is code that can handle dispatch by name and dispatch by vtable.
We can conclude that Swift is both a floor wax and a dessert topping.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.