Skip to content

Adopt bulk Dictionary creation in bridging #21235

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 232 additions & 31 deletions stdlib/public/Darwin/Foundation/NSDictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,140 @@ extension Dictionary : _ObjectiveCBridgeable {
to: NSDictionary.self)
}

/***
Precondition: `buffer` points to a region of memory bound to `AnyObject`,
with a capacity large enough to fit at least `index`+1 elements of type `T`

_bridgeInitialize rebinds the `index`th `T` of `buffer` to `T`,
and initializes it to `value`

Note: *not* the `index`th element of `buffer`, since T and AnyObject may be
different sizes. e.g. if T is String (2 words) then given a buffer like so:

[object:AnyObject, object:AnyObject, uninitialized, uninitialized]

`_bridgeInitialize(1, of: buffer, to: buffer[1] as! T)` will leave it as:

[object:AnyObject, object:AnyObject, string:String]

and `_bridgeInitialize(0, of: buffer, to: buffer[0] as! T)` will then leave:

[string:String, string:String]

Doing this in reverse order as shown above is required if T and AnyObject are
different sizes. Here's what we get if instead of 1, 0 we did 0, 1:

[object:AnyObject, object:AnyObject, uninitialized, uninitialized]
[string:String, uninitialized, uninitialized]
<segfault trying to treat the second word of 'string' as an AnyObject>

Note: if you have retained any of the objects in `buffer`, you must release
them separately, _bridgeInitialize will overwrite them without releasing them
*/
@inline(__always)
private static func _bridgeInitialize<T>(index:Int,
of buffer: UnsafePointer<AnyObject>, to value: T) {
let typedBase = UnsafeMutableRawPointer(mutating:
buffer).assumingMemoryBound(to: T.self)
let rawTarget = UnsafeMutableRawPointer(mutating: typedBase + index)
rawTarget.initializeMemory(as: T.self, repeating: value, count: 1)
}

@inline(__always)
private static func _verbatimForceBridge<T>(
_ buffer: UnsafeMutablePointer<AnyObject>,
count: Int,
to: T.Type
) {
//doesn't have to iterate in reverse because sizeof(T) == sizeof(AnyObject)
for i in 0..<count {
_bridgeInitialize(index: i, of: buffer, to: buffer[i] as! T)
}
}

@inline(__always)
private static func _verbatimBridge<T>(
_ buffer: UnsafeMutablePointer<AnyObject>,
count: Int,
to type: T.Type
) -> Int {
var numUninitialized = count
while numUninitialized > 0 {
guard let bridged = buffer[numUninitialized - 1] as? T else {
return numUninitialized
}
numUninitialized -= 1
_bridgeInitialize(index: numUninitialized, of: buffer, to: bridged)
}
return numUninitialized
}

@inline(__always)
private static func _nonVerbatimForceBridge<T>(
_ buffer: UnsafeMutablePointer<AnyObject>,
count: Int,
to: T.Type
) {
for i in (0..<count).reversed() {
let bridged = Swift._forceBridgeFromObjectiveC(buffer[i], T.self)
_bridgeInitialize(index: i, of: buffer, to: bridged)
}
}

@inline(__always)
private static func _nonVerbatimBridge<T>(
_ buffer: UnsafeMutablePointer<AnyObject>,
count: Int,
to: T.Type
) -> Int {
var numUninitialized = count
while numUninitialized > 0 {
guard let bridged = Swift._conditionallyBridgeFromObjectiveC(
buffer[numUninitialized - 1], T.self)
else {
return numUninitialized
}
numUninitialized -= 1
_bridgeInitialize(index: numUninitialized, of: buffer, to: bridged)
}
return numUninitialized
}

@inline(__always)
private static func _forceBridge<T>(
_ buffer: UnsafeMutablePointer<AnyObject>,
count: Int,
to: T.Type
) {
if _isBridgedVerbatimToObjectiveC(T.self) {
_verbatimForceBridge(buffer, count: count, to: T.self)
} else {
_nonVerbatimForceBridge(buffer, count: count, to: T.self)
}
}

@inline(__always)
private static func _conditionallyBridge<T>(
_ buffer: UnsafeMutablePointer<AnyObject>,
count: Int,
to: T.Type
) -> Bool {
let numUninitialized:Int
if _isBridgedVerbatimToObjectiveC(T.self) {
numUninitialized = _verbatimBridge(buffer, count: count, to: T.self)
} else {
numUninitialized = _nonVerbatimBridge(buffer, count: count, to: T.self)
}
if numUninitialized == 0 {
return true
Copy link
Member

@lorentey lorentey Dec 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right -- verbatim bridging should still perform a checked downcast, or we would allow casting an NSDictionary of NSString keys to [NSNumber: Any].

(Maybe there is room to add an unchecked downcasting variant for use in case we're totally sure.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, it looks like we don't perform this check for verbatim-bridged forced downcasts on master, either -- we only check conditional downcasts. That seems like a bug!

Copy link
Member

@lorentey lorentey Dec 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except it isn't -- the check is of course still performed when we're downcasting individual items. This is by design; bridging is done lazily in these cases. Sorry for the brain hiccup!

Despite my confusion about forced casts, conditional casts are always expected to eagerly check that all items can be bridged/downcasted to the target type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have this right in the most recent update I pushed. The verbatim ones use ? and !, and the non verbatim ones use Swift._forceBridgeFromObjectiveC and Swift._conditionallyBridgeFromObjectiveC. It's possible we could merge some of the cases now, I'm not entirely sure.

}
let numInitialized = count - numUninitialized
(UnsafeMutableRawPointer(mutating: buffer).assumingMemoryBound(to:
T.self) + numUninitialized).deinitialize(count: numInitialized)
return false
}

@_specialize(where Key == String, Value == Any)
public static func _forceBridgeFromObjectiveC(
_ d: NSDictionary,
result: inout Dictionary?
Expand All @@ -73,53 +207,120 @@ extension Dictionary : _ObjectiveCBridgeable {

if _isBridgedVerbatimToObjectiveC(Key.self) &&
_isBridgedVerbatimToObjectiveC(Value.self) {
//Lazily type-checked on access
result = [Key : Value](_cocoaDictionary: d)
return
}

if Key.self == String.self {
let keyStride = MemoryLayout<Key>.stride
let valueStride = MemoryLayout<Value>.stride
let objectStride = MemoryLayout<AnyObject>.stride

//If Key or Value are smaller than AnyObject, a Dictionary with N elements
//doesn't have large enough backing stores to hold the objects to be bridged
//For now we just handle that case the slow way.
if keyStride < objectStride || valueStride < objectStride {
var builder = _DictionaryBuilder<Key, Value>(count: d.count)
d.enumerateKeysAndObjects({ (anyKey: Any, anyValue: Any, _) in
let anyObjectKey = anyKey as AnyObject
let anyObjectValue = anyValue as AnyObject
builder.add(
key: Swift._forceBridgeFromObjectiveC(anyObjectKey, Key.self),
value: Swift._forceBridgeFromObjectiveC(anyObjectValue, Value.self))
})
result = builder.take()
} else {
defer { _fixLifetime(d) }

let numElems = d.count

// String and NSString have different concepts of equality, so
// string-keyed NSDictionaries may generate key collisions when bridged
// over to Swift. See rdar://problem/35995647
var dict = Dictionary(minimumCapacity: d.count)
d.enumerateKeysAndObjects({ (anyKey: Any, anyValue: Any, _) in
let key = Swift._forceBridgeFromObjectiveC(
anyKey as AnyObject, Key.self)
let value = Swift._forceBridgeFromObjectiveC(
anyValue as AnyObject, Value.self)
// FIXME: Log a warning if `dict` already had a value for `key`
dict[key] = value
})
result = dict
return
}
let handleDuplicates = (Key.self == String.self)

result = Dictionary(_unsafeUninitializedCapacity: numElems,
allowingDuplicates: handleDuplicates) { (keys, vals, outCount) in

let objectKeys = UnsafeMutableRawPointer(mutating:
keys.baseAddress!).assumingMemoryBound(to: AnyObject.self)
let objectVals = UnsafeMutableRawPointer(mutating:
vals.baseAddress!).assumingMemoryBound(to: AnyObject.self)

// `Dictionary<Key, Value>` where either `Key` or `Value` is a value type
// may not be backed by an NSDictionary.
var builder = _DictionaryBuilder<Key, Value>(count: d.count)
d.enumerateKeysAndObjects({ (anyKey: Any, anyValue: Any, _) in
let anyObjectKey = anyKey as AnyObject
let anyObjectValue = anyValue as AnyObject
builder.add(
key: Swift._forceBridgeFromObjectiveC(anyObjectKey, Key.self),
value: Swift._forceBridgeFromObjectiveC(anyObjectValue, Value.self))
})
result = builder.take()
}
//This initializes the first N AnyObjects of the Dictionary buffers.
//Any unused buffer space is left uninitialized
//This is fixed up in-place as we bridge elements, by _bridgeInitialize
__NSDictionaryGetObjects(d, objectVals, objectKeys, numElems)

_forceBridge(objectKeys, count: numElems, to: Key.self)
_forceBridge(objectVals, count: numElems, to: Value.self)

outCount = numElems
}
}
}

@_specialize(where Key == String, Value == Any)
public static func _conditionallyBridgeFromObjectiveC(
_ x: NSDictionary,
result: inout Dictionary?
) -> Bool {
let anyDict = x as [NSObject : AnyObject]
if _isBridgedVerbatimToObjectiveC(Key.self) &&
_isBridgedVerbatimToObjectiveC(Value.self) {
result = Swift._dictionaryDownCastConditional(anyDict)

if let native = [Key : Value]._bridgeFromObjectiveCAdoptingNativeStorageOf(
x as AnyObject) {
result = native
return true
}

let keyStride = MemoryLayout<Key>.stride
let valueStride = MemoryLayout<Value>.stride
let objectStride = MemoryLayout<AnyObject>.stride

//If Key or Value are smaller than AnyObject, a Dictionary with N elements
//doesn't have large enough backing stores to hold the objects to be bridged
//For now we just handle that case the slow way.
if keyStride < objectStride || valueStride < objectStride {
result = x as [NSObject : AnyObject] as? Dictionary
return result != nil
}

result = anyDict as? Dictionary
return result != nil
defer { _fixLifetime(x) }

let numElems = x.count
var success = true

// String and NSString have different concepts of equality, so
// string-keyed NSDictionaries may generate key collisions when bridged
// over to Swift. See rdar://problem/35995647
let handleDuplicates = (Key.self == String.self)

let tmpResult = Dictionary(_unsafeUninitializedCapacity: numElems,
allowingDuplicates: handleDuplicates) { (keys, vals, outCount) in

let objectKeys = UnsafeMutableRawPointer(mutating:
keys.baseAddress!).assumingMemoryBound(to: AnyObject.self)
let objectVals = UnsafeMutableRawPointer(mutating:
vals.baseAddress!).assumingMemoryBound(to: AnyObject.self)

//This initializes the first N AnyObjects of the Dictionary buffers.
//Any unused buffer space is left uninitialized
//This is fixed up in-place as we bridge elements, by _bridgeInitialize
__NSDictionaryGetObjects(x, objectVals, objectKeys, numElems)

success = _conditionallyBridge(objectKeys, count: numElems, to: Key.self)
if success {
success = _conditionallyBridge(objectVals,
count: numElems, to: Value.self)
if !success {
(UnsafeMutableRawPointer(mutating: objectKeys).assumingMemoryBound(to:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, this was the fix for the memory leak >.< cleaning up the buffer that failed worked fine, but if the second buffer failed we wouldn't clean up the first one.

Key.self)).deinitialize(count: numElems)
}
}
outCount = success ? numElems : 0
}

result = success ? tmpResult : nil
return success
}

@_effects(readonly)
Expand Down
Loading