swift icon indicating copy to clipboard operation
swift copied to clipboard

[Casting] Structs cannot conform to NSObjectProtocol

Open tbkka opened this issue 1 year ago • 2 comments

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

tbkka avatar Jul 11 '24 22:07 tbkka

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.)

tbkka avatar Jul 11 '24 22:07 tbkka

Is this true in the face of bridging? Does e.g. ("1" as Any) as? NSObjectProtocol currently work by bridging to NSString?

jckarter avatar Jul 12 '24 01:07 jckarter

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.

mikeash avatar Jul 12 '24 13:07 mikeash

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:

  • Any does not directly cast to a class existential
  • Unwrap the Any to 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 __SwiftValue box does conform to NSObjectProtocol, so succeeds. This is wrong.
  • New version: We try casting __SwiftValue box to NSObjectProtocol using a full recursive cast. The full cast machinery knows that __SwiftValue must be unconditionally unwrapped, so ultimately tests whether S conforms to NSObjectProtocol and 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.

tbkka avatar Jul 12 '24 19:07 tbkka

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.

tbkka avatar Jul 12 '24 20:07 tbkka