Dynamic dispatching protocol extension doesn't work multiple targets

584 views Asked by At

This is my code in my main target(so not the test target):

protocol ProtocolA {
    func dontCrash()
}

extension ProtocolA {
    func dontCrash() {
        fatalError()
    }

    func tryCrash() {
        dontCrash()
    }
}

class MyClass: ProtocolA {}

In my test target (so different target), I got this code:

import XCTest
@testable import Project

extension MyClass {
    func dontCrash() {
        print("I dont crash")
    }
}

class ProjectTests: XCTestCase {
    func testExample() {
        MyClass().tryCrash()
    }
}

It crashes. Why it doesn't use the dynamic dispatching mechanism? MyClass has it's own implementation of dontCrash(), I expect that one to fire.

1

There are 1 answers

1
rob mayoff On BEST ANSWER

Your Project module declares MyClass's conformance to ProtocolA.

Swift implements that conformance using a data structure called a “protocol witness table”. For each method declared by the protocol, the witness table contains a function that calls the actual implementation of the method for the conforming type.

To be concrete, there is a witness table for the conformance of MyClass to ProtocolA. That witness table contains a function for the dontCrash method declared by ProtocolA. That function in the witness table calls the MyClass dontCrash method.

You can see the function from the protocol witness table in the stack trace when your test case hits fatalError:

#8  0x00000001003ab9d9 in _assertionFailure(_:_:file:line:flags:) ()
#9  0x00000001000016fc in ProtocolA.dontCrash() at /Users/rmayoff/TestProjects/Project/Project/AppDelegate.swift:11
#10 0x0000000100001868 in protocol witness for ProtocolA.dontCrash() in conformance MyClass ()
#11 0x000000010000171e in ProtocolA.tryCrash() at /Users/rmayoff/TestProjects/Project/Project/AppDelegate.swift:15
#12 0x00000001030f1987 in ProjectTests.testExample() at /Users/rmayoff/TestProjects/Project/ProjectTests/ProjectTests.swift:12
#13 0x00000001030f19c4 in @objc ProjectTests.testExample() ()

Frame #10 is the call from tryCrash to the function in the protocol witness table. Frame #9 is the call from the protocol witness table function to the actual implementation of dontCrash.

Swift emits the protocol witness table in the module that declares the conformance. So, in your case, the witness table is part of the Project module.

Your override of dontCrash in your test bundle cannot change the contents of the witness table. It's too late for that. The witness table was fully defined when Swift generated the Project module.

Here's why it has to be this way:

Suppose I'm the author of the Project module and you're just a user of it. When I wrote the Project module, I knew calling MyClass().dontCrash() would call fatalError, and I relied on this behavior. In many places inside Project, I called MyClass().dontCrash() specifically because I knew it would call fatalError. You, as a user of Project, don't know how much Project depends on that behavior.

Now you use the Project module in your app, but you retroactively change MyClass().dontCrash() to not call fatalError. Now all those places where Project calls MyClass().dontCrash() don't behave in the way that I expected when I wrote the Project module. You have broken the Project module, even though you didn't change the source code of the Project module or any of the modules that Project imports.

It's critical to the correct operation of the Project module that this not happen. So the only way to change what MyClass().dontCrash() means (when called from inside the Project module) is to change the source code of the Project module itself (or change the source code of something that Project imports).