Crash casting class from obj_copyClassList to a type

This is similar to this post https://developer.apple.com/forums/thread/700770 on using objc_copyClassList to obtain the available classes. When iterating the list, I try casting the result to an instance of a protocol and that works fine:

    protocol DynamicCounter {
        init(controlledByPlayer: Bool, game: Game)
    }

    class BaseCounter: NSObject, DynamicCounter {
    }
    static func withAllClasses<R>(
      _ body: (UnsafeBufferPointer<AnyClass>) throws -> R
    ) rethrows -> R {

      var count: UInt32 = 0
      let classListPtr = objc_copyClassList(&count)
      defer {
        free(UnsafeMutableRawPointer(classListPtr))
      }
      let classListBuffer = UnsafeBufferPointer(
        start: classListPtr, count: Int(count)
      )

      return try body(classListBuffer)
    }
    
    static func initialize() {
        let monoClasses = withAllClasses { $0.compactMap { $0 as? DynamicCounter.Type } }
        for cl in monoClasses {
            cl.initialize()
        }
    }

The above code works fine if I use DynamicCounter.Type on the cast but crashes if try casting to BaseCounter.Type instead.

Is there a way to avoid the weird and non Swift classes?

Answered by DTS Engineer in 811697022

Perfect! Thanks.

So I’m testing this in a command-line tool rather than the REPL. My experience is that, when dealing with odd stuff, it’s best to avoid both the REPL and playgrounds.

Oh, and I’m testing an macOS 14.7, although I don’t expect the exact macOS version to matter.

In my test setup I was able to get your code working with this change:

static func initialize() {
    let monoClasses = withAllClasses { $0.compactMap { thisClass -> BaseCounter.Type? in
        guard class_getSuperclass(thisClass) != nil else {
            print("no super, class: \(thisClass)")
            return nil
        }
        return thisClass as? BaseCounter.Type
    } }
    for cl in monoClasses {
        let o = cl.init(false)
        print(o)
    }
}

In short, I pre-filter out all base classes, that is, classes with no super classes. The end result is this:

no super, class: Object
no super, class: __NSGenericDeallocHandler
no super, class: __NSAtom
no super, class: _NSZombie_
no super, class: __NSMessageBuilder
no super, class: NSProxy
no super, class: _TtCs12_SwiftObject
no super, class: NSObject
<xxst.BaseCounter: 0x6000010100d0>
<xxst.TestCounter: 0x6000010100d0>

As you can see, there’s a bunch of classes that get filtered out. These classes are all ‘weird’, and something about them is triggering the Swift runtime to trap. Filtering them out is fine, because BaseCounter and its subclasses all have an clear superclass.

I think it’d be reasonable for you to file a bug against the Swift runtime suggesting that it do this filtering itself. Please post your bug number, just for the record.

Share and Enjoy

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

Please post a minimal example that reproduces the crash.

I tried to do this with the snippet you posted because there’s stuff missing, like Game and DynamicCounter, and I’m not sure if that actually impacts on this issue.

Share and Enjoy

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

Here's a recreate example with swift repl:

import Foundation

protocol DynamicCounter {
     init(_ isPlayer: Bool)
}

class BaseCounter: NSObject, DynamicCounter {
    required init(_ isPlayer: Bool) {
    }
}

class TestCounter: BaseCounter {
    required init(_ isPlayer: Bool) {
        super.init(isPlayer)
    }
}


class Helper {
    static func withAllClasses<R>(
      _ body: (UnsafeBufferPointer<AnyClass>) throws -> R
    ) rethrows -> R {

      var count: UInt32 = 0
      let classListPtr = objc_copyClassList(&count)
      defer {
        free(UnsafeMutableRawPointer(classListPtr))
      }
      let classListBuffer = UnsafeBufferPointer(
        start: classListPtr, count: Int(count)
      )

      return try body(classListBuffer)
    }
    
    static func initialize() {
        let monoClasses = withAllClasses { $0.compactMap { $0 as? BaseCounter.Type } }
        for cl in monoClasses {
            cl.init(false)
        }
    }
}

Helper.initialize()

output:

2024-10-29 15:30:48.760664-0400 repl_swift[13210:34621315] *** NSForwarding: warning: object 0x200b5cc48 of class '__NSGenericDeallocHandler' does not implement methodSignatureForSelector: -- trouble ahead
2024-10-29 15:30:48.761418-0400 repl_swift[13210:34621315] *** NSForwarding: warning: object 0x200b5cc48 of class '__NSGenericDeallocHandler' does not implement doesNotRecognizeSelector: -- abort
Execution interrupted. Enter code to recover and continue.
Accepted Answer

Perfect! Thanks.

So I’m testing this in a command-line tool rather than the REPL. My experience is that, when dealing with odd stuff, it’s best to avoid both the REPL and playgrounds.

Oh, and I’m testing an macOS 14.7, although I don’t expect the exact macOS version to matter.

In my test setup I was able to get your code working with this change:

static func initialize() {
    let monoClasses = withAllClasses { $0.compactMap { thisClass -> BaseCounter.Type? in
        guard class_getSuperclass(thisClass) != nil else {
            print("no super, class: \(thisClass)")
            return nil
        }
        return thisClass as? BaseCounter.Type
    } }
    for cl in monoClasses {
        let o = cl.init(false)
        print(o)
    }
}

In short, I pre-filter out all base classes, that is, classes with no super classes. The end result is this:

no super, class: Object
no super, class: __NSGenericDeallocHandler
no super, class: __NSAtom
no super, class: _NSZombie_
no super, class: __NSMessageBuilder
no super, class: NSProxy
no super, class: _TtCs12_SwiftObject
no super, class: NSObject
<xxst.BaseCounter: 0x6000010100d0>
<xxst.TestCounter: 0x6000010100d0>

As you can see, there’s a bunch of classes that get filtered out. These classes are all ‘weird’, and something about them is triggering the Swift runtime to trap. Filtering them out is fine, because BaseCounter and its subclasses all have an clear superclass.

I think it’d be reasonable for you to file a bug against the Swift runtime suggesting that it do this filtering itself. Please post your bug number, just for the record.

Share and Enjoy

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

Thanks for the feedback and the solution. I have reported the issue via: https://feedbackassistant.apple.com/feedback/15639632

There was not direct choice for Swift runtime, so hopefully that is enough.

Just another side note, I ran into an unexpected issue on the main application code because of NSPreviewTargetController. I was not expecting a cast to cause this threading violation message:

WebKit Threading Violation - initial use of WebKit from a secondary thread.
FAULT: assertion failed: '+[NSRemoteView ensureAuxServiceAwareOfHostApp:] has been invoked on a thread which is incompatible with AppKit; the problem is likely in a much shallower frame in the backtrace' in +[NSRemoteView ensureAuxServiceAwareOfHostApp:] on line 1340 of file /AppleInternal/Library/BuildRoots/4ff29661-3588-11ef-9513-e2437461156c/Library/Caches/com.apple.xbs/Sources/ViewBridge/NSRemoteView.m
I was not expecting a cast to cause this threading violation message

It’s certainly weird, but its not hard to see how it could happen. If the cast ‘spins up’ the Objective-C class then its +initialize will run and that can do all sorts of odd things.

I shoulda asked about this earlier, but what’s the big picture here? This pattern of searching the class list for all the subclasses of a class, or all the classes that conform to a protocol, is not something we generally do on Apple platforms. That’s for exactly the reason you’re bumping into here: The dynamic nature of the Objective-C runtime can result in unexpected side effects.

Share and Enjoy

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

The use case is finding out the list of classes that confirm to a given protocol or are subclasses of a desired class. This avoids having to know the list of classes at compile time and having to maintain the list statically.

The use case is finding out the list of classes that confirm to a given protocol or are subclasses of a desired class.

Right. And I consider that to be an anti-pattern on Apple platforms. You can make it work, but it has all sort of negative consequences.

And that’s why I’m asking about the big picture. For example, if this were a tool that you only use during development, you should go to town! OTOH, if you plan to incorporate this into a shipping product, I’d advise you to rethink your choices.

Consider this test project, which is your code wrapped into something I can run:

import Foundation

class BaseCounter: NSObject { }

func withAllClasses<R>(_ body: (UnsafeBufferPointer<AnyClass>) throws -> R) rethrows -> R {

    var count: UInt32 = 0
    let classListPtr = objc_copyClassList(&count)
    defer {
        free(UnsafeMutableRawPointer(classListPtr))
    }
    let classListBuffer = UnsafeBufferPointer(
        start: classListPtr, count: Int(count)
    )

    return try body(classListBuffer)
}

func test() {
    let monoClasses = withAllClasses {
        $0.compactMap { thisClass -> BaseCounter.Type? in
            guard class_getSuperclass(thisClass) != nil else {
                print("no super, class: \(thisClass)")
                return nil
            }
            return thisClass as? BaseCounter.Type
        }
    }
    print("done, result: \(monoClasses)")
}

test()

Now add this to the project:

@import Foundation;

@interface InnocentBystander : NSObject

@end

@implementation InnocentBystander

+ (void)initialize {
    NSLog(@"+[InnocentBystander initialize]");
}

@end

When you run the code you see this:

no super, class: Object
no super, class: __NSGenericDeallocHandler
no super, class: __NSAtom
no super, class: _NSZombie_
no super, class: __NSMessageBuilder
no super, class: NSProxy
no super, class: _TtCs12_SwiftObject
no super, class: NSObject
+[InnocentBystander initialize]
done, result: [xxst.BaseCounter]

So, just the act of running your code has triggered all Objective-C class initialisers in your process. That’s not good.

Share and Enjoy

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

I ended up finding that I needed to exclude any classes not part of the module by doing something like

let name = class_getName(cl)
if memcmp(name, "MyModule.", 9) != 0) {
  continue
}

That avoids the Objective-C class initialization problem and it is also much faster. Not perfect but good enough.

If there is a better way to avoid doing this that doesn't involve manually maintaining a list of classes, I can give it a try.

Crash casting class from obj_copyClassList to a type
 
 
Q