5

Given the method

func enumCaseCount<T: Hashable>(ofType type: T.Type) -> Int {
    // Needed check if type is an enum type

   return 3
}

Used as follows

private enum SimpleEnum: String {
    case a = "A"
    case b = "B"
    case c = "C"
}

enumCaseCount(ofType: SimpleEnum.self)

Any idea how to check if the given metatype is an enum?


Classes can be tested this way

class Test {}
Test.self is AnyClass // returns true
Binarian
  • 12,296
  • 8
  • 53
  • 84
  • 1
    What you are looking to do is not possible. First of all, there's no generic enum type, since enums have nothing in common, so you cannot check if a type is an enum or not. Second of all, you cannot iterate through the cases of an enum, so you wouldn't be able to programatically determine how many cases an enum has. See [this](https://stackoverflow.com/questions/27094878/how-do-i-get-the-count-of-a-swift-enum) Q&A, but beware all workaround there require your enums to have a specific raw type, so they don't work on any arbitrary enum. – Dávid Pásztor Jan 06 '18 at 12:27
  • 3
    Once Swift is ABI stable, you *could* [climb through the metadata yourself](https://github.com/apple/swift/blob/master/docs/ABI/TypeMetadata.rst) to do this. I wouldn't advise doing so until that point though. However once https://github.com/apple/swift-evolution/pull/114 (hopefully) comes around at long last, you'll just be able to use `ValueEnumerable`. – Hamish Jan 06 '18 at 12:42
  • @Hamish Weirdly enough I have never seen the `ValueEnumerable` proposal, even though that PR has been alive for quite some time; that looks really neat. Recently updated, though: do you think this is something we'll see _"soon"_? (Already in Swift 4.1?) – dfrib Jan 06 '18 at 13:27
  • @dfri Unfortunately, assuming it does get accepted (it hasn't even been scheduled for review yet), it [probably won't surface until at least Swift 5](https://github.com/apple/swift-evolution/pull/114#issuecomment-336590026). Swift 4.1 is basically finished (just bug fixes now), so def won't make it into that. – Hamish Jan 06 '18 at 13:42
  • @Hamish I see, thanks. Btw, do you have any insight as to why `MemoryLayout.size` reports `0` byte when `T` is an `enum`? Are `enum`s (themselves) considered zero sized types in Swift, even if the `RawValue` has a non-zero size? – dfrib Jan 06 '18 at 13:53
  • @Hamish (For enums without associated values) It seems as if enums with single cases also reports size `0`; for several cases (and a specified `RawValue` type; even for one that that has a multi-byte size, say `Int` (`8`)) a single byte `1` is reported. It seems more coherent for enums with associated values: single-case enums report the size of the sum of the associated values of the single case, whereas multi-case enums report size as if a union (largest case size) plus one byte. Kind of weird size reporting, imo, for the enums that do not hold associated values. – dfrib Jan 06 '18 at 14:14
  • ... It would seem as if (for all enums) zero- or single-case enums are reported as `0`-sized (for the enum itself), as well as the size of the single case if this is one with associated values. For enums with no associated values, e.g. the common `RawValue` size seems never to be included when reporting the size of the `enum`. – dfrib Jan 06 '18 at 14:16
  • @dfri If the `enum` has more than one case, then `MemoryLayout.size` shouldn't be `0`. If however the `enum` has one case, then the size can indeed be zero (as there's only one possible value to represent). The presence of a raw value doesn't impact this, as raw values are calculated, not stored directly (this makes sense for resilience and can save memory). Although note that the *stride* can't be zero in such a case, as an array of a single-value enum can't be zero in size. – Hamish Jan 06 '18 at 14:17
  • In addition, if an enum has no cases, then it's an uninhabited type (a type with no values), so `MemoryLayout.size` *can* be `0` (though really the size is just undefined as you can't have a value). – Hamish Jan 06 '18 at 14:17
  • @Hamish Ah, if raw values are calculated, this would explain it all (and also explain why associated values will reflect on the `enum` size, as these naturally needs to be stored). Ninja-edit: my observations in the comments placed mean time are fully explained by your latest comments (on-the-fly calculation of raw values), thanks :) – dfrib Jan 06 '18 at 14:18
  • 1
    @dfri Yup, if you're interested, the current layout details for enumerations is given here: https://github.com/apple/swift/blob/415cd50ba21ceb08dbae4cabdde9035e89f59be1/docs/ABI/TypeLayout.rst#fragile-enum-layout. Note that the compiler can use extra inhabitants, i.e bit patterns that don't form valid values of the enum (and spare bits to form extra inhabitants) to store the "discriminator" (which case the value represents), so an additional byte doesn't always have to be added for enums with associated values :) – Hamish Jan 06 '18 at 14:22
  • @dfri The `ValueEnumerable` proposal is now under review :D – Hamish Jan 08 '18 at 19:19
  • @Hamish nice! \o/ – dfrib Jan 08 '18 at 21:03

3 Answers3

4

For the fun of it, as a (workaround) hack, we could instantiate an instance of T and perform runtime instrospection on it using Mirror, specifically its displayStyle property. Before we proceed, we note that we'll only use this for debugging purposes

Mirrors are used by playgrounds and the debugger.

I'll also point out that we're really chasing our tail here as we resort to runtime to query things known (by the compiler, at least) at compile time.


Anyway, first of all, I'll rename enumCaseCount(...) to isEnum(...), as this question only covers querying whether a metatype is an enum or not. For similar (somewhat brittle) hacks to query the number of cases of a given enum, see:

Now, the generic placeholder T in isEnum(...) only knows that it is a type conforming to Hashable, which doesn't give us any straight-forward way to instantiate an instance of T (if Hashable blueprinted, say, an initializer init(), we'd could readily construct an instance of T an perform runtime introspection upon it). Instead, we'll resort to manually allocating raw memory for a single T instance (UnsafeMutableRawPointer.allocate(bytes:alignedTo:)), binding it to T (bindMemory(to:capacity:)), and finally deallocating the memory (deallocate(bytes:alignedTo:)) once we've finished our runtime introspection of the instance referenced to by the pointer to the bound memory. As for the runtime introspection, we simply use Mirror to check whether its displayStyle is enum or not.

func isEnum<T: Hashable>(_: T.Type) -> Bool {
    var result = false
    // Allocate memory with size and alignment matching T.
    let bytesPointer = UnsafeMutableRawPointer.allocate(
        bytes: MemoryLayout<T>.size,
        alignedTo: MemoryLayout<T>.alignment)
    // Bind memory to T and perform introspection on the instance
    // reference to by the bound memory.
    if case .some(.`enum`) = Mirror(reflecting:
        bytesPointer.bindMemory(to: T.self, capacity: 1).pointee)
        .displayStyle {
        print("Is an enum")
        result = true
    } else { print("Is not an enum") }
    // Deallocate the manually allocate memory.
    bytesPointer.deallocate(bytes: MemoryLayout<T>.size,
                            alignedTo: MemoryLayout<T>.alignment)
    return result
}

Example usage:

enum SimpleEnum { case a, b, c }

enum SimpleStrEnum: String {
    case a = "A"
    case b = "B"
    case c = "C"
}

enum SimpleExplicitIntEnum: Int { case a, b, c }

struct SimpleStruct: Hashable {
    let i: Int
    // Hashable
    var hashValue: Int { return 0 }
    static func ==(lhs: SimpleStruct, rhs: SimpleStruct) -> Bool { return true }
}

print(isEnum(SimpleEnum.self))            // true
print(isEnum(SimpleStrEnum.self))         // true
print(isEnum(SimpleExplicitIntEnum.self)) // true
print(isEnum(SimpleStruct.self))          // false
dfrib
  • 70,367
  • 12
  • 127
  • 192
  • This is a really interesting hack, although note you're breaking a precondition of [`withMemoryRebound(to:capacity:)`](https://developer.apple.com/documentation/swift/unsafepointer/2430863-withmemoryrebound) – "*The type `T` must be the same size and be layout compatible with the pointer’s `Pointee` type.*", so I'm not convinced that it's well-defined behaviour. – Hamish Jan 06 '18 at 14:33
  • @Hamish Yes that clause is most certainly not guaranteed, so this is probably UB :/ Ninja-edit: also, _"After executing body, this method rebinds memory back to the original Pointee type."_, so I probably don't want to perform introspection on the return value of the closure (although this should have been copied in the case of value types, but the method could be invoked for reference types, in which case I'm unsure what `value` would store). – dfrib Jan 06 '18 at 14:36
  • @Hamish could we meet those pre-conditions, by construction of `chunk`? In the closure of `withUnsafePointer(...)` above, `Pointee` should be `[UInt8]`, but I guess the size of the `Pointee` will depend on the size of the `chunk`, then? Possibly this could be aligned to the size of `T`, but I'm on thin ice here :) as for the layout, wouldn't a 1-stride byte array (large enough) be compatible with any single-instance larger type (with a size smaller or equal to the array), assuming all our types reside in contiguous memory? (Although if the larger single-instance type has padding in it...) – dfrib Jan 06 '18 at 14:53
  • 1
    I don't believe Swift has actually defined any formal rules around layout compatibility (this should come with ABI stability), so technically we can't really reason about whether two types are layout compatible or not (though it's safe to assume for obvious cases like types with equivalent structures). So unfortunately, I'm not sure it's possible to go about this in a guaranteed well-defined manner. Although note that you have a pointer to a `[UInt8]`, which is only a word long (arrays store their contents indirectly), so you can't treat that as a buffer of `MemoryLayout.size` `UInt8`s. – Hamish Jan 06 '18 at 15:28
  • 1
    (If you wanted a buffer of `UInt8`s, you could call `withUnsafeBufferPointer(_:)` on the array) – Hamish Jan 06 '18 at 15:33
  • @Hamish Thanks, will look into later (although I believe for a `UnsafeBufferPointer` I wont have access to `withMemoryRebound(...)`, but the existing use of the `[UInt8]` array above is not as intended; didn't know also arrays stored their contents indirectly!). – dfrib Jan 06 '18 at 15:50
  • No worries, and you should be able to call `.withMemoryRebound` on the (unwrapped) `baseAddress` of the buffer pointer (which is an `UnsafePointer`), though I'm not sure that'd actually help in this case as you'd need `MemoryLayout.size == MemoryLayout.size`, at which point you might as well have a single value and not an array. – Hamish Jan 06 '18 at 16:27
  • @Hamish Ah yes, thanks. Would be nice to know the details of how harsh the _size_ precondition (e.g. in this case, `MemoryLayout.size == MemoryLayout.size`) to the (temp.) mem-rebound really is; here we know that our pointer (although `UInt8`) actually is just the first element of a contiguous buffer. Most likely UB nonetheless, to invoke `withMemoryRebound` for, say, a `MemoryLayout.size` which is larger than `MemoryLayout.size`; even if we know (from the `reserveCapacity(...)` call) that `MemoryLayout.size =< MemoryLayout<"Buffer">.size`. Anyway, was a fun exercise! – dfrib Jan 06 '18 at 17:50
  • 1
    Looking [at the source](https://github.com/apple/swift/blob/0782b482b364f15741cc4a445976d8167c1a9d25/stdlib/public/core/UnsafePointer.swift.gyb#L812), the `capacity:` passed is used to both temporarily bind the memory to `T`, but also to rebind to the original type, so if the strides differ you could bind e.g 5 instances of `Pointee` to 1 instance of `T`, but then you'd only rebind 1 instance of `Pointee`, which could be problematic (actually the updated docs for `withMemoryRebound` ask for the same size *and* stride). – Hamish Jan 06 '18 at 18:32
  • 1
    Also worth noting that [the new `withMemoryRebound` method](https://github.com/apple/swift/blob/0782b482b364f15741cc4a445976d8167c1a9d25/stdlib/public/core/UnsafeBufferPointer.swift.gyb#L539) (to be introduced in 4.1) has an actual `_debugPrecondition` that `MemoryLayout.stride == MemoryLayout.stride`. So overall, I'd say it's a pretty strict precondition. – Hamish Jan 06 '18 at 18:32
  • What you could do instead is use `bindMemory` on a raw pointer to the array's buffer (using the `.withUnsafeBytes` method) in order to bind to `T`, get the instance of `T` (the pointee), and then re-bind the memory back to the array's element type. That would work as you can pass different capacities, so there's no size-equivalence precondition; only a layout-compatibility one. – Hamish Jan 06 '18 at 18:40
  • Hang on a sec, there's no need to go through any of that palaver; [just allocate temporary memory of type `T` and get the pointee](https://gist.github.com/hamishknight/b3988f22334f07682258d8f98fffa493). The value of of type `T` is still undefined, so I'm not convinced that using `Mirror` on it is well-defined, but it's less undefined than rebinding memory ;D. Though if `T` is a non-trivial type (enum with associated values that are reference types), you'll get further undefined behaviour when the compiler tries to insert retain/release calls to undefined references. – Hamish Jan 06 '18 at 19:05
  • @Hamish Nice, `bindMemory(...)` should do the trick! If we pair it with `UnsafeMutableRawPointer.allocate(bytes:alignedTo:)` (and `deallocate(...)`) instead of working with an array buffer, I _think_ we should be set for size as well as alignment, as we can customize our allocation for the purpose of binding the memory to `T`. But I have been wrong before when working with Swift and raw ptrs x) – dfrib Jan 06 '18 at 19:23
  • @Hamish I just saw your lastest comment after writing my own allocate/bind/deallocate chunk; your approach without the bind is even neater, so I think we've at least minimized UB some ... But I'm off for other stuff now, thanks for all the help and feedback! – dfrib Jan 06 '18 at 19:25
  • No worries, sorry for putting you through the countless re-edits! – Hamish Jan 06 '18 at 19:25
  • 1
    @Hamish Yes I guess the gist of it is that we can't seem to circumvent actually _initializing_ the allocated memory to an instance of `T` even with the raw pointer tricks - as we don't know anything regarding the initializers for `T` just from it being `Hashable`. No worries, I'm just glad for the feedback and the exercise rather than the resulting answer (which is still UB). I Swift way to seldom these days, so a Swift discussion (although as SO comments ...) is always appreciated! :) – dfrib Jan 06 '18 at 21:50
  • It throws an error if the type is a class, but with a guard `is AnyClass` it was easily fixed. – Binarian Jan 08 '18 at 08:33
1

As others have mentioned, there's no great non-hacky way to do this in Swift. However, it's one of the example use cases of Sourcery, a metaprogramming library (this means it analyzes your code to generate additional code). You write a Stencil template to describe its behavior, and it's executed as a build phase in Xcode. It can autogenerate this code for any enums found in your project.

AutoCases enum example

Connor Neville
  • 7,291
  • 4
  • 28
  • 44
0

To check whether a certain type is in fact an Enum you can use:

func isEnum<T>(_ type: T.Type) -> Bool {
    let ptr = unsafeBitCast(T.self as Any.Type, to: UnsafeRawPointer.self)
    return ptr.load(as: Int.self) == 513
}

And you can simply use it like so:

enum MyEnum {}
struct MyStruct {}
class MyClass {}

isEnum(MyEnum.self)   // this returns true
isEnum(MyStruct.self) // this returns false
isEnum(MyClass.self)  // this returns false

And in your example you would use it like so:

func enumCaseCount<T: Hashable>(ofType type: T.Type) -> Int {
   isEnum(T.self) // returns true if enum
   return 3
}
a7md
  • 123
  • 1
  • 7
  • That is interesting. Why is an enum pointer 513, any documentation/video? Is it official behavior or can it change at any time in the future? – Binarian Jul 08 '20 at 18:16
  • While what you're doing there is cool from the standpoint of knowledge of reverse engineering swift implementation, it's pretty worthless as a practical solution in anyone's app, because 513 is a 'magic number' and Apple is not bound to support it in any way. In fact there are rules for the app store that require one to use only documented/supported interfaces, which that is not. Further it is utterly obtuse insofar as nobody knows where that number came from or how to verify it. Everything about this is too clever to be of much use except as an intellectual exercise. – clearlight Aug 07 '22 at 17:08
  • 1
    @Binarian very sorry for the much overdue reply! Hadn't noticed your comment. This is what apple is using to extract type information in their ReflectionMirror APIs (check [here](https://github.com/apple/swift/blob/a3941bf215e6f9592ecae0649f99a660b3a3f45e/stdlib/public/core/ReflectionMirror.swift#L224)). – a7md Aug 08 '22 at 20:05
  • @clearlight thanks for your feedback. As mentioned in my comment above, this is what Apple is using in the standard library. So this wasn't the result of trial and error or any reverse engineering effort on my part. On the point that there is no guarantee this won't change in the future, I'm no language nor compiler expert, so I'm not really sure. My **guess** would be that, this is something that is baked into the language, and it won't change easily. Since we're not actually invoking any private APIs here, I'd argue it is relatively safe to use this code in production. – a7md Aug 08 '22 at 20:15
  • @a7md looks way too 'under the hood' to be appropriate for prod. code w/o very compelling reason. Those values could change any time for any reason w/o notice. Apple doesn't doc them or describe as public interfaces. And, while it seems unlikely they'd change, there's little reason to endure the risk which could result in catastrophic failure if they changed it out from under an app where it wasn't convenient for the engineers to revise the app. It's just tacky. And I am not above rev. engineering, and hacking up heuristics if necessary or prototyping, but that's just a maintenance no-no – clearlight Aug 08 '22 at 20:48
  • @a7md and finally the other problem is "magic" #'s are tacky, so then you have to make your own constant to define it since you don't have access to Apple's, and responsible production code would have to add a comment/reference, since people would need to be able to quickly know exactly where to look to see how it is used and be able to check for additions and differences. That has to be the avenue of last resort. – clearlight Aug 08 '22 at 20:50
  • Also you have to ask yourself - if Swift developers think people need this, why aren't they exposing it in the language after 5 major versions? Is anyone talking about it on any roadmap? Better than get into unsafe pointers and magic numbers, why don't you make a case (no pun intended) for it on the Swift language development forum? @dfrib's answer is much better, although still pretty extreme, and even he acknowledges it is a 'brittle hack'. – clearlight Aug 08 '22 at 20:59
  • I agree with you that this is hacky. I didn’t mean to allude to otherwise. I also agree with you that dfrib’s approach is better as it uses Apple’s API instead of the underlying mechanism, which is as you’ve mentioned still brittle. – a7md Aug 09 '22 at 04:12
  • I haven’t personally found a use case for needing to know the exact type of a variable and most developers won’t ever need to. That’s why an official API doesn’t exist. So what I should’ve probably emphasized in my answer is, this will always be hacky unless Apple provides something we could use. My approach is merely a single approach to the problem. I personally wouldn’t mind using this in production unless it’s something very critical that it has zero tolerance for failure. That’s why I said "... relatively safe to use this code in production" – a7md Aug 09 '22 at 04:19
  • I should probably word my answers a little bit better going forward! Thanks for the feedback – a7md Aug 09 '22 at 04:24