Conditionally downcasting CF types

At the moment, the following code triggers an error diagnostic:

let a: Any? = nil
let b = a as? CFDictionary // Conditional downcast to CoreFoundation type 'CFDictionary' will always succeed

I think the runtime should compare the type IDs using CFGetTypeID and then perform a cast, which will allow us to get rid of this diagnostic.

Would it be okay to add this check to the runtime? cc @Joe_Groff @John_McCall

Sure, I think we always had it in mind that we could support dynamic casting to CF types via the type-id mechanism. There are three big caveats, though:

  • The implementation has to handle the fact that foreign class metadata candidates for CF classes can't be relied on to include whatever additional information enables CF casting. That probably means that this has to be a side-table of information that we can emit when we emit a candidate rather than something we can assume is embedded in the metadata.
  • We know for a fact that CF type IDs don't always match the imported CF class hierarchy; in fact CF type IDs can't express a hierarchy at all. And we don't really know what all the potential compatibility landmines are here; very little code actually uses CF type IDs for anything.
  • Casting to toll-free-bridged types like CFDictionary would almost certainly be better handled by using ObjC casting to the NS equivalent type instead of using CF type IDs.

But if you want to look into it as a general matter, you're welcome to.

2 Likes

Something like this?

import Foundation

protocol CFType: AnyObject {
    static var typeID: CFTypeID { get }
}

protocol CFTollFreeBridgedType: CFType {
    associatedtype BridgedNSType
}

func cfCast<T: CFType>(_ v: Any, to type: T.Type = T.self) -> T? {
    let ref = v as CFTypeRef
    if CFGetTypeID(ref) == type.typeID {
        return (ref as! T)
    } else {
        return nil
    }
}

func cfCast<T: CFTollFreeBridgedType>(_ v: Any, to type: T.Type = T.self) -> T? {
    if let nsValue = v as? T.BridgedNSType {
        return (nsValue as! T)
    } else {
        return nil
    }
}

// =================================

extension CFString: CFTollFreeBridgedType {
    typealias BridgedNSType = NSString
    static var typeID = CFStringGetTypeID()
}

extension CFAllocator: CFType {
    static var typeID = CFAllocatorGetTypeID()
}

let cfString: Any = "foo" as CFString
let nsString: Any = "foo" as NSString
let swiftString: Any = "foo" as String
let cfNumber: Any = 1 as CFNumber
let nsNumber: Any = 1 as NSNumber
let swiftNumber: Any = 1 as Int
let cfAllocator: Any = kCFAllocatorSystemDefault as Any

cfCast(cfString, to: CFString.self)    // "foo"
cfCast(nsString, to: CFString.self)    // "foo"
cfCast(swiftString, to: CFString.self) // "foo"
cfCast(cfNumber, to: CFString.self)    // nil
cfCast(nsNumber, to: CFString.self)    // nil
cfCast(swiftNumber, to: CFString.self) // nil
cfCast(cfAllocator, to: CFString.self) // nil

cfCast(cfString, to: CFAllocator.self)    // nil
cfCast(nsString, to: CFAllocator.self)    // nil
cfCast(swiftString, to: CFAllocator.self) // nil
cfCast(cfNumber, to: CFAllocator.self)    // nil
cfCast(nsNumber, to: CFAllocator.self)    // nil
cfCast(swiftNumber, to: CFAllocator.self) // nil
cfCast(cfAllocator, to: CFAllocator.self) // CFAllocator

// usage
CFStringGetLength(cfCast(cfString)) // 3
1 Like

Yeah, looks about right.

1 Like

I’m making a package that should make interacting with the Keychain easier, lots of CF-prefixes and some weird casting issues. I was hoping to create a protocol and use it to cast values similarly to CFType in the example.

As someone who’s stumbled into this thread looking for a solution that would look very much like this, is it viable solution?

Or, to put it another way, do your caveats also apply to the example? Caveat three seems to be taken care of, but the first two stump me a bit.