[Casting] Structs cannot conform to NSObjectProtocol
Casting a struct to NSObjectProtocol should always fail. This was partly fixed last year, but there was still a hole that would successfully cast an Optional<S> to NSObjectProtocol because the cast logic in this case lost track of the destination type.
More specifically, tryCastFromObjCBridgeableToClass was also being used to cast to class existentials. These two cases (casting to a class or casting to a class-constrained existential) both involve the same bridge logic, but then need to follow it with an additional step to get to the final type. Creating a new tryCastFromObjCBridgeableToClassExistential made it easy to separate these cases.
Resolves rdar://107559117
This is likely to cause some issues with breaking existing code. I suspect that means it should not be merged yet -- let's let 6.1 go past, then merge it in the Spring when people will have a longer period to adapt to the change. (We'll likely need some bincompat checks in this as well.)
Is this true in the face of bridging? Does e.g. ("1" as Any) as? NSObjectProtocol currently work by bridging to NSString?
Yes, although we use Swift's StringStorage so it's not very different. It's more obvious with an integer, e.g. import Foundation; print(type(of: (1 as Any) as! NSObjectProtocol)) prints __NSCFNumber.
Does e.g. ("1" as Any) as? NSObjectProtocol currently work by bridging to NSString?
Yes. In this case, the as? cast would proceed as follows:
Anydoes not directly cast to a class existential- Unwrap the
Anyto get a Swift String - Swift String does not directly cast to a class existential
- As a fallback, after exploring several direct cast options, we try Obj-C bridging
- Swift String bridges to NSString
- NSString casts to the NSObjectProtocol class existential
- Success!
The title here may be a bit misleading: The problem is limited to structs that do not support Obj-C bridging. Since such structs cannot conform to NSObjectProtocol, it should not be possible to cast them to the class existential.
Attempting to cast struct S {} to NSObjectProtocol fails as you would expect -- there's no Obj-C bridging fallback so the cast ultimately fails. But Optional<S> is bridgeable; if nil it bridges to NSNull, otherwise it bridges using as! AnyObject, which in this case boxes the struct into a __SwiftValue box.
- Old version: The
__SwiftValuebox does conform toNSObjectProtocol, so succeeds. This is wrong. - New version: We try casting
__SwiftValuebox toNSObjectProtocolusing a full recursive cast. The full cast machinery knows that__SwiftValuemust be unconditionally unwrapped, so ultimately tests whetherSconforms toNSObjectProtocoland then fails as it should.
I did consider whether there was a way to avoid boxing into __SwiftValue just to have to unbox again, but I don't see a clear way to do that without changing the ObjectiveCBridgeable protocol itself. That seemed overly intrusive.
An alternative approach would be to modify the _ObjectiveCBridgeable protocol so that the caller could specify the expected result type.
Currently, that protocol lets the type specify the output type:
public protocol _ObjectiveCBridgeable {
associatedtype _ObjectiveCType: AnyObject
func _bridgeToObjectiveC() -> _ObjectiveCType
But that puts the onus on the caller to do additional work when casting to a constrained existential type. If the caller specified the target type, that would let us push any protocol conformance checks further down:
func _bridgeToObjectiveC<R>(type: R.Type) throws -> R
In essence, this would just extend the tryCast() model down into the bridging logic. If we pursued that, we'd probably want to plumb that entire model through, including take/copy preferences and failure reporting info.