[SR-2388] Forming a CFDictionary from a Swift Dictionary is now really difficult Created: 18 Aug 2016  Updated: 24 Jul 2017  Resolved: 11 May 2017

Status: Resolved
Project: Swift
Component/s: Compiler

Type: Bug Priority: Medium
Reporter: Matt Neuburg Assignee: Jordan Rose
Resolution: Done Votes: 0
Labels: None
Environment:

Xcode Version 8.0 beta 6 (8S201h)


Attachments: Zip Archive bk2ch23p829imageIO.zip    
Radar URL: rdar://problem/32003073

 Description   

Before seed 6, I used to be able to say this:

        let d : [NSObject:AnyObject] = [
            kCGImageSourceShouldAllowFloat : true,
            kCGImageSourceCreateThumbnailWithTransform : true,
            kCGImageSourceCreateThumbnailFromImageAlways : true,
            kCGImageSourceThumbnailMaxPixelSize : w
        ]
        let imref = CGImageSourceCreateThumbnailAtIndex(src, 0, d)!

Now I have to take everything across the bridge by hand. All the CFString keys have to be cast as String or NSString. We don't get any automatic bridge-crossing, so we have to tell Swift explicitly what the value types are, and then we have to cast the resulting dictionary to CFDictionary as well. Even if we get past the compiler, it is still possible to do this wrong and thus end up with an invalid CFDictionary so that at runtime CGImageSourceCreateThumbnailAtIndex yields nil. The only way I've found to get things right is like this:

        let d = [
            kCGImageSourceShouldAllowFloat as String : true as NSNumber,
            kCGImageSourceCreateThumbnailWithTransform as String : true as NSNumber,
            kCGImageSourceCreateThumbnailFromImageAlways as String : true as NSNumber,
            kCGImageSourceThumbnailMaxPixelSize as String : w as NSNumber
        ]
        let imref = CGImageSourceCreateThumbnailAtIndex(src, 0, d as CFDictionary)!

This is absolutely horrible.

The situation is compounded by the fact that no one seems to have told NSDictionary that its Swift equivalent is now [AnyHashable : Any]. We see that in the APIs and it works, but when you try to cast a CFDictionary to it, the compiler complains:

        let result = CGImageSourceCopyPropertiesAtIndex(src, 0, nil)! as [AnyHashable:Any]
        // compile error

In my opinion, Swift now goes too far with its refusal to help things get across the bridges, the Swift to Objective-C bridge and the CFTypeRef toll-free bridge. (In fact, there now seems to be rather a heavy toll on that bridge. )



 Comments   
Comment by Joe Groff [ 18 Aug 2016 ]

To work around the second problm, you can do nsDict as [NSObject: AnyObject] as [AnyHashable: Any], which is silly but should satisfy the compiler for now.

Comment by Jordan Rose [ 18 Aug 2016 ]

I think cfDict as NSDictionary as [AnyHashable: Any] also works.

Comment by Joe Groff [ 18 Aug 2016 ]

Not yet, unfortunately, we still only allow explicit as conversions from NS classes to the "old" value type mappings [AnyObject]/[NSObject: AnyObject]/etc.

Comment by Joe Groff [ 18 Aug 2016 ]

Another problem is that `CFString` isn't `Hashable`, but it ought to be. You can add this extension as a workaround:

extension CFString: Hashable {
  public var hashValue: Int {
    return Int(bitPattern: CFHash(self))
  }
  public static func ==(a: CFString, b: CFString) -> Bool {
    return CFEqual(a, b)
  }
}

at which point you no longer need the coercions inside the dictionary literal:

let d : [CFString: Any] = [
  kCGImageSourceShouldAllowFloat : true,
  kCGImageSourceCreateThumbnailWithTransform : true,
  kCGImageSourceCreateThumbnailFromImageAlways : true,
  kCGImageSourceThumbnailMaxPixelSize : w
]
Comment by Matt Neuburg [ 18 Aug 2016 ]

Joe Groff That compiles (thank you!) but it doesn't actually work. Without the explicit as Number we fail to get a valid CFDictionary and we crash when we force-unwrap.

The reason is that merely saying true or w does not actually get us across the bridge to AnyObject — and typing the dictionary as [CFString:AnyObject] doesn't help, because we don't get automatic bridge-crossing that way any more either.

Comment by Joe Groff [ 18 Aug 2016 ]

Matt Neuburg What type is w? Bool, Int, and UInt should bridge to NSNumber when you convert to NSDictionary or CFDictionary, but if it's some other number type it may be getting boxed.

Comment by Joe Groff [ 18 Aug 2016 ]

For example, this prints `true` for me:

let w = 1024

let d1 : [CFString: Any] = [
  kCGImageSourceShouldAllowFloat : true,
  kCGImageSourceCreateThumbnailWithTransform : true,
  kCGImageSourceCreateThumbnailFromImageAlways : true,
  kCGImageSourceThumbnailMaxPixelSize : w
]
let d2 : [CFString: AnyObject] = [
  kCGImageSourceShouldAllowFloat : true as NSNumber,
  kCGImageSourceCreateThumbnailWithTransform : true as NSNumber,
  kCGImageSourceCreateThumbnailFromImageAlways : true as NSNumber,
  kCGImageSourceThumbnailMaxPixelSize : w as NSNumber
]
print((d1 as NSDictionary).isEqual(to: d2 as NSDictionary))
Comment by Matt Neuburg [ 18 Aug 2016 ]

Joe Groff It comes out as an NSDictionary but, as I said, it doesn't make it as a valid CFDictionary that ImageIO can understand. That is the bridge we are not crossing.

Here's the full code (along with your CFString extension):

        let url = Bundle.main.url(forResource:"colson", withExtension: "jpg")!
        let src = CGImageSourceCreateWithURL(url as CFURL, nil)!
        let scale = UIScreen.main.scale
        let w = self.iv.bounds.width * scale
        // have to cross over to Objective-C manually
        let d : [CFString:Any] = [
            kCGImageSourceShouldAllowFloat : true as NSNumber,
            kCGImageSourceCreateThumbnailWithTransform : true as NSNumber,
            kCGImageSourceCreateThumbnailFromImageAlways : true as NSNumber,
            kCGImageSourceThumbnailMaxPixelSize : w as NSNumber
        ]
        let imref = CGImageSourceCreateThumbnailAtIndex(src, 0, d as CFDictionary)!

I've attached my test project; launch, then tap the second button to view the image in the interface. I crash at that moment if all four as NSNumber casts have been removed. bk2ch23p829imageIO.zip

Comment by Joe Groff [ 18 Aug 2016 ]

Jordan Rose, is there a chance the crash in CF is related to https://github.com/apple/swift/pull/4366 ? Does CFNumber not toll-free-bridge with NSNumber subclasses?

Comment by Jordan Rose [ 18 Aug 2016 ]

It does, but CFBoolean is a distinct type from CFNumber at the CF layer, so it very well could be related.

Comment by Joe Groff [ 18 Aug 2016 ]

Ah, that could definitely be it then.

Comment by Joe Groff [ 18 Aug 2016 ]

Matt Neuburg If you do true as Bool as NSNumber, do you see the same crash as if you do no coercions at all? Do you crash if you pass w without as NSNumber?

Comment by Wil Shipley [ 18 Aug 2016 ]

When I used this form (this is what the auto-updating stuff in Xcode ended up spitting out) it compiled but I do NOT get an actual image — the load fails.

Unable to find source-code formatter for language: swift. Available languages are: actionscript, html, java, javascript, none, sql, xhtml, xml
            let thumbnailSideLength = 512
            let optionsDictionary = [
                kCGImageSourceCreateThumbnailFromImageIfAbsent as NSString: true,
                kCGImageSourceThumbnailMaxPixelSize as NSString: thumbnailSideLength,
            ] as CFDictionary
            guard let thumbnail = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, optionsDictionary) else {
                    print("\(#function) failed to load thumbnail for \(name)/\(maker)/\(productLine)")
                    throw PlatonicPieceOfFurnitureError.cannotCreateImageFromThumbnailImageSource
            }

When I change my dictionary to this it succeeds:

Unable to find source-code formatter for language: swift. Available languages are: actionscript, html, java, javascript, none, sql, xhtml, xml
            let optionsDictionary = [
                kCGImageSourceCreateThumbnailFromImageIfAbsent as NSString: true as NSNumber,
                kCGImageSourceThumbnailMaxPixelSize as NSString: thumbnailSideLength as NSNumber,
            ] as CFDictionary
Comment by Jordan Rose [ 18 Aug 2016 ]

Confirmed that with https://github.com/apple/swift/pull/4366 this succeeds without the as NSNumber. I'm using this bug to track making all CF types Hashable by default.

Comment by Jordan Rose [ 18 Aug 2016 ]

https://github.com/apple/swift/pull/4394

Comment by Jordan Rose [ 19 Aug 2016 ]

CF types have been made Hashable in https://github.com/apple/swift/pull/4417. If there are any more improvements / fixes we can make around CF, let's put them in separate bug reports.

Comment by Greg Parker [ 20 Aug 2016 ]

Reopening. #4394 and #4417 were reverted due to CI failures. New test Interpreter/SDK/cf.swift fails when run with build-script --test-optimized.

Comment by Jordan Rose [ 4 May 2017 ]

Swift Sync System create

Comment by Jordan Rose [ 8 May 2017 ]

CF types have been remade Hashable in https://github.com/apple/swift/pull/4568. #finally

Comment by Jordan Rose [ 11 May 2017 ]

And merged into Swift 4.0 in https://github.com/apple/swift/pull/9401.

Comment by Matt Neuburg [ 24 Jul 2017 ]

Jordan Rose Greg Parker Joe Groff I am now reaping the rewards of this change. I can say

let result = CGImageSourceCopyPropertiesAtIndex(src, 0, nil)! as! [AnyHashable:Any]

and this works because CFString is AnyHashable.

And I can say

let d : [AnyHashable:Any] = [
    kCGImageSourceShouldAllowFloat : true ,
    kCGImageSourceCreateThumbnailWithTransform : true ,
    kCGImageSourceCreateThumbnailFromImageAlways : true ,
    kCGImageSourceThumbnailMaxPixelSize : w
 ]
let imref = CGImageSourceCreateThumbnailAtIndex(src, 0, d as CFDictionary)!

That's convenient and intuitive. Thanks!

Generated at Wed Feb 21 09:08:28 CST 2018 using JIRA 7.3.4#73015-sha1:a262b3457b3605f12635df4b0a0c3dc71d631a1e.