NSClassFromString unexpectedly returning nil for generic NSObject subclass

I'm finding that if I try to lookup a generic subclass of an NSObject by its name NSClassFromString, it only succeeds when that subclass was declared at the top level:

//  This always works, so long as it's declared at the top level:

class Solid<T>: NSObject { }

print(NSClassFromString(Solid<Int>.className())) // Optional(Solid<Swift.Int>)

//  This always fails:

func someFunction() {
    class Ephemeral<T>: NSObject { }
    
    print(NSClassFromString(Ephemeral<Int>.className())) // nil
}

someFunction()

The second example works just fine if the type is made non-generic.
Is this intentional and expected behavior, or is this a bug?

2 Likes

This also happens if any part of the final type signature is defined from the context of a function:

class SomethingGeneric<T>: NSObject { } // top level

func someFunction() {
    struct Local { } // function level
    
    print(NSClassFromString(SomethingGeneric<Local>.className())) // nil
}

_objc_realizeClassFromSwift works, so, that's interesting. :)

class AGeneric<T>: NSObject { }

func aFunction() {
    enum LocalType { }
    
    let klass = AGeneric<LocalType>.self
    
    print(NSClassFromString(klass.className())) // nil
    
    _objc_realizeClassFromSwift(klass, nil)
    
    print(NSClassFromString(klass.className())) // AGeneric<LocalType>
}

aFunction()

That function has a very entertaining doc comment:

/** 
 * Perform Objective-C initialization of a Swift class.
 * Do not call this function. It is provided for the Swift runtime's use only 
 * and will change without notice or mercy.
 */
5 Likes

This looks to me like a bug in realizing generic subclasses of NSObject, but only when nested inside of functions — with Swift 5.5.2:

import Foundation

func printClass<T: NSObject>(_ type: T.Type) {
    print(type, "=>", String(describing: NSClassFromString(T.className())))
}

class Generic<T>: NSObject {}
printClass(Generic<Int>.self)
// ↪︎ Generic<Int> => Optional(main.Generic<Swift.Int>)

private class PrivateClass: NSObject {}
printClass(PrivateClass.self)
// ↪︎ PrivateClass => Optional(main.(unknown context at $10df76c28).PrivateClass)

class TopLevelClass: NSObject {
    class NestedClass: NSObject {}
    class NestedGenericClass<T>: NSObject {}
    private class NestedPrivateClass: NSObject {}
    
    static func printNestedClasses() {
        printClass(NestedClass.self)
        // ↪︎ NestedClass => Optional(main.TopLevelClass.NestedClass)

        printClass(NestedGenericClass<Int>.self)
        // ↪︎ NestedGenericClass<Int> => Optional(main.TopLevelClass.NestedGenericClass<Swift.Int>)

        printClass(NestedPrivateClass.self)
        // ↪︎ NestedPrivateClass => Optional(main.TopLevelClass.(unknown context at $10df76d60).NestedPrivateClass)
    }
}

TopLevelClass.printNestedClasses()

func classesNestedInFunction() {
    class FunctionClass: NSObject {}
    printClass(FunctionClass.self)
    // ↪︎ FunctionClass => Optional(main.(unknown context at $10df76db0).(unknown context at $10df76db8).FunctionClass)
    
    class FunctionGenericClass<T>: NSObject {}
    printClass(FunctionGenericClass<Int>.self)
    // ↪︎ FunctionGenericClass<Int> => nil

    // Can't define classes as `private` to a function.
}

classesNestedInFunction()

Where all other types are being realized automatically (either statically, or at runtime), generic subclasses nested inside of functions specifically appear not to be. This is likely worth a report on bugs.swift.org.

2 Likes

Dropped this bug in the bugs.swift.org tracker (upvotes appreciated), and filed a radar as well.