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.

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.

Crash casting class from obj_copyClassList to a type
 
 
Q