CaseIterable for C-imported enums

From https://github.com/apple/swift-evolution/blob/main/proposals/0194-derived-collection-of-enum-cases.md we learn that:

  • Enums imported from C/Obj-C headers will not participate in the derived CaseIterable conformance.

It appears that there has been no evolution in this area, nor have I been able to find any discussion on the topic.

We can learn a little more about how Swift imports C enums here: https://github.com/apple/swift/blob/main/docs/HowSwiftImportsCAPIs.md#enums
And there has been some enum evolution here: https://github.com/apple/swift-evolution/blob/main/proposals/0192-non-exhaustive-enums.md

Synthesizing allCases for C-imported enums remains unavailable, and it isn't possible to trigger it either, since we do not control the file declaring the imported enum:

extension SpectreKeyPurpose: CaseIterable {} // Extension outside of file declaring enum 'SpectreKeyPurpose' prevents automatic synthesis of 'allCases' for protocol 'CaseIterable'

Given that there is real value in iterating enum cases, in particular for well-defined enum types such as those that are frozen or based on NS_ENUM, can the Swift team share what was the original motivation for excluding CaseIterable conformance for C-imported enums, and is there any interest in revisiting this?

5 Likes

Native Swift enums aren't automatically CaseIterable either, so maybe it's just for consistency? Not all enums make sense to be runtime-iterable, semantically.

And the more recent decision to disallow protocol conformances outside the original module might have just overlooked this case.

C enums are just a thin wrapper around a potentially arbitrary set of integers. There’s an argument to be made for NS_CLOSED_ENUM, but this isn’t possible for NS_ENUM since MyEnum(rawValue: 0x20fb27ba) might very well be a valid private enum case that’s understood by the C but not exposed in a header.

2 Likes

So I was going to say that too, but CaseIterable never promises to return every case, so having a default CaseIterable that returns “the public cases present at compilation time” wouldn’t be too weird. But then you run into trouble if a case is ever added, because you could use a library compiled a while ago with your brand new app and they’d disagree on what cases should be included. Since the conformance is synthesized on the use side, not in the system library, there’s no central place for it to be updated when a new case has been added.

2 Likes

Huh? From the docs:

CaseIterable

A type that provides a collection of all of its values.

That seems pretty explicit that any possible value ought to be contained in the allCases collection (the docs for which reiterate the "all values" terminology).

1 Like

Given that it's not possible in C/Objective-C itself to enumerate all the values of an enum (leading to the "define a case equal to the last case + 1" trick), the question I would ask is "why do you want to iterate all of the cases?"

Since CaseIterable synthesis requires the conformance to be stated in the same file where the enum is declared, then that capability is effectively something that should be considered to be an innate characteristic of the type—like Equatable and Hashable, it's either there by design or it isn't. Given that it's impossible for the module that owns the enum to own the conformance(†), I think it would be an overreach to automatically synthesize in this case (especially due to the issue @jrose points out above).

(†) The one situation where I could rationalize doing this synthesis would be if we were compiling a Swift overlay for an underlying Objective-C module.

2 Likes

Oh, fair enough. I was thinking about how the compiler can’t guarantee that a custom implementation covers every value (say, for a struct), but the documentation does make it sound like an implementation shouldn’t be excluding values, and with C enums other than NS_CLOSED_ENUM we could never be sure of that. So we’re back to what Harlan said, yeah.

1 Like

By that same token, though, doesn't it stand to reason that synthesis of Swift code for C-imported types is exactly that area of responsibility which represents the innate characteristics of the type?

Certainly, I agree that there is no way for a C enum to "declare" itself as being case iterable, and as such, there is no inherent way for the C type to declare a desire for such a conformance to be synthesized, but that's purely a consequence of a Swift language feature that is missing in C – ie. it's not a deliberate type choice to be absent a case iterability.

As such, the only way for an original C type to declare its conformance with a Swift language feature such as CaseIterable, is for the conformance to be declared in a separate file shipped alongside the original in C — a separate file where CaseIterable is added in an extension and synthesized accordingly. Alternatively, a C-level type annotation would be necessary, or we can assume that NS_CLOSED_ENUM implies such a conformance (does such an assumption do any harm?).

1 Like

The bar for whether we allow something for C decls that we don't for Swift decls isn't "do[es no] harm", though. (I would argue that there is harm in treating some classes of declaration different from others, in terms of how you reason about the code.) Typically we shouldn't synthesize any conformances that aren't explicitly requested by the author, and when there are exceptions (like Equatable/Hashable being synthesized implicitly for raw-value enums), since those precede the ability to synthesize Equatable/Hashable in general, the broad view is that if the general synthesis had come first, then we wouldn't implicitly synthesize those either.

For what it's worth, there's a capability in ClangImporter that lets C/Objective-C code declare custom attributes that should be injected when it's imported into Swift. From NSObjCRuntime.h:

   // Indicates that the thing it is applied to should be imported as 'Sendable' in Swift:
   // * Type declarations are imported into Swift with a 'Sendable' conformance.
   // * Block parameters are imported into Swift with an '@Sendable' function type. (Write it in the same place you would put 'NS_NOESCAPE'.)
   // * 'id' parameters are imported into Swift as 'Sendable', not 'Any'.
   // * Other object-type parameters are imported into Swift with an '& Sendable' requirement.
#  define NS_SWIFT_SENDABLE __attribute__((swift_attr("@Sendable")))

I wonder if some variation of this, which could be used to add a conformance to the originating C declaration, might be more palatable than trying to make it work from Swift. (If that's a direction someone wanted to explore, it should still only apply to NS_CLOSED_ENUM.)

2 Likes

There are code size reasons to not automatically synthesize CaseIterable, or indeed other conformances like Equatable, Hashable, and CustomStringConvertible, on un-annotated imported types. Specifically, there are two code size reasons, and their relationship is multiplicative:

  1. Because of dynamic casting, you can't actually tell which types you need a conformance for, so you end up having to create conformances for every eligible type on the off chance there's an as? cast somewhere that needs it.
  2. What's worse, there's no central place to emit these conformances where all clients of a given module will see it, so every client has to emit a conformance for every imported type it can see.

A typical AppKit/UIKit/SwiftUI app or framework imports thousands of Objective-C types and uses only a small fraction of them, so you can see how this would quickly balloon code size—and every time you split your project into smaller frameworks, the problem would get worse, because each of those frameworks would have to emit its own set of conformances.

Having said that, it would be practical to make it so that the Swift overlay of an Objective-C module, or the Swift half of a mixed-language target, could declare an extension with a conformance and no implementation to apply the automatic synthesis logic that's used for pure Swift types. This would avoid both halves of the code-size issue: the code is only generated for types that opt in, and the generated code is emitted into one module and shared by all clients.

This approach—where only the module or its overlay would be able to synthesize the conformance—would work alright for NS_ENUM and NS_TYPED_ENUM too. After all, if the module added a case the automatic synthesis would update allCases, and the Swift implementation should always match the Objective-C frameworks in use in the process.

(And yes, it would still be valid to have private cases that wouldn't appear in allCases, but…so what? CaseIterable is a convenience; it doesn't promise that there are no values that would match @unknown default.)

8 Likes

Just want to reiterate from above that to my reading, this is contradicted by the docs. As currently presented, it would IMO be a violation of documented CaseIterable semantics for a type to admit values which do not appear in the allCases collection.

I have run into that recently as well while defining structs and enums in C that are shared between Metal and Swift. I'm owning these types and it would be nice to somehow get Equatable and Hashable conformance synthesised. I don't mind adding attributes to the C definition at all. This appears to be working for enums if I declare them using NS_ENUM like that:

#ifdef __METAL_VERSION__
#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type
#else
#import <Foundation/Foundation.h>
#endif /* __METAL_VERSION__ */

typedef NS_ENUM(uint8_t, Foo) {
    bar,
    baz
};

but I haven't found a way to do the same with structs. I could image we could add another attribute to also make C enums conform to CaseIterable.

Edit: just resized that enums in Swift get this kind of for free anyway and you don't actually need the NS_ENUM but can use e.g. __attribute__((enum_extensibility(closed))); instead.