Swift regret: how C enums get imported— Jordan Rose (@UINT_MIN) October 6, 2021
This is another of those “there’s a right answer but it’s too late to change it” issues. The problem is that C enums have way fewer guarantees than Swift 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_OPTIONS macros and decide whether to import a C enum as a Swift enum or a bespoke OptionSet struct.
Aside: why are
NS_ENUMseparate? I don’t know! There’s a C++ benefit to having them separate:
kFooBar | kFooBazcan’t be assigned to a Foo-enum-typed variable, so
NS_OPTIONSdeclares 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_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.
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
This was my area of the compiler and I’m sorry. If you want your C enums to look good in Swift, use
NS_OPTIONS, or the GNU-style
flag_enum. (Why open/closed? “Private cases”, from above. See SE-0192.)