Swift Regret: Unannotated C Enums

Part of the Swift Regrets series.

Swift has pretty strong support for importing C types, but sometimes it’s less than ideal about it. This particular issue is another of those “there’s a right answer but it’s too late to change it” cases. The problem is that C enums have way fewer guarantees than Swift enums: they’re basically glorified integer constants. You can even assign a case directly to an int without casting. So naturally, people used them for all sorts of things beyond “mutually exclusive values” (especially in Old C before static const was a thing). By far the two most common “extra” uses of enums are bitsets and just grouping related constants. And even for “regular” enums, there are sometimes “private cases”, integer values not listed in the original definition.

Fortunately for us at Apple, we’d already gone through a period where we had to be more explicit about enums: the 64-bit Intel transition. So we could look for the NS_ENUM and NS_OPTIONS macros and decide whether to import a C enum as a Swift enum or a bespoke OptionSet struct.

Aside: why are NS_OPTIONS and NS_ENUM separate? I don’t know! There’s a C++ benefit to having them separate: kFooBar | kFooBaz can’t be assigned to a Foo-enum-typed variable, so NS_OPTIONS declares Foo to be a typedef of the underlying integer instead. But older code would have already had this problem when they just used enum. Maybe it was fixing a longstanding complaint, or maybe it was something Clang was stricter about than GCC. (If you know, please tell me!)

Anyway, Swift had NS_ENUM and NS_OPTIONS covered. But what about everything else? Enums in pure C libraries? Well…we can take care of another easy case: anonymous enums. In this case the user was definitely trying to declare a bunch of constants. So we bring them into Swift as integer-typed globals. That’s the best we can do. (But note that it’s often the wrong integer type, because C defaults to int but also has a bunch of integer conversions. static const is better for this even if it’s more verbose.)

That leaves us with enums that fit in none of these buckets. They’re not known to be mutually-exclusive, but they’re also not known to be bitsets or constants. So Swift makes a type for them, a RawRepresentable struct, and imports the cases as globals of that type.

As globals!

The right answer was right there! It’s what we do for NS_OPTIONS, but without conforming to OptionSet: import the cases as members of the type, including prefix stripping (so kFooOptionsBar becomes FooOptions.bar).

This was my area of the compiler and I’m sorry. If you want your C enums to look good in Swift, use NS_ENUM, NS_CLOSED_ENUM, and NS_OPTIONS, or the GNU-style __attribute__((enum_extensibility(open))), enum_extensibility(closed), and flag_enum. (Why open/closed? “Private cases”, from above. See SE-0192.)