objc_copyClassList() not enumerable?

This is a derivative of this and this.

On iOS 15(.3.1), if I just try to fetch and enumerate the classes of the objc runtime, waiting until viewDidAppear() to make sure there wasn't some initialization issue:

var count = UInt32(0)
var classList = objc_copyClassList(&count)!
print("COUNT \(count)")
print("CLASS LIST \(classList)")
for i in 0..<Int(count) {
    print("\(i)")
    classList[i]
}

produces the following before a Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1820e0cdc)

COUNT 28353
CLASS LIST 0x000000010bf24000
0
1
2
2022-02-17 16:24:02.977904-0800 TWiG V[2504:705046] *** NSForwarding: warning: object 0x1dbd32148 of class '__NSGenericDeallocHandler' does not implement methodSignatureForSelector: -- trouble ahead
2022-02-17 16:24:02.978001-0800 TWiG V[2504:705046] *** NSForwarding: warning: object 0x1dbd32148 of class '__NSGenericDeallocHandler' does not implement doesNotRecognizeSelector: -- abort

I don't know how to do any less with it than just fetching the value. I'm not trying to print it or anything, and yet it still fails. Is there some magic I'm missing?

Why have the API if the results crash your program? If the issue is legit, it would be nice of the docs pointed out a workaround, or at least the proper way to cope with the result.

(I do not have hardened runtime turned on, XCode 13.2.1)

Accepted Reply

The class list returned by the Objective-C runtime contains all sorts of weird and wonderful stuff. If you pick an arbitrary class out of the list, you can’t rely on it behaving in any useful way. For example, one of the classes in the list is _NSZombie_ and if you call any methods on that then you’re in for a shock (-:

I’m not 100% sure why but your Swift code is triggering a call into the Swift dynamic cast infrastructure, and that has sent the -methodSignatureForSelector: message to the __NSGenericDeallocHandler class. The __NSGenericDeallocHandler doesn’t implement that method, and that triggers a language exception.

The take-home lesson here is that, if you’re working with an arbitrary class you discover via the Objective-C runtime, you have to be very careful what you do with it. I recommend that you use Objective-C runtime calls to interrogate the class to make sure it behaves reasonably before you let it ‘escape’ into code that you don’t control, like the Swift runtime.

For example, this code runs just fine:

var count: UInt32 = 0
let classList = objc_copyClassList(&count)!
defer { free(UnsafeMutableRawPointer(classList)) }
let classes = UnsafeBufferPointer(start: classList, count: Int(count))
for cls in classes {
    print(String(cString: class_getName(cls)))
}

The only thing is does with cls — other than retains and releases, which always work — is call the Objective-C runtime routine class_getName.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Replies

The class list returned by the Objective-C runtime contains all sorts of weird and wonderful stuff. If you pick an arbitrary class out of the list, you can’t rely on it behaving in any useful way. For example, one of the classes in the list is _NSZombie_ and if you call any methods on that then you’re in for a shock (-:

I’m not 100% sure why but your Swift code is triggering a call into the Swift dynamic cast infrastructure, and that has sent the -methodSignatureForSelector: message to the __NSGenericDeallocHandler class. The __NSGenericDeallocHandler doesn’t implement that method, and that triggers a language exception.

The take-home lesson here is that, if you’re working with an arbitrary class you discover via the Objective-C runtime, you have to be very careful what you do with it. I recommend that you use Objective-C runtime calls to interrogate the class to make sure it behaves reasonably before you let it ‘escape’ into code that you don’t control, like the Swift runtime.

For example, this code runs just fine:

var count: UInt32 = 0
let classList = objc_copyClassList(&count)!
defer { free(UnsafeMutableRawPointer(classList)) }
let classes = UnsafeBufferPointer(start: classList, count: Int(count))
for cls in classes {
    print(String(cString: class_getName(cls)))
}

The only thing is does with cls — other than retains and releases, which always work — is call the Objective-C runtime routine class_getName.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

This method of enumeration worked for me. I see 3 differences:

  • Yours included the defer/dealloc
  • Yours created an UnsafeBufferPointer and used the for each in idiom
  • Yours does not use the subscript access of the UnsafeMutableRawPointer

I wish I understood better which was making the difference, but for now, I'm just happy to have a working solution. Thank you.

I see 3 differences

All of those are best practice but they’re not the special sauce that makes this work. Rather, I avoided this line in your code:

classList[i]

That line trigger’s Swift’s dynamic cast infrastructure, which relies on -methodSignatureForSelector:, which isn’t implemented by the __NSGenericDeallocHandler class.

So, the special sauce is this:

if you’re working with an arbitrary class you discover via the Objective-C runtime, you have to be very careful what you do with it. I recommend that you use Objective-C runtime calls to interrogate the class to make sure it behaves reasonably before you let it ‘escape’ into code that you don’t control, like the Swift runtime.

In my example I used the Objective-C runtime routine class_getName to get the class name, but there are a bunch of other things that you can do to identify that you’re working with a reasonable class before you let it escape into general-purpose code, like the Swift runtime.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"