[Proposal draft] Bridge Numeric Types to NSNumber and Cocoa Structs to NSValue


(Douglas Gregor) #1

Introduction

A handful of Swift numeric types are bridged to NSNumber when passed into Objective-C object contexts. We should extend this bridging behavior to all Swift numeric types. We should also bridge common Cocoa structs such as NSRangeby boxing them into NSValue objects.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the hazard of passing in types that don't bridge well to Objective-C objects. Particularly problematic are number types; whereas Int, UInt, and Double will automatically bridge as NSNumber, other-sized numeric types fall back to opaque boxing:

let i = 17
let plist = ["seventeen": i]
// OK
try! NSJSONSerialization.data(withJSONObject: plist)

let j: UInt8 = 38
let brokenPlist = ["thirty-eight": j]
// Will throw because `j` didn't bridge to a JSON type
try! NSJSONSerialization.data(withJSONObject: brokenPlist)
We had shied away from enabling this bridging for all numeric types in the Swift 1.x days, among other reasons because we allowed implicit bridging conversions in both directions from Swift value types to NS objects and back, which meant that you could slowly and brokenly convert between any two numeric types transitively via NSNumber if we allowed this. We killed the implicit conversions completely with SE-0072 <https://github.com/apple/swift-evolution/blob/master/proposals/0072-eliminate-implicit-bridging-conversions.md> so that is no longer a concern, so expanding the bridging behavior should no longer be a major problem, since it must now always be explicitly asked for.

There are also many Cocoa APIs that accept NSArray and NSDictionary objects with members that are NSValue-boxed structs. Matt Neuberg highlights Core Automation as an example in this bug report <https://bugs.swift.org/browse/SR-2414>. With id-as-Any, it's natural to expect this to work:

anim.toValue = CGPoint.zero
However, the CGPoint value does not box as a meaningful Objective-C object, so this currently breaks Core Animation at runtime despite compiling successfully. It would be more idiomatic to bridge these types to NSValue.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#proposed-solution>Proposed solution

All of Swift's number types should be made to bridge to NSNumber when used as objects in Objective-C:

Int8
Int16
Int32
Int64
UInt8
UInt16
UInt32
UInt64
Float
Double
Cocoa structs with existing NSValue factory and property support should be made to bridge to NSValue when used as objects:

NSRange
CGPoint
CGVector
CGSize
CGRect
CGAffineTransform
UIEdgeInsets
UIOffset
CATransform3D
CMTime
CMTimeRange
CMTimeMapping
MKCoordinate
MKCoordinateSpan
SCNVector3
SCNVector4
SCNMatrix4
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#detailed-design>Detailed design

Bridged NSNumber and NSValue objects must be castable back to their original Swift value types. NSValue normally preserves the type information of its included struct in its objCType property. We can check the objCType of an NSValue instance when attempting to cast back to a specific bridged struct type.

NSNumber is a bit trickier, since Cocoa's implementation does not generally guarantee to remember the exact number type an instance was constructed from. We can instead say that casting an NSNumber to a Swift number type succeeds if the value of the NSNumber is exactly representable as the target type. This is imperfect, since it means that an NSNumbercan potentially be cast to a different type from the original value, but it at least ensures that Swift values round-trip through the bridge without depending on NSNumber implementation details.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, values that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from concrete number and struct types to Anyto id to Any and back by dynamic casting. The ability to reliably distinguish the exact number type that an NSNumberwas constructed from would be lost.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#alternatives-considered>Alternatives considered

We can of course do nothing and leave the behavior as-is.

NSValue also carries factories for valueWithPointer: and valueWithNonretainedObject:. Maybe we could bridgeUnsafePointer and Unmanaged this way, but we probably shouldn’t.

  - Doug


(Zachary Waldowski) #2

Early feedback is +1, you folks sure know how to write proposals on
Foundation. :wink:

I filed radar <rdar://problem/20807093> related to this a while ago. Is
there any chance the struct conformances could be applied using the
objc_boxable attribute added to Clang last year? Apple's definition of
what they consider to be simple, fixed structs should ideally live in
one place (apinotes would do as well).

Sincerely,
  Zachary Waldowski
  zach@waldowski.me

···

On Tue, Aug 23, 2016, at 03:36 PM, Douglas Gregor via swift-evolution wrote:

Introduction
A handful of Swift numeric types are bridged to NSNumber when passed
into Objective-C object contexts. We should extend this bridging
behavior to all Swift numeric types. We should also bridge common
Cocoa structs such as NSRangeby boxing them into NSValue objects.
Swift-evolution thread: TBD[1]
Motivation
SE-0116[2] changed how Objective-C's id and untyped collections import
into Swift to use the Any type. This makes it much more natural to
pass in Swift value types such as String and Array, but introduces the
hazard of passing in types that don't bridge well to Objective-C
objects. Particularly problematic are number types; whereas Int, UInt,
and Double will automatically bridge as NSNumber, other-sized numeric
types fall back to opaque boxing:
let i = 17 let plist = ["seventeen": i] // OK try!
NSJSONSerialization.data(withJSONObject: plist)

let j: UInt8 = 38 let brokenPlist = ["thirty-eight": j] // Will throw
because `j` didn't bridge to a JSON type try!
NSJSONSerialization.data(withJSONObject: brokenPlist)
We had shied away from enabling this bridging for all numeric types in
the Swift 1.x days, among other reasons because we allowed implicit
bridging conversions in both directions from Swift value types to NS
objects and back, which meant that you could slowly and brokenly
convert between any two numeric types transitively via NSNumber if we
allowed this. We killed the implicit conversions completely with SE-
0072[3] so that is no longer a concern, so expanding the bridging
behavior should no longer be a major problem, since it must now always
be explicitly asked for.
There are also many Cocoa APIs that accept NSArray and NSDictionary
objects with members that are NSValue-boxed structs. Matt Neuberg
highlights Core Automation as an example in this bug report[4]. With
id-as-Any, it's natural to expect this to work:
anim.toValue = CGPoint.zero
However, the CGPoint value does not box as a meaningful Objective-C
object, so this currently breaks Core Animation at runtime despite
compiling successfully. It would be more idiomatic to bridge these
types to NSValue.
Proposed solution
All of Swift's number types should be made to bridge to NSNumber when
used as objects in Objective-C:

* Int8
* Int16
* Int32
* Int64
* UInt8
* UInt16
* UInt32
* UInt64
* Float
* Double
Cocoa structs with existing NSValue factory and property support
should be made to bridge to NSValue when used as objects:

* NSRange
* CGPoint
* CGVector
* CGSize
* CGRect
* CGAffineTransform
* UIEdgeInsets
* UIOffset
* CATransform3D
* CMTime
* CMTimeRange
* CMTimeMapping
* MKCoordinate
* MKCoordinateSpan
* SCNVector3
* SCNVector4
* SCNMatrix4
Detailed design
Bridged NSNumber and NSValue objects must be castable back to their
original Swift value types. NSValue normally preserves the type
information of its included struct in its objCType property. We can
check the objCType of an NSValue instance when attempting to cast back
to a specific bridged struct type.
NSNumber is a bit trickier, since Cocoa's implementation does not
generally guarantee to remember the exact number type an instance was
constructed from. We can instead say that casting an NSNumber to a
Swift number type succeeds if the value of the NSNumber is exactly
representable as the target type. This is imperfect, since it means
that an NSNumbercan potentially be cast to a different type from the
original value, but it at least ensures that Swift values round-trip
through the bridge without depending on NSNumber implementation
details.
Impact on existing code
This change has no static source impact, but changes the dynamic
behavior of the Objective-C bridge. From Objective-C's perspective,
values that used to bridge as opaque objects will now come in as
semantically meaningful Objective-C objects. This should be a safe
change, since existing code should not be relying on the behavior of
opaque bridged objects. From Swift's perspective, values should still
be able to round-trip from concrete number and struct types to Anyto
id to Any and back by dynamic casting. The ability to reliably
distinguish the exact number type that an NSNumberwas constructed from
would be lost.
Alternatives considered
We can of course do nothing and leave the behavior as-is.
NSValue also carries factories for valueWithPointer: and
valueWithNonretainedObject:. Maybe we could bridgeUnsafePointer and
Unmanaged this way, but we probably shouldn’t.

- Doug
_________________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Links:

  1. https://lists.swift.org/pipermail/swift-evolution/
  2. https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md
  3. https://github.com/apple/swift-evolution/blob/master/proposals/0072-eliminate-implicit-bridging-conversions.md
  4. https://bugs.swift.org/browse/SR-2414


(Tony Parker) #3

Hi Doug,

Introduction

A handful of Swift numeric types are bridged to NSNumber when passed into Objective-C object contexts. We should extend this bridging behavior to all Swift numeric types. We should also bridge common Cocoa structs such as NSRangeby boxing them into NSValue objects.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the hazard of passing in types that don't bridge well to Objective-C objects. Particularly problematic are number types; whereas Int, UInt, and Double will automatically bridge as NSNumber, other-sized numeric types fall back to opaque boxing:

let i = 17
let plist = ["seventeen": i]
// OK
try! NSJSONSerialization.data(withJSONObject: plist)

let j: UInt8 = 38
let brokenPlist = ["thirty-eight": j]
// Will throw because `j` didn't bridge to a JSON type
try! NSJSONSerialization.data(withJSONObject: brokenPlist)
We had shied away from enabling this bridging for all numeric types in the Swift 1.x days, among other reasons because we allowed implicit bridging conversions in both directions from Swift value types to NS objects and back, which meant that you could slowly and brokenly convert between any two numeric types transitively via NSNumber if we allowed this. We killed the implicit conversions completely with SE-0072 <https://github.com/apple/swift-evolution/blob/master/proposals/0072-eliminate-implicit-bridging-conversions.md> so that is no longer a concern, so expanding the bridging behavior should no longer be a major problem, since it must now always be explicitly asked for.

There are also many Cocoa APIs that accept NSArray and NSDictionary objects with members that are NSValue-boxed structs. Matt Neuberg highlights Core Automation as an example in this bug report <https://bugs.swift.org/browse/SR-2414>. With id-as-Any, it's natural to expect this to work:

anim.toValue = CGPoint.zero
However, the CGPoint value does not box as a meaningful Objective-C object, so this currently breaks Core Animation at runtime despite compiling successfully. It would be more idiomatic to bridge these types to NSValue.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#proposed-solution>Proposed solution

All of Swift's number types should be made to bridge to NSNumber when used as objects in Objective-C:

Int8
Int16
Int32
Int64
UInt8
UInt16
UInt32
UInt64
Float
Double
Cocoa structs with existing NSValue factory and property support should be made to bridge to NSValue when used as objects:

NSRange
CGPoint
CGVector
CGSize
CGRect
CGAffineTransform
UIEdgeInsets
UIOffset
CATransform3D
CMTime
CMTimeRange
CMTimeMapping
MKCoordinate
MKCoordinateSpan
SCNVector3
SCNVector4
SCNMatrix4

How do new types get added to this list? It’s certainly not the case that NSValue knows about them specifically; usually some kind of category is added after the fact.

Also, how does this bridging work for swift-corelibs-foundation on Linux, where bridging is disabled?

- Tony

···

On Aug 23, 2016, at 3:36 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#detailed-design>Detailed design

Bridged NSNumber and NSValue objects must be castable back to their original Swift value types. NSValue normally preserves the type information of its included struct in its objCType property. We can check the objCType of an NSValue instance when attempting to cast back to a specific bridged struct type.

NSNumber is a bit trickier, since Cocoa's implementation does not generally guarantee to remember the exact number type an instance was constructed from. We can instead say that casting an NSNumber to a Swift number type succeeds if the value of the NSNumber is exactly representable as the target type. This is imperfect, since it means that an NSNumbercan potentially be cast to a different type from the original value, but it at least ensures that Swift values round-trip through the bridge without depending on NSNumber implementation details.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, values that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from concrete number and struct types to Anyto id to Any and back by dynamic casting. The ability to reliably distinguish the exact number type that an NSNumberwas constructed from would be lost.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#alternatives-considered>Alternatives considered

We can of course do nothing and leave the behavior as-is.

NSValue also carries factories for valueWithPointer: and valueWithNonretainedObject:. Maybe we could bridgeUnsafePointer and Unmanaged this way, but we probably shouldn’t.

  - Doug
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Jaden Geller) #4

Strong +1 to this proposal. Lack of implicit bridging for Int8/Int16/Int32/Int64 types has caused crashes in production code I've worked with, so this is a real problem. Now that `id` is imported as `Any`, it's even easy to run into these crashes since there's no compile-time error when you pass an `Int8` into Objective-C as `Any` and try to use it as if it were an `NSNumber`.

···

On Aug 23, 2016, at 3:36 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

Introduction

A handful of Swift numeric types are bridged to NSNumber when passed into Objective-C object contexts. We should extend this bridging behavior to all Swift numeric types. We should also bridge common Cocoa structs such as NSRangeby boxing them into NSValue objects.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the hazard of passing in types that don't bridge well to Objective-C objects. Particularly problematic are number types; whereas Int, UInt, and Double will automatically bridge as NSNumber, other-sized numeric types fall back to opaque boxing:

let i = 17
let plist = ["seventeen": i]
// OK
try! NSJSONSerialization.data(withJSONObject: plist)

let j: UInt8 = 38
let brokenPlist = ["thirty-eight": j]
// Will throw because `j` didn't bridge to a JSON type
try! NSJSONSerialization.data(withJSONObject: brokenPlist)
We had shied away from enabling this bridging for all numeric types in the Swift 1.x days, among other reasons because we allowed implicit bridging conversions in both directions from Swift value types to NS objects and back, which meant that you could slowly and brokenly convert between any two numeric types transitively via NSNumber if we allowed this. We killed the implicit conversions completely with SE-0072 <https://github.com/apple/swift-evolution/blob/master/proposals/0072-eliminate-implicit-bridging-conversions.md> so that is no longer a concern, so expanding the bridging behavior should no longer be a major problem, since it must now always be explicitly asked for.

There are also many Cocoa APIs that accept NSArray and NSDictionary objects with members that are NSValue-boxed structs. Matt Neuberg highlights Core Automation as an example in this bug report <https://bugs.swift.org/browse/SR-2414>. With id-as-Any, it's natural to expect this to work:

anim.toValue = CGPoint.zero
However, the CGPoint value does not box as a meaningful Objective-C object, so this currently breaks Core Animation at runtime despite compiling successfully. It would be more idiomatic to bridge these types to NSValue.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#proposed-solution>Proposed solution

All of Swift's number types should be made to bridge to NSNumber when used as objects in Objective-C:

Int8
Int16
Int32
Int64
UInt8
UInt16
UInt32
UInt64
Float
Double
Cocoa structs with existing NSValue factory and property support should be made to bridge to NSValue when used as objects:

NSRange
CGPoint
CGVector
CGSize
CGRect
CGAffineTransform
UIEdgeInsets
UIOffset
CATransform3D
CMTime
CMTimeRange
CMTimeMapping
MKCoordinate
MKCoordinateSpan
SCNVector3
SCNVector4
SCNMatrix4
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#detailed-design>Detailed design

Bridged NSNumber and NSValue objects must be castable back to their original Swift value types. NSValue normally preserves the type information of its included struct in its objCType property. We can check the objCType of an NSValue instance when attempting to cast back to a specific bridged struct type.

NSNumber is a bit trickier, since Cocoa's implementation does not generally guarantee to remember the exact number type an instance was constructed from. We can instead say that casting an NSNumber to a Swift number type succeeds if the value of the NSNumber is exactly representable as the target type. This is imperfect, since it means that an NSNumbercan potentially be cast to a different type from the original value, but it at least ensures that Swift values round-trip through the bridge without depending on NSNumber implementation details.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, values that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from concrete number and struct types to Anyto id to Any and back by dynamic casting. The ability to reliably distinguish the exact number type that an NSNumberwas constructed from would be lost.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#alternatives-considered>Alternatives considered

We can of course do nothing and leave the behavior as-is.

NSValue also carries factories for valueWithPointer: and valueWithNonretainedObject:. Maybe we could bridgeUnsafePointer and Unmanaged this way, but we probably shouldn’t.

  - Doug
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #5

+1. This is fantastic.

···

On Aug 23, 2016, at 5:36 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

Introduction

A handful of Swift numeric types are bridged to NSNumber when passed into Objective-C object contexts. We should extend this bridging behavior to all Swift numeric types. We should also bridge common Cocoa structs such as NSRangeby boxing them into NSValue objects.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the hazard of passing in types that don't bridge well to Objective-C objects. Particularly problematic are number types; whereas Int, UInt, and Double will automatically bridge as NSNumber, other-sized numeric types fall back to opaque boxing:

let i = 17
let plist = ["seventeen": i]
// OK
try! NSJSONSerialization.data(withJSONObject: plist)

let j: UInt8 = 38
let brokenPlist = ["thirty-eight": j]
// Will throw because `j` didn't bridge to a JSON type
try! NSJSONSerialization.data(withJSONObject: brokenPlist)
We had shied away from enabling this bridging for all numeric types in the Swift 1.x days, among other reasons because we allowed implicit bridging conversions in both directions from Swift value types to NS objects and back, which meant that you could slowly and brokenly convert between any two numeric types transitively via NSNumber if we allowed this. We killed the implicit conversions completely with SE-0072 <https://github.com/apple/swift-evolution/blob/master/proposals/0072-eliminate-implicit-bridging-conversions.md> so that is no longer a concern, so expanding the bridging behavior should no longer be a major problem, since it must now always be explicitly asked for.

There are also many Cocoa APIs that accept NSArray and NSDictionary objects with members that are NSValue-boxed structs. Matt Neuberg highlights Core Automation as an example in this bug report <https://bugs.swift.org/browse/SR-2414>. With id-as-Any, it's natural to expect this to work:

anim.toValue = CGPoint.zero
However, the CGPoint value does not box as a meaningful Objective-C object, so this currently breaks Core Animation at runtime despite compiling successfully. It would be more idiomatic to bridge these types to NSValue.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#proposed-solution>Proposed solution

All of Swift's number types should be made to bridge to NSNumber when used as objects in Objective-C:

Int8
Int16
Int32
Int64
UInt8
UInt16
UInt32
UInt64
Float
Double
Cocoa structs with existing NSValue factory and property support should be made to bridge to NSValue when used as objects:

NSRange
CGPoint
CGVector
CGSize
CGRect
CGAffineTransform
UIEdgeInsets
UIOffset
CATransform3D
CMTime
CMTimeRange
CMTimeMapping
MKCoordinate
MKCoordinateSpan
SCNVector3
SCNVector4
SCNMatrix4
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#detailed-design>Detailed design

Bridged NSNumber and NSValue objects must be castable back to their original Swift value types. NSValue normally preserves the type information of its included struct in its objCType property. We can check the objCType of an NSValue instance when attempting to cast back to a specific bridged struct type.

NSNumber is a bit trickier, since Cocoa's implementation does not generally guarantee to remember the exact number type an instance was constructed from. We can instead say that casting an NSNumber to a Swift number type succeeds if the value of the NSNumber is exactly representable as the target type. This is imperfect, since it means that an NSNumbercan potentially be cast to a different type from the original value, but it at least ensures that Swift values round-trip through the bridge without depending on NSNumber implementation details.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, values that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from concrete number and struct types to Anyto id to Any and back by dynamic casting. The ability to reliably distinguish the exact number type that an NSNumberwas constructed from would be lost.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#alternatives-considered>Alternatives considered

We can of course do nothing and leave the behavior as-is.

NSValue also carries factories for valueWithPointer: and valueWithNonretainedObject:. Maybe we could bridgeUnsafePointer and Unmanaged this way, but we probably shouldn’t.

  - Doug
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Douglas Gregor) #6

Early feedback is +1, you folks sure know how to write proposals on Foundation. :wink:

I filed radar <rdar://problem/20807093> related to this a while ago. Is there any chance the struct conformances could be applied using the objc_boxable attribute added to Clang last year? Apple's definition of what they consider to be simple, fixed structs should ideally live in one place (apinotes would do as well).

I’d forgotten about objc_boxable. It might be possible to synthesize an _ObjectiveCBridgeable conformance for objc_boxable types, using the @encode string to match whether a particular NSValue can be bridged to that type.

  - Doug

···

On Aug 23, 2016, at 3:44 PM, Zach Waldowski via swift-evolution <swift-evolution@swift.org> wrote:

Sincerely,
  Zachary Waldowski
  zach@waldowski.me <mailto:zach@waldowski.me>

On Tue, Aug 23, 2016, at 03:36 PM, Douglas Gregor via swift-evolution wrote:

Introduction

A handful of Swift numeric types are bridged to NSNumber when passed into Objective-C object contexts. We should extend this bridging behavior to all Swift numeric types. We should also bridge common Cocoa structs such as NSRangeby boxing them into NSValue objects.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the hazard of passing in types that don't bridge well to Objective-C objects. Particularly problematic are number types; whereas Int, UInt, and Double will automatically bridge as NSNumber, other-sized numeric types fall back to opaque boxing:

let i = 17
let plist = ["seventeen": i]
// OK
try! NSJSONSerialization.data(withJSONObject: plist)

let j: UInt8 = 38
let brokenPlist = ["thirty-eight": j]
// Will throw because `j` didn't bridge to a JSON type
try! NSJSONSerialization.data(withJSONObject: brokenPlist)
We had shied away from enabling this bridging for all numeric types in the Swift 1.x days, among other reasons because we allowed implicit bridging conversions in both directions from Swift value types to NS objects and back, which meant that you could slowly and brokenly convert between any two numeric types transitively via NSNumber if we allowed this. We killed the implicit conversions completely with SE-0072 <https://github.com/apple/swift-evolution/blob/master/proposals/0072-eliminate-implicit-bridging-conversions.md> so that is no longer a concern, so expanding the bridging behavior should no longer be a major problem, since it must now always be explicitly asked for.

There are also many Cocoa APIs that accept NSArray and NSDictionary objects with members that are NSValue-boxed structs. Matt Neuberg highlights Core Automation as an example in this bug report <https://bugs.swift.org/browse/SR-2414>. With id-as-Any, it's natural to expect this to work:

anim.toValue = CGPoint.zero
However, the CGPoint value does not box as a meaningful Objective-C object, so this currently breaks Core Animation at runtime despite compiling successfully. It would be more idiomatic to bridge these types to NSValue.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#proposed-solution>Proposed solution

All of Swift's number types should be made to bridge to NSNumber when used as objects in Objective-C:

Int8
Int16
Int32
Int64
UInt8
UInt16
UInt32
UInt64
Float
Double
Cocoa structs with existing NSValue factory and property support should be made to bridge to NSValue when used as objects:

NSRange
CGPoint
CGVector
CGSize
CGRect
CGAffineTransform
UIEdgeInsets
UIOffset
CATransform3D
CMTime
CMTimeRange
CMTimeMapping
MKCoordinate
MKCoordinateSpan
SCNVector3
SCNVector4
SCNMatrix4
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#detailed-design>Detailed design

Bridged NSNumber and NSValue objects must be castable back to their original Swift value types. NSValue normally preserves the type information of its included struct in its objCType property. We can check the objCType of an NSValue instance when attempting to cast back to a specific bridged struct type.

NSNumber is a bit trickier, since Cocoa's implementation does not generally guarantee to remember the exact number type an instance was constructed from. We can instead say that casting an NSNumber to a Swift number type succeeds if the value of the NSNumber is exactly representable as the target type. This is imperfect, since it means that an NSNumbercan potentially be cast to a different type from the original value, but it at least ensures that Swift values round-trip through the bridge without depending on NSNumber implementation details.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, values that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from concrete number and struct types to Anyto id to Any and back by dynamic casting. The ability to reliably distinguish the exact number type that an NSNumberwas constructed from would be lost.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#alternatives-considered>Alternatives considered

We can of course do nothing and leave the behavior as-is.

NSValue also carries factories for valueWithPointer: and valueWithNonretainedObject:. Maybe we could bridgeUnsafePointer and Unmanaged this way, but we probably shouldn’t.

- Doug
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Douglas Gregor) #7

Hi Doug,

Introduction

A handful of Swift numeric types are bridged to NSNumber when passed into Objective-C object contexts. We should extend this bridging behavior to all Swift numeric types. We should also bridge common Cocoa structs such as NSRangeby boxing them into NSValue objects.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the hazard of passing in types that don't bridge well to Objective-C objects. Particularly problematic are number types; whereas Int, UInt, and Double will automatically bridge as NSNumber, other-sized numeric types fall back to opaque boxing:

let i = 17
let plist = ["seventeen": i]
// OK
try! NSJSONSerialization.data(withJSONObject: plist)

let j: UInt8 = 38
let brokenPlist = ["thirty-eight": j]
// Will throw because `j` didn't bridge to a JSON type
try! NSJSONSerialization.data(withJSONObject: brokenPlist)
We had shied away from enabling this bridging for all numeric types in the Swift 1.x days, among other reasons because we allowed implicit bridging conversions in both directions from Swift value types to NS objects and back, which meant that you could slowly and brokenly convert between any two numeric types transitively via NSNumber if we allowed this. We killed the implicit conversions completely with SE-0072 <https://github.com/apple/swift-evolution/blob/master/proposals/0072-eliminate-implicit-bridging-conversions.md> so that is no longer a concern, so expanding the bridging behavior should no longer be a major problem, since it must now always be explicitly asked for.

There are also many Cocoa APIs that accept NSArray and NSDictionary objects with members that are NSValue-boxed structs. Matt Neuberg highlights Core Automation as an example in this bug report <https://bugs.swift.org/browse/SR-2414>. With id-as-Any, it's natural to expect this to work:

anim.toValue = CGPoint.zero
However, the CGPoint value does not box as a meaningful Objective-C object, so this currently breaks Core Animation at runtime despite compiling successfully. It would be more idiomatic to bridge these types to NSValue.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#proposed-solution>Proposed solution

All of Swift's number types should be made to bridge to NSNumber when used as objects in Objective-C:

Int8
Int16
Int32
Int64
UInt8
UInt16
UInt32
UInt64
Float
Double
Cocoa structs with existing NSValue factory and property support should be made to bridge to NSValue when used as objects:

NSRange
CGPoint
CGVector
CGSize
CGRect
CGAffineTransform
UIEdgeInsets
UIOffset
CATransform3D
CMTime
CMTimeRange
CMTimeMapping
MKCoordinate
MKCoordinateSpan
SCNVector3
SCNVector4
SCNMatrix4

How do new types get added to this list? It’s certainly not the case that NSValue knows about them specifically; usually some kind of category is added after the fact.

I think the list was gathered from the documentation at:

  https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSValue_Class/

New types would get added when the owner of a particular framework determines that one of their value types can be mapped to a more-specific Objective-C class than Swift’s box type, at which point an _ObjectiveCBridgeable conformance can be added to do it. This proposal is taking the presence of those NSValue categories as an indication that NSValue is the best Objective-C class to which these types should be mapped.

Also, how does this bridging work for swift-corelibs-foundation on Linux, where bridging is disabled?

It doesn’t. This only affects how value types are boxed when we are providing interoperability with Objective-C.

  - Doug

···

On Aug 23, 2016, at 4:00 PM, Tony Parker <anthony.parker@apple.com> wrote:

On Aug 23, 2016, at 3:36 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

- Tony

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#detailed-design>Detailed design

Bridged NSNumber and NSValue objects must be castable back to their original Swift value types. NSValue normally preserves the type information of its included struct in its objCType property. We can check the objCType of an NSValue instance when attempting to cast back to a specific bridged struct type.

NSNumber is a bit trickier, since Cocoa's implementation does not generally guarantee to remember the exact number type an instance was constructed from. We can instead say that casting an NSNumber to a Swift number type succeeds if the value of the NSNumber is exactly representable as the target type. This is imperfect, since it means that an NSNumbercan potentially be cast to a different type from the original value, but it at least ensures that Swift values round-trip through the bridge without depending on NSNumber implementation details.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, values that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from concrete number and struct types to Anyto id to Any and back by dynamic casting. The ability to reliably distinguish the exact number type that an NSNumberwas constructed from would be lost.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#alternatives-considered>Alternatives considered

We can of course do nothing and leave the behavior as-is.

NSValue also carries factories for valueWithPointer: and valueWithNonretainedObject:. Maybe we could bridgeUnsafePointer and Unmanaged this way, but we probably shouldn’t.

  - Doug
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Joe Groff) #8

Hi Doug,

Introduction

A handful of Swift numeric types are bridged to NSNumber when passed into Objective-C object contexts. We should extend this bridging behavior to all Swift numeric types. We should also bridge common Cocoa structs such as NSRangeby boxing them into NSValue objects.

Swift-evolution thread: TBD

Motivation

SE-0116 changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the hazard of passing in types that don't bridge well to Objective-C objects. Particularly problematic are number types; whereas Int, UInt, and Double will automatically bridge as NSNumber, other-sized numeric types fall back to opaque boxing:

let i = 17
let plist = ["seventeen"
: i]

// OK
try! NSJSONSerialization.
data(withJSONObject: plist)

let j: UInt8 = 38
let brokenPlist = ["thirty-eight"
: j]

// Will throw because `j` didn't bridge to a JSON type
try! NSJSONSerialization.data(withJSONObject: brokenPlist)
We had shied away from enabling this bridging for all numeric types in the Swift 1.x days, among other reasons because we allowed implicit bridging conversions in both directions from Swift value types to NS objects and back, which meant that you could slowly and brokenly convert between any two numeric types transitively via NSNumber if we allowed this. We killed the implicit conversions completely with SE-0072 so that is no longer a concern, so expanding the bridging behavior should no longer be a major problem, since it must now always be explicitly asked for.

There are also many Cocoa APIs that accept NSArray and NSDictionary objects with members that are NSValue-boxed structs. Matt Neuberg highlights Core Automation as an example in this bug report. With id-as-Any, it's natural to expect this to work:

anim.toValue = CGPoint.zero
However, the CGPoint value does not box as a meaningful Objective-C object, so this currently breaks Core Animation at runtime despite compiling successfully. It would be more idiomatic to bridge these types to NSValue.

Proposed solution

All of Swift's number types should be made to bridge to NSNumber when used as objects in Objective-C:

  • Int8
  • Int16
  • Int32
  • Int64
  • UInt8
  • UInt16
  • UInt32
  • UInt64
  • Float
  • Double
Cocoa structs with existing NSValue factory and property support should be made to bridge to NSValue when used as objects:

  • NSRange
  • CGPoint
  • CGVector
  • CGSize
  • CGRect
  • CGAffineTransform
  • UIEdgeInsets
  • UIOffset
  • CATransform3D
  • CMTime
  • CMTimeRange
  • CMTimeMapping
  • MKCoordinate
  • MKCoordinateSpan
  • SCNVector3
  • SCNVector4
  • SCNMatrix4

How do new types get added to this list? It’s certainly not the case that NSValue knows about them specifically; usually some kind of category is added after the fact.

I pulled this list from the API docs. Zach mentioned the existence of the `objc_boxable` attribute in Objective-C. Would it make more sense to make this an importer feature, introducing the bridging to NSValue based on that attribute rather than hard-coding the conformances in the overlay?

Also, how does this bridging work for swift-corelibs-foundation on Linux, where bridging is disabled?

How much corelibs code works directly with NSNumber or NSValue? In native Swift code, you ought to be able to pass around Anys containing value types and dynamically cast out their values with Swift's native casting features.

-Joe

···

On Aug 23, 2016, at 4:00 PM, Tony Parker via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 23, 2016, at 3:36 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:


(Tony Parker) #9

Hi Doug,

Introduction

A handful of Swift numeric types are bridged to NSNumber when passed into Objective-C object contexts. We should extend this bridging behavior to all Swift numeric types. We should also bridge common Cocoa structs such as NSRangeby boxing them into NSValue objects.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the hazard of passing in types that don't bridge well to Objective-C objects. Particularly problematic are number types; whereas Int, UInt, and Double will automatically bridge as NSNumber, other-sized numeric types fall back to opaque boxing:

let i = 17
let plist = ["seventeen": i]
// OK
try! NSJSONSerialization.data(withJSONObject: plist)

let j: UInt8 = 38
let brokenPlist = ["thirty-eight": j]
// Will throw because `j` didn't bridge to a JSON type
try! NSJSONSerialization.data(withJSONObject: brokenPlist)
We had shied away from enabling this bridging for all numeric types in the Swift 1.x days, among other reasons because we allowed implicit bridging conversions in both directions from Swift value types to NS objects and back, which meant that you could slowly and brokenly convert between any two numeric types transitively via NSNumber if we allowed this. We killed the implicit conversions completely with SE-0072 <https://github.com/apple/swift-evolution/blob/master/proposals/0072-eliminate-implicit-bridging-conversions.md> so that is no longer a concern, so expanding the bridging behavior should no longer be a major problem, since it must now always be explicitly asked for.

There are also many Cocoa APIs that accept NSArray and NSDictionary objects with members that are NSValue-boxed structs. Matt Neuberg highlights Core Automation as an example in this bug report <https://bugs.swift.org/browse/SR-2414>. With id-as-Any, it's natural to expect this to work:

anim.toValue = CGPoint.zero
However, the CGPoint value does not box as a meaningful Objective-C object, so this currently breaks Core Animation at runtime despite compiling successfully. It would be more idiomatic to bridge these types to NSValue.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#proposed-solution>Proposed solution

All of Swift's number types should be made to bridge to NSNumber when used as objects in Objective-C:

Int8
Int16
Int32
Int64
UInt8
UInt16
UInt32
UInt64
Float
Double
Cocoa structs with existing NSValue factory and property support should be made to bridge to NSValue when used as objects:

NSRange
CGPoint
CGVector
CGSize
CGRect
CGAffineTransform
UIEdgeInsets
UIOffset
CATransform3D
CMTime
CMTimeRange
CMTimeMapping
MKCoordinate
MKCoordinateSpan
SCNVector3
SCNVector4
SCNMatrix4

How do new types get added to this list? It’s certainly not the case that NSValue knows about them specifically; usually some kind of category is added after the fact.

I think the list was gathered from the documentation at:

  https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSValue_Class/

New types would get added when the owner of a particular framework determines that one of their value types can be mapped to a more-specific Objective-C class than Swift’s box type, at which point an _ObjectiveCBridgeable conformance can be added to do it. This proposal is taking the presence of those NSValue categories as an indication that NSValue is the best Objective-C class to which these types should be mapped.

Also, how does this bridging work for swift-corelibs-foundation on Linux, where bridging is disabled?

It doesn’t. This only affects how value types are boxed when we are providing interoperability with Objective-C.

  - Doug

Then it seems we are introducing an incompatibility, since really this is about interop with Foundation API and not really about Objective-C.

We would need a plan for how Foundation implementation (JSONSerialization is one example, but there are others) is supposed to handle this when the compiler does not provide any help by automatically boxing things like UInt8 into an NSNumber.

NSNumber makes some sense to me, but it’s still not clear to me why it’s important to automatically box something like MKCoordinateSpan into an NSValue when passed into Objective-C, instead of just leaving it in the general purpose SwiftValue box. If any Objective-C API wanted an NSValue it should ask for it, and then people can simply construct it themselves instead of us inserting magic into bridging.

- Tony

···

On Aug 24, 2016, at 11:09 AM, Douglas Gregor <dgregor@apple.com> wrote:

On Aug 23, 2016, at 4:00 PM, Tony Parker <anthony.parker@apple.com <mailto:anthony.parker@apple.com>> wrote:

On Aug 23, 2016, at 3:36 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

- Tony

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#detailed-design>Detailed design

Bridged NSNumber and NSValue objects must be castable back to their original Swift value types. NSValue normally preserves the type information of its included struct in its objCType property. We can check the objCType of an NSValue instance when attempting to cast back to a specific bridged struct type.

NSNumber is a bit trickier, since Cocoa's implementation does not generally guarantee to remember the exact number type an instance was constructed from. We can instead say that casting an NSNumber to a Swift number type succeeds if the value of the NSNumber is exactly representable as the target type. This is imperfect, since it means that an NSNumbercan potentially be cast to a different type from the original value, but it at least ensures that Swift values round-trip through the bridge without depending on NSNumber implementation details.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, values that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from concrete number and struct types to Anyto id to Any and back by dynamic casting. The ability to reliably distinguish the exact number type that an NSNumberwas constructed from would be lost.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#alternatives-considered>Alternatives considered

We can of course do nothing and leave the behavior as-is.

NSValue also carries factories for valueWithPointer: and valueWithNonretainedObject:. Maybe we could bridgeUnsafePointer and Unmanaged this way, but we probably shouldn’t.

  - Doug
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Douglas Gregor) #10

Hi Doug,

Introduction

A handful of Swift numeric types are bridged to NSNumber when passed into Objective-C object contexts. We should extend this bridging behavior to all Swift numeric types. We should also bridge common Cocoa structs such as NSRangeby boxing them into NSValue objects.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the hazard of passing in types that don't bridge well to Objective-C objects. Particularly problematic are number types; whereas Int, UInt, and Double will automatically bridge as NSNumber, other-sized numeric types fall back to opaque boxing:

let i = 17
let plist = ["seventeen": i]
// OK
try! NSJSONSerialization.data(withJSONObject: plist)

let j: UInt8 = 38
let brokenPlist = ["thirty-eight": j]
// Will throw because `j` didn't bridge to a JSON type
try! NSJSONSerialization.data(withJSONObject: brokenPlist)
We had shied away from enabling this bridging for all numeric types in the Swift 1.x days, among other reasons because we allowed implicit bridging conversions in both directions from Swift value types to NS objects and back, which meant that you could slowly and brokenly convert between any two numeric types transitively via NSNumber if we allowed this. We killed the implicit conversions completely with SE-0072 <https://github.com/apple/swift-evolution/blob/master/proposals/0072-eliminate-implicit-bridging-conversions.md> so that is no longer a concern, so expanding the bridging behavior should no longer be a major problem, since it must now always be explicitly asked for.

There are also many Cocoa APIs that accept NSArray and NSDictionary objects with members that are NSValue-boxed structs. Matt Neuberg highlights Core Automation as an example in this bug report <https://bugs.swift.org/browse/SR-2414>. With id-as-Any, it's natural to expect this to work:

anim.toValue = CGPoint.zero
However, the CGPoint value does not box as a meaningful Objective-C object, so this currently breaks Core Animation at runtime despite compiling successfully. It would be more idiomatic to bridge these types to NSValue.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#proposed-solution>Proposed solution

All of Swift's number types should be made to bridge to NSNumber when used as objects in Objective-C:

Int8
Int16
Int32
Int64
UInt8
UInt16
UInt32
UInt64
Float
Double
Cocoa structs with existing NSValue factory and property support should be made to bridge to NSValue when used as objects:

NSRange
CGPoint
CGVector
CGSize
CGRect
CGAffineTransform
UIEdgeInsets
UIOffset
CATransform3D
CMTime
CMTimeRange
CMTimeMapping
MKCoordinate
MKCoordinateSpan
SCNVector3
SCNVector4
SCNMatrix4

How do new types get added to this list? It’s certainly not the case that NSValue knows about them specifically; usually some kind of category is added after the fact.

I think the list was gathered from the documentation at:

  https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSValue_Class/

New types would get added when the owner of a particular framework determines that one of their value types can be mapped to a more-specific Objective-C class than Swift’s box type, at which point an _ObjectiveCBridgeable conformance can be added to do it. This proposal is taking the presence of those NSValue categories as an indication that NSValue is the best Objective-C class to which these types should be mapped.

Also, how does this bridging work for swift-corelibs-foundation on Linux, where bridging is disabled?

It doesn’t. This only affects how value types are boxed when we are providing interoperability with Objective-C.

  - Doug

Then it seems we are introducing an incompatibility, since really this is about interop with Foundation API and not really about Objective-C.

It’s tightening up the “boxed” representation exposed to Objective-C. I don’t see that it introduces a new incompatibility; at worst, it expends an existing incompatibility that exists for Int/UInt/Float/Double to more types.

We would need a plan for how Foundation implementation (JSONSerialization is one example, but there are others) is supposed to handle this when the compiler does not provide any help by automatically boxing things like UInt8 into an NSNumber.

The corelibs JSONSerialization will need to deal with these types somehow—probably by having them conform to some protocol that allows them to work with JSONSerialization. If we have to do that by boxing them in (NS|CF)Number/(NS|CF)Value to share implementation at the CF level, we can do that via a protocol or specific type checks.

NSNumber makes some sense to me, but it’s still not clear to me why it’s important to automatically box something like MKCoordinateSpan into an NSValue when passed into Objective-C, instead of just leaving it in the general purpose SwiftValue box. If any Objective-C API wanted an NSValue it should ask for it, and then people can simply construct it themselves instead of us inserting magic into bridging.

MKCoordinateSpan will get boxed regardless; this boxes it the same way an Objective-C programmer would so it’ll be seen appropriately in Objective-C.

  - Doug

···

On Aug 24, 2016, at 11:29 AM, Tony Parker <anthony.parker@apple.com> wrote:

On Aug 24, 2016, at 11:09 AM, Douglas Gregor <dgregor@apple.com <mailto:dgregor@apple.com>> wrote:

On Aug 23, 2016, at 4:00 PM, Tony Parker <anthony.parker@apple.com <mailto:anthony.parker@apple.com>> wrote:

On Aug 23, 2016, at 3:36 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

- Tony

- Tony

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#detailed-design>Detailed design

Bridged NSNumber and NSValue objects must be castable back to their original Swift value types. NSValue normally preserves the type information of its included struct in its objCType property. We can check the objCType of an NSValue instance when attempting to cast back to a specific bridged struct type.

NSNumber is a bit trickier, since Cocoa's implementation does not generally guarantee to remember the exact number type an instance was constructed from. We can instead say that casting an NSNumber to a Swift number type succeeds if the value of the NSNumber is exactly representable as the target type. This is imperfect, since it means that an NSNumbercan potentially be cast to a different type from the original value, but it at least ensures that Swift values round-trip through the bridge without depending on NSNumber implementation details.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, values that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from concrete number and struct types to Anyto id to Any and back by dynamic casting. The ability to reliably distinguish the exact number type that an NSNumberwas constructed from would be lost.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-nsnumber-and-nsvalue.md#alternatives-considered>Alternatives considered

We can of course do nothing and leave the behavior as-is.

NSValue also carries factories for valueWithPointer: and valueWithNonretainedObject:. Maybe we could bridgeUnsafePointer and Unmanaged this way, but we probably shouldn’t.

  - Doug
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution