How safe is `Mirror` meant to be?

Some context:

I have the following code that is meant to look up property names for view controllers:

func getPropertyName(of target: UIResponder, in parent: UIResponder) -> String? {
        
  for child in Mirror(reflecting: parent).children {
    if let value = child.value as? UIResponder,
       value === target,
       let label = child.label {
      return "\(type(of: parent)).\(label)"
    }
  }
        
  return nil
}

This code is meant to be used as getPropertyName(of sender, in: someViewController) and return something like "ItemViewController.addToCartBtn"`.

In the wild, a customer is seeing a crash with the following message and stack:

Found unexpected null pointer value while trying to cast value of type 'NSTimer' (0x...) to 'UIResponder' (0x...)

libswiftCore.dylib`getNonNullSrcObject(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*) + 256
libswiftCore.dylib`tryCastToObjectiveCClass(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*&, swift::TargetMetadata<swift::InProcess> const*&, bool, bool) + 88
libswiftCore.dylib`tryCast(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*&, swift::TargetMetadata<swift::InProcess> const*&, bool, bool) + 992
libswiftCore.dylib`tryCast(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*&, swift::TargetMetadata<swift::InProcess> const*&, bool, bool) + 1924

This suggested that children returned a property dictionary and the cast to responder is failing due to a potential bug in Swift or edge condition.

But that's only half the story, since while trying to reproduce that, I was able to trivially cause a different crash by having it run on an object with a deallocated unowned:

class ViewController: UIViewController {
    
    unowned var myTimer: Timer
    
    required init?(coder: NSCoder) {
        let timer = Timer(timeInterval: 5, repeats: false, block: { _ in })
        myTimer = timer
        super.init(coder: coder)
    }
}
Fatal error: Attempted to read an unowned reference but the object was already deallocated

swift::swift_abortRetainUnowned(void const*) ()
swift_unknownObjectUnownedLoadStrong ()
(anonymous namespace)::copyFieldContents(swift::OpaqueValue*, (anonymous namespace)::FieldType) ()
swift_reflectionMirror_subscript ()

So...

These crashes lead me to believe that Mirror is not nearly as safe as it looks. Should I not use Mirror in production code or are there additional safeguards I can put in place to avoid fatal errors?

I'd avoid Mirror in production code. It can be very useful sometimes, but in my experience it's pretty crashtastic. :confused:

Swift lacks proper introspection capabilities - Mirror is just a bit of a hacky stopgap (IMO). There's been a few proposals for a proper system, over the years, but alas none are currently active.

1 Like