[Swift 4.0] Dictionary with non-verbatim bridged keys

PR for context: https://github.com/apple/swift/pull/12249.

Note: The setup in which I experienced the crash was on Xcode 9.2 with Swift 4.0. The issue has been resolved for me on Xcode 9.3 running Swift 4.1 so this post is more exploratory.

Context

I experienced the same issue that #12249 fixes where there was a

...crash involving _HashableTypedNativeDictionaryStorage when printing dictionaries whose keys or values aren't bridged verbatim.

I am curious to learn more about why this happened especially since I was able to reproduce it quite easily.
The playground (running in Xcode 9.2 with Swift 4.0) I used look like this:

import Foundation

let testKey: String = "Somebody once told me" 
let dictionary: [String: NSNumber] = [testKey: 12]
print(dictionary)
// ["Somebody once told me": 12]
print(dictionary.keys)
// error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=EXC_I386_GPFLT).
// The process has been left at the point where it was interrupted, use "thread return -x" 
// to return to the state before expression evaluation.

However, when I change the testKey to be of type NSString then it no longer crashes:

import Foundation

let testKey: NSString = "Somebody once told me"
let dictionary: [String: NSNumber] = [testKey as String: 12]
print(dictionary)
// ["Somebody once told me": 12]
print(dictionary.keys)
// ["Somebody once told me"] 

After looking through the variable inspector, the only difference I noticed was:

  • using just Swift strings, I observed they had no "owner"
  • using a Swift string that was casted from an NSString, there was an "owner" (the backing NSString I assume)

Swift string

  • I'm unable to post the image because of the 1 image upload limit but this looks similar to the below screenshot except owner is marked as nil.
  • I posted the screenshot in the PR as a comment.

Swift string casted from NSString

screen shot 2018-02-15 at 10 50 18 am

Question

How could I find out more about what "owner" and "bridged verbatim" means? How did the latter affect the crash? Thanks for taking the time to read this, I'm just curious to understand this better :smile:

I found one post that mentions the latter which does mention that "String/NSString aren’t. [verbatim bridged]". This makes sense as per the description of the PR.

Let me know if this post is in the wrong section but the PR was marked as [stdlib] so this seemed appropriate

Looking through the Swift docs, it seems like “bridged verbatim” is defined here: https://github.com/apple/swift/blob/33996de39ca369270defeca22269c5219ed54077/docs/Arrays.rst#bridged-verbatim

Every class type or @objc existential (such as AnyObject) is bridged to Objective-C and bridged back to Swift via the identity transformation, i.e. it is bridged verbatim.

2 Likes

In stdlib’s internal terminology, “owner” is usually the name of a property that holds the reference-counted storage object for some Swift type. I don’t think it’s a particularly great name.

Like @rashad said, a type is “verbatim bridged” when it has the exact same representation in Objective-C as in Swift. For example, UIView is bridged verbatim, but NSNumber, NSString or NSDictionary aren’t – converting between these and, say, Int/String/Dictionary<String, AnyObject> requires some custom conversion code.

The underlying cause of this particular crash was that Dictionary's internal storage class always subclasses NSDictionary, but the implementation is only actually valid when the keys and values are verbatim bridged. (In that happy case, the subclassing simplifies conversion of Swift dictionaries into NSDictionary.) The NSDictionary implementation bridges values on the fly, which won’t work correctly when bridging actually allocates a new object. (For one, the result gets immediately deallocated on return; but even if it was autoreleased, it’s not okay for a dictionary to change the object identities of its contents on every lookup.)

When Dictionary.Keys/.Values did not include a CustomStringConvertible implementation, they were printed using reflection, which found the NSDictionary storage object and tried to print it as such. This blew up when the implementation was not actually there.

The fix hid this particular issue, but the invalid NSDictionary objects are still there, waiting for an opportunity to cause more headaches elsewhere. Cue ominous sound effect

1 Like