Imported C enum must conform to `BinaryInteger`?

I'm still struggling to use SDL from Swift. I'm using a Swift library I found, which was working for me a couple weeks ago, but now I'm trying to add text support to it and I can't build any of it.

SDL_PixelFormatEnum I believe is an imported-from-C enum; at least SDL_PIXELFORMAT_INDEX1LSB is imported from the C headers. I thought those always came in as Int, so I'm not sure what's going on here.

/Users/rmann/Projects/Personal/InfoDisplay/InfoDisplay/Packages/SDL/Sources/SDL/PixelFormat.swift:83:60: error: initializer 'init(_:)' requires that 'SDL_PixelFormatEnum' conform to 'BinaryInteger'
    static let index1LSB = SDLPixelFormat.Format(rawValue: UInt32(SDL_PIXELFORMAT_INDEX1LSB))
                                                           ^
Swift.UnsignedInteger:2:23: note: where 'T' = 'SDL_PixelFormatEnum'
    @inlinable public init<T>(_ source: T) where T : BinaryInteger
                      ^

SDLPixelFormat.Format looks like:

public extension SDLPixelFormat {
    
    /// SDL Pixel Format Enum
    struct Format: RawRepresentable, Equatable, Hashable {
        
        public let rawValue: UInt32
        
        public init(rawValue: UInt32) {
            
            self.rawValue = rawValue
        }
    }
}

and in an extension:

public extension SDLPixelFormat.Format {
    
    /// SDL_PIXELFORMAT_INDEX1LSB
    static let index1LSB = SDLPixelFormat.Format(rawValue: UInt32(SDL_PIXELFORMAT_INDEX1LSB))

What’s happened here is that the Swift importer has decided to import SDL_PixelFormatEnum as a struct. Thus, in Swift, SDL_PIXELFORMAT_INDEX1LSB is not an enum case but a struct constant. Thus, this line:

static let index1LSB = SDLPixelFormat.Format(rawValue: UInt32(SDL_PIXELFORMAT_INDEX1LSB))

is trying to initialise a UInt32 from an SDL_PixelFormatEnum. To make this work, do this:

static let index1LSB = SDLPixelFormat.Format(rawValue: UInt32(SDL_PIXELFORMAT_INDEX1LSB.rawValue))

which you can simplify to this:

static let index1LSB = SDLPixelFormat.Format(rawValue: SDL_PIXELFORMAT_INDEX1LSB.rawValue)

I’m not sure what the exact rules the importer uses to decide whether to create an enum or a struct, but I can see what it chose to do the latter in this case. The SDL_PixelFormatEnum is a complex beast, full of macros and potentially overlapping cases.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes

Thanks Quinn. I'm trying to understand why it worked a couple weeks ago. Is there a chance this import behavior changed from Swift 5.0 to 5.1?

I seem to have some trouble keeping Xcode and SPM in sync, in the sense that Xcode won't always rebuild all the dependencies, but if I rebuild them with SPM (or attempt to, because it used Swift 5.1 and would fail), now Xcode fails when it tries to build my app. It's very confusing.

Is there a chance this import behavior changed from Swift 5.0 to 5.1?

I dug out my test project out of the trash and opened it in Xcode 11 (hence Swift 5). It behaves the same way, that is, SDL_PixelFormatEnum is imported as a struct, not an enum. *shrug*

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

I discovered something tonight. The difference in import seems to be if the enum is declared as

enum {
...
};

or

typedef enum {
...
} EnumName;

The latter comes in as a struct.

Hmmm, that’s not what I’m seeing. I created a new command-line tool test project, added an Objective-C class to give me a bridging header, and then added this to that bridging header:

enum Test1 {
    Test1Case1,
    Test1Case2
};

typedef enum {
    Test2Case1,
    Test2Case2
} Test2;

Both Test1 and Test2 comes through as structs.

This is with Xcode 11.3.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

What I saw was that SDL 2.0.9 (installed by default on Raspbian Buster) has an anonymous enum. SDL 2.0.10 (what I had installed on macOS) has the typedefed and named enum.

The one sure-fire way to get an enum to come through as an enum is to use NS_ENUM, so I dug into what that actually does. If you have a declaration like this:

typedef NS_ENUM(NSInteger, QQQTest) {
    QQQTestCase1,
    QQQTestCase2
};

it expands to this:

typedef enum __attribute__((enum_extensibility(open))) QQQTest : NSInteger QQQTest;
enum QQQTest : NSInteger {
    QQQTestCase1,
    QQQTestCase2
};

Note I’ve rewrapped this to make it easier to read.

I then started removing goo until it switched back to a struct. Eventually I got it down to this:

enum __attribute__((enum_extensibility(open))) QQQTest8 {
    QQQTest8Case1,
    QQQTest8Case2
};

It seems that the enum_extensibility attribute is key here. And based on that I took another look at SE-0192 Handling Future Enum Cases, and it turns out that this discusses this pretty extensively. Specifically, in the C enums section it says:

This section only applies to enums that Swift considers "true enums",
rather than option sets or funny integer values … the presence of
enum_extensibility(closed) or enum_extensibility(open) will
instruct Swift to treat the enum as a "true enum".

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like

Thanks for the deep dive. I don't mind if it's a struct or an enum, I just want it to stay consistent. Unfortunately, I can't use NS_ENUM, it's a header provided by a third-party. I'd love to add apinotes, but that's hard enough to do, and I don't know if it provides a facility for specifying enum/struct. The SDL header doesn't have anything special in the way it declares its enum, just enum { … }; in 2.0.9, and typedef enum { … } Name; in 2.0.10.

In any case this will all be moot if I can't get SPM to issue the right linker flags.

Thanks again!