Extensions polluting protocol witness tables

I'm not sure if my analysis here is correct, and would love feedback.

I work on a large Swift project with a complex dependency graph. In this project I ran into an issue where one module had extension of RawRepresentable where RawValue == String with a single method. I believe this resulted in all String enums in modules which transitively depended on the extending module having an entry for this custom function in their witness tables. The outcome of this was the transitive dependency became a direct dependency of all of those modules (I noticed this when trying to build the project as dylibs).

I can kind of see why this is the case, and it makes sense though it may be problematic. The ship has probably sailed on disallowing this for core protocols like RawRepresentable, but I wonder if there would be value in having an attribute which would disallow other modules from writing an extension to a protocol unless that extension is qualified by a protocol or type which is defined by that module (I have no idea what to call it, @disallowExternallyQualifiedExtensions?). I'm curious to hear other people's thoughts on this.

1 Like

I'm an expert on this, but I'm pretty sure of this:

Swift extensions are not dynamically dispatched, and thus shouldn't be part of the witness table. If you don't use the function from the extension, the function should stay unreferenced. That is unless they are providing a default implementation for a protocol's requirement.

Maybe inlined functions from other modules could produces those direct references though.

This function was used in a protocol requirement! It wasn't being explicitly conformed to by the enum, however.

Here is the error text (I've been replaced actual names with Foo/Bar). FooModule is the one deep in the dependency tree with the extension providing offendingFunction (a function with the same signature exists in a protocol defined in FooModule). BarModule doesn't explicitly depend on FooModule but transitively it does.

Undefined symbols for architecture x86_64:
(extension in FooModule):Swift.RawRepresentable< where A.RawValue == Swift.String>.offendingProperty.getter : Swift.String", referenced from:
protocol witness for Swift.CodingKey.offendingProperty.getter : Swift.String in conformance BarModule.BarContainer<A>.(CodingKeys in _HEX) : Swift.CodingKey in BarModule in BarModule.o
protocol witness for Swift.CodingKey.offendingMethod.getter : Swift.String in conformance BarModule.(BarResult in _HEX)<A>.CodingKeys : Swift.CodingKey in BarModule in BarModule.o

Oh, let me guess: some module you use has this:

extension RawRepresentable where RawValue == String {
   var stringValue: String { rawValue }
}

and then you have:

enum Something: String, CodingKey {
  ...
}

and this automatically uses the extension as the default implementation for the CodingKey conformance, creating a dependency on the other module. So you'd like to add @disallowExternallyQualifiedExtensions to that enum so it doesn't do that and... emit a compile-time error? Is that it?

Yup, nail on head. Sorry, if I had just added those code snippets this would have been more straightforward :slight_smile:

(Inaccurate statements removed)

UPDATE Oh, now I see, the problem is that this method is part of the CodingKey protocol! So I guess this isn't a broader problem, but only happens if it "accidentally" uses that method in a conformance.

That sounds like it. You can write your own version of stringValue and the compiler won't pick the one from the other library. You'd be forced to implement it anyway if the other library did not provide it.

You wouldn't be forced to implement because the compiler synthesizes it for you (or more likely it just uses an implementation defined a Foundation extension on RawRepresentable). So I just removed the extension defining stringValue and fixed the places that used that spelling to use rawValue instead :) It is great getting to the bottom of this though, thank you for your help!