Implementing a public protocol via an internal protocol

We're doing something like the following:

public protocol Foo {
    func bar()
}

internal protocol FooImpl: Foo {}
extension FooImpl {
    func bar() {
    }
}

class Baz: FooImpl {
}

The real version of FooImpl has some internal requirements that we don't want to expose which the default implementations of functions depend on, so they can't just go in an extension on Foo.

This has been working for us in practice for a while, but Xcode 14.1 partially broke it in a weird way that leaves me unsure if it's even supposed to work. When built, the default implementations supplied by FooImpl have internal linkage, and the app linking our library can only call them virtually via the Foo protocol witness table. However, as of Xcode 14.1 this stopped working for supplying a conformance to Sequence.

With something like the following:

public protocol Foo: Sequence {
    func bar()
    func makeIterator() -> Iterator
}

internal protocol FooImpl: Foo {}
extension FooImpl {
    func bar() {
    }
    func makeIterator() -> Iterator {
        fatalError()
    }
}

class Baz: FooImpl {
}

Calls to bar() are made via the protocol witness table as before. However, calls to makeIterator() are seemingly being devirtualized and are turned into direct calls to FooImpl.makeIterator(). This works great for calls made in the same library, but for calls made outside from outside the library this results in a linker error because FooImpl.makeIterator() is an internal symbol.

Is this just a bug, or is this pattern something which was never actually supposed to work and only did by coincidence?

2 Likes

Mmm, this is a very interesting use case; certainly it should work.
There have been some implementation changes which impacted this: cc @xedin, @hborla