Extension-only compilation unit in static framework


(Ryan Peck) #1

I'm having issues with a static Swift framework due to the inclusion of files that include an extension providing a protocol conformance and nothing else. When linked statically, the object file is not loaded by the linker, so the conformance doesn't exist (dynamic casting to the protocol type fails).

A simplified set up is as follows:

  1. Xcode project with 3 targets:
    1. FrameworkA.framework (dynamic framework)
    2. FrameworkB.framework (framework with mach-o type Static Library, depends on FrameworkA.framework)
    3. FrameworkBTests.xctest (links FrameworkB.framework statically, FrameworkA.framework dynamically)
  2. In FrameworkA, a single source file FrameworkA.swift:
    public protocol Abstract { }
    
    public struct Concrete: Abstract {
        public init() { }
    }
    
  3. In FrameworkB, two source files:
    1. OtherProtocol.swift that defines the OtherProtocol protocol
      public protocol OtherProtocol {
          func otherMethod()
      }
      
    2. Concrete+OtherProtocol.swift that extends Concrete to conform to OtherProtocol, nothing else in the file:
      import FrameworkA
      
      extension Concrete: OtherProtocol {
          public func otherMethod() {
              print("CONCRETE OTHER METHOD")
          }
      }
      
  4. In FrameworkBTests, a single test case file:
    import XCTest
    
    import FrameworkA
    import FrameworkB
    
    class FrameworkBTests: XCTestCase {
    
        func testConformance() {
            let abstract: Abstract = Concrete()
    
            XCTAssertNotNil(abstract as? OtherProtocol)
        }
    }
    

Running the test above will fail every time, because the conformance of Concrete to OtherProtocol is not loaded during static linking. Even adding other public symbols to that file does not make a difference unless they are referenced by the test target as well.

This is similar to Objective-C files that had only a category not being loaded correctly. I can work around it in a few different ways, including use of the -all_load or -force_load flags, explicitly referencing the conformance in the tests, or moving the conformance into the same file as the OtherProtocol definition.

I'm also not thrilled about the linker flags since it puts additional burden on using the frameworks downstream, and means that behavior might vary based on if that flag is set (maybe vary between tests and actual usage unintentionally). I wanted to understand a few things:

  1. What would be the preferred workaround for this issue?
  2. If using -all_load or -force_load, is dead code stripping more effective for Swift compared to Objective-C? Will any non-Objective-C-backed Swift code be stripped even if initially loaded by those flags?

I also had a thought about using an extension on Never to load the conformances explicitly in a file I know is loaded (without putting the conformance itself in that file), and it seems to work. This ultimately felt off and I've decided against it for now, but I was also curious how robust something like this would be against future optimization:

@inline(never)
public func doNotOptimize<T>(_ _: T) { }

extension Never {
    internal func _loadConcreteConformanceToOtherProtocol() {
        doNotOptimize(Concrete() as OtherProtocol)
    }
}

(Brentley Jones) #2

I just ran into this very same issue :cry:.


(Joe Groff) #3

This is a bug. We should mark the conformance as used if the protocol and conforming type are both public. -force_load-ing the object file is probably your best workaround.


(Brentley Jones) #4

@Joe_Groff Was there an SR created for this? I wanted to link it somewhere :slight_smile:.