Binary distributable XCFrameworks with Property Wrappers Issue

I’ve just run into an issue with a binary distributable XCFramework and stored properties with property wrappers.

We distribute a closed source iOS XCFramework written in Swift and have been chugging along just peachy. During the build we build both the simulator and device variants and use xcodebuild -create-xcframework to package it up all nice, including the .swiftinterface files.

Recently I came across a compatibility issue on our server side with how we were JSON encoding Int64s... The server is expecting a string and serving these values as strings (based on Protobuf). By default the JSON encoder/decoder deals with numbers. This exact scenario was discussed on the Swift Forums and the proposed solution was to use a property wrapper to handle that.

So I went down that route and all was good in the world locally. I went to cut a release and was surprised when my builds that consumed the framework via SPM began to fail.

Digging into it, I am getting the following error: Invalid redeclaration of synthesized property '_theValue'. Following that error through takes me to the .swiftinterface file were I ended up seeing something I was not expecting...

@frozen public struct TestStruct : Swift.Codable, Swift.Equatable {
  @PropertyWrapperTinkerSDK.StringRepresentedFixedWidthInt public var theValue: Swift.Int64 {
     get
  }
  private var _theValue: PropertyWrapperTinkerSDK.StringRepresentedFixedWidthInt<Swift.Int64>
  public init(theValue: Swift.Int64)
  public static func == (a: PropertyWrapperTinkerSDK.TestStruct, b: PropertyWrapperTinkerSDK.TestStruct) -> Swift.Bool
  public func encode(to encoder: Swift.Encoder) throws 
  public init(from decoder: Swift.Decoder) throws
}

Specifically, the private var _* declaration for the backing value.

It would seem to me that this being a private implementation detail shouldn’t even be in the .swiftinterface file. Manually removing these private var _* entries allows the consuming applications to compile and... seem to work... But it seems a bit scary going in here and manually modifying this file.

I was wondering if anybody else has seen this and has a recommendation of how to proceed?

1 Like

I'm more surprised that editing the .swiftinterface solves the issue, aren't these files nowadays just used for documentation purposes and the actual type info consumed by the compiler located in .swiftmodule?

I can’t explain the error, but the private property is included in the module interface because the struct is frozen. For frozen types, the compiler has to know the full memory layout.

You can see the same thing in other module interfaces. For example, if you go spelunking in SwiftUI's .swiftinterface file, you'll also find some private properties in there. Example:

@frozen public struct Font : Swift.Hashable {
  private var provider: SwiftUI.AnyFontBox
  …
}
1 Like

Frozen structs have to include all stored properties, so that clients know how big the struct is (that’s the main point of freezing a struct). Property wrappers can change how the field is stored, so they have to be included; there’s no special case for property wrappers that just wrap a single stored property of their wrapped type. (In theory the compiler could infer that if the property wrapper is also frozen.)

As for swiftinterface vs swiftmodule, the swiftinterface is the evolution-stable representation of the module, while the swiftmodule contains additional implementation details (for a library-evolution module, this is mostly for debugging purposes). Manually editing is of course not recommended, because you are lying to clients about what your library’s stable interface is, but in this case the lie is compatible.

EDIT: But the compiler should not be synthesizing storage for property wrappers in swiftinterface files, that’s definitely a bug. Arguably the property wrapper being exposed as public is also a bug, since you should be able to replace with a compatible property wrapper that has the same projected value type.

1 Like

I was indeed concerned about modifying the file and... lying..

The question then is, is there a way to work around the compiler error: Invalid redeclaration of synthesized property '_theValue'?

I mean, the simplest thing is to not use @frozen, and if you can’t do that you can implement init(from:) manually instead of using a property wrapper to get the string effect you want.

I'm trying to recall what prompted being @frozen in the first place. I think it was a case where older versions of iOS (11 & 12) were not able to consume the framework without crashing. But at this point I am not 100% certain as to what necessitated the addition of @frozen.

After some digging into the PR where the @frozen was added, I referenced this issue, [SR-11969] Public Structs crash iOS 12.x apps when their framework is built with BUILD_LIBRARY_FOR_DISTRIBUTION=YES · Issue #54394 · apple/swift · GitHub, which was indeed the reason why we marked our public structs as @frozen.