[swift-evolution] [Proposal draft] NSError bridging

Shawn Erickson shawnce at gmail.com
Tue Jun 28 10:40:08 CDT 2016


I did a quick read and this looks great! Thanks to you two for pulling this
together.

I will attempt a deeper read and comment as needed later today. I am
interested in helping with this as possible.

-Shawn

On Mon, Jun 27, 2016 at 2:41 PM Douglas Gregor via swift-evolution <
swift-evolution at swift.org> wrote:

> Hi all,
>
> Proposal link:
> https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md
>
> Here is a detailed proposal draft for bridging NSError to ErrorProtocol.
> Getting this right is surprisingly involved, so the detailed design on this
> proposal is fairly large. Comments welcome!
>
> - Doug
>
> NSError Bridging
>
>    - Proposal: SE-NNNN
>    <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/NNNN-nserror-bridging.md>
>    - Author: Doug Gregor <https://github.com/DougGregor>, Charles Srstka
>    <https://github.com/CharlesJS>
>    - Status: Awaiting review
>    - Review manager: TBD
>
>
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#introduction>
> Introduction
>
> Swift's error handling model interoperates directly with Cocoa's NSError
> conventions. For example, an Objective-C method with an NSError** parameter,
> e.g.,
>
> - (nullable NSURL *)replaceItemAtURL:(NSURL *)url options:(NSFileVersionReplacingOptions)options error:(NSError **)error;
>
> will be imported as a throwing method:
>
> func replaceItem(at url: URL, options: ReplacingOptions = []) throws -> URL
>
> Swift bridges between ErrorProtocol-conforming types and NSError so, for
> example, a Swift enum that conforms toErrorProtocol can be thrown and
> will be reflected as an NSError with a suitable domain and code.
> Moreover, an NSErrorproduced with that domain and code can be caught as
> the Swift enum type, providing round-tripping so that Swift can deal in
> ErrorProtocol values while Objective-C deals in NSError objects.
>
> However, the interoperability is incomplete in a number of ways, which
> results in Swift programs having to walk a careful line between the
> ErrorProtocol-based Swift way and the NSError-based way. This proposal
> attempts to bridge those gaps.
>
> Swift-evolution thread: Charles Srstka's pitch for Consistent bridging
> for NSErrors at the language boundary
> <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160502/016618.html>,
> which discussed Charles' original proposal
> <https://github.com/apple/swift-evolution/pull/331> that addressed these
> issues by providing NSError to ErrorProtocol bridging and exposing the
> domain, code, and user-info dictionary for all errors. This proposal
> expands upon that work, but without directly exposing the domain, code, and
> user-info.
>
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#motivation>
> Motivation
>
> There are a number of weaknesses in Swift's interoperability with Cocoa's
> error model, including:
>
>    1.
>
>    There is no good way to provide important error information when
>    throwing an error from Swift. For example, let's consider a simple
>    application-defined error in Swift:
>
>    enum HomeworkError : Int, ErrorProtocol {
>      case forgotten
>      case lost
>      case dogAteIt
>    }
>
>    One can throw HomeworkError.dogAteIt and it can be interpreted as an
>    NSError by Objective-C with an appropriate error domain (effectively,
>    the mangled name of the HomeworkError type) and code (effectively, the
>    case discriminator). However, one cannot provide a localized description,
>    help anchor, recovery attempter, or any other information commonly placed
>    into the userInfo dictionary of an NSError. To provide these values,
>    one must specifically construct an NSError in Swift, e.g.,
>
>    throw NSError(code: HomeworkError.dogAteIt.rawValue,
>                  domain: HomeworkError._domain,
>                  userInfo: [ NSLocalizedDescriptionKey : "the dog ate it" ])
>
>    2.
>
>    There is no good way to get information typically associated with
>    NSError's userInfo in Swift. For example, the Swift-natural way to
>    catch a specific error in the AVError error domain doesn't give one
>    access to the userInfo dictionary, e.g.,:
>
>    catch let error as AVError where error == .diskFull {
>      // AVError is an enum, so one only gets the equivalent of the code.
>      // There is no way to access the localized description (for example) or
>      // any other information typically stored in the ``userInfo`` dictionary.
>    }
>
>    The workaround is to catch as an NSError, which is quite a bit more
>    ugly:
>
>    catch let error as NSError where error._domain == AVFoundationErrorDomain && error._code == AVFoundationErrorDomain.diskFull.rawValue {
>      // okay: userInfo is finally accessible, but still weakly typed
>    }
>
>    This makes it extremely hard to access common information, such as the
>    localized description. Moreover, the userInfo dictionary is
>    effectively untyped so, for example, one has to know a priori that the
>    value associated with the known AVErrorDeviceKey will be typed as
>    CMTime:
>
>    catch let error as NSError where error._domain = AVFoundationErrorDomain {
>      if let time = error.userInfo[AVErrorDeviceKey] as? CMTime {
>        // ...
>      }
>    }
>
>    It would be far better if one could catch an AVError directly and
>    query the time in a type-safe manner:
>
>    catch let error as AVError {
>      if let time = error.time {
>        // ...
>      }
>    }
>
>    3.
>
>    NSError is inconsistently bridged with ErrorProtocol. Swift
>    interoperates by translating between NSError and ErrorProtocol when
>    mapping between a throwing Swift method/initializer and an Objective-C
>    method with an NSError** parameter. However, an Objective-C method
>    that takes an NSError* parameter (e.g., to render it) does not bridge
>    to ErrorProtocol, meaning that NSError is part of the API in Swift in
>    some places (but not others). For example, NSError leaks through when
>    the following UIDocument API in Objective-C:
>
>    - (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;
>
>    is imported into Swift as follows:
>
>    func handleError(_ error: NSError, userInteractionPermitted: Bool)
>
>    One would expect the first parameter to be imported as ErrorProtocol.
>
>
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#proposed-solution>Proposed
> solution
>
> This proposal involves directly addressing (1)-(3) with new protocols and
> a different way of bridging Objective-C error code types into Swift, along
> with some conveniences for working with Cocoa errors:
>
>    1.
>
>    Introduce three new protocols for describing more information about
>    errors: LocalizedError, RecoverableError, andCustomNSError. For
>    example, an error type can provide a localized description by conforming to
>    LocalizedError:
>
>    extension HomeworkError : LocalizedError {
>      var errorDescription: String? {
>        switch self {
>        case .forgotten: return NSLocalizedString("I forgot it")
>        case .lost: return NSLocalizedString("I lost it")
>        case .dogAteIt: return NSLocalizedString("The dog ate it")
>        }
>      }
>    }
>
>    2.
>
>    Imported Objective-C error types should be mapped to struct types that
>    store an NSError so that no information is lost when bridging from an
>    NSError to the Swift error types. We propose to introduce a new macro,
>    NS_ERROR_ENUM, that one can use to both declare an enumeration type
>    used to describe the error codes as well as tying that type to a specific
>    domain constant, e.g.,
>
>    typedef NS_ERROR_ENUM(NSInteger, AVError, AVFoundationErrorDomain) {
>      AVErrorUnknown                                      = -11800,
>      AVErrorOutOfMemory                                  = -11801,
>      AVErrorSessionNotRunning                            = -11803,
>      AVErrorDeviceAlreadyUsedByAnotherSession            = -11804,
>      // ...
>    }
>
>    The imported AVError will have a struct that allows one to access the
>    userInfo dictionary directly. This retains the ability to catch via a
>    specific code, e.g.,
>
>    catch AVError.outOfMemory {
>     // ...
>    }
>
>    However, catching a specific error as a value doesn't lose information:
>
>    catch let error as AVError where error.code == .sessionNotRunning {
>     // able to access userInfo here!
>    }
>
>    This also gives the ability for one to add typed accessors for known
>    keys within the userInfo dictionary:
>
>    extension AVError {
>     var time: CMTime? {
>       get {
>         return userInfo[AVErrorTimeKey] as? CMTime?
>       }
>
>       set {
>         userInfo[AVErrorTimeKey] = newValue.map { $0 as CMTime }
>       }
>     }
>    }
>
>    3.
>
>    Bridge NSError to ErrorProtocol, so that all NSError uses are bridged
>    consistently. For example, this means that the Objective-C API:
>
>    - (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;
>
>    is imported into Swift as:
>
>    func handleError(_ error: ErrorProtocol, userInteractionPermitted: Bool)
>
>    This will use the same bridging logic in the Clang importer that we
>    use for other value types (Array, String, URL, etc.), but with the
>    runtime translation we've already been doing for catching/throwing errors.
>
>    When we introduce this bridging, we will need to remove NSError's
>    conformance to ErrorProtocol to avoid creating cyclic implicit
>    conversions. However, we still need an easy way to create an
>    ErrorProtocol instance from an arbitrary NSError, e.g.,
>
>    extension NSError {
>      var asError: ErrorProtocol { ... }
>    }
>
>    4.
>
>    In Foundation, add an extension to ErrorProtocol that provides typed
>    access to the common user-info keys. Note that we focus only on those
>    user-info keys that are read by user code (vs. only accessed by frameworks):
>
>    extension ErrorProtocol {
>      // Note: for exposition only. Not actual API.
>      private var userInfo: [NSObject : AnyObject] {
>        return (self as! NSError).userInfo
>      }
>
>      var localizedDescription: String {
>        return (self as! NSError).localizedDescription
>      }
>
>      var filePath: String? {
>        return userInfo[NSFilePathErrorKey] as? String
>      }
>
>      var stringEncoding: String.Encoding? {
>        return (userInfo[NSStringEncodingErrorKey] as? NSNumber)
>                 .map { String.Encoding(rawValue: $0.uintValue) }
>      }
>
>      var underlying: ErrorProtocol? {
>        return (userInfo[NSUnderlyingErrorKey] as? NSError)?.asError
>      }
>
>      var url: URL? {
>        return userInfo[NSURLErrorKey] as? URL
>      }
>    }
>
>    5.
>
>    Rename ErrorProtocol to Error: once we've completed the bridging
>    story, Error becomes the primary way to work with error types in
>    Swift, and the value type to which NSError is bridged:
>
>    func handleError(_ error: Error, userInteractionPermitted: Bool)
>
>
>
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#detailed-design>Detailed
> design
>
> This section details both the design (including the various new protocols,
> mapping from Objective-C error code enumeration types into Swift types,
> etc.) and the efficient implementation of this design to interoperate with
> NSError. Throughout the detailed design, we already assume the name
> change from ErrorProtocol to Error.
>
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#new-protocols>New
> protocols
>
> This proposal introduces several new protocols that allow error types to
> expose more information about error types.
>
> The LocalizedError protocol describes an error that provides localized
> messages for display to the end user, all of which provide default
> implementations. The conforming type can provide implementations for any
> subset of these requirements:
>
> protocol LocalizedError : Error {
>   /// A localized message describing what error occurred.
>   var errorDescription: String? { get }
>
>   /// A localized message describing the reason for the failure.
>   var failureReason: String? { get }
>
>   /// A localized message describing how one might recover from the failure.
>   var recoverySuggestion: String? { get }
>
>   /// A localized message providing "help" text if the user requests help.
>   var helpAnchor: String? { get }
> }
> extension LocalizedError {
>   var errorDescription: String? { return nil }
>   var failureReason: String? { return nil }
>   var recoverySuggestion: String? { return nil }
>   var helpAnchor: String? { return nil }
> }
>
> The RecoverableError protocol describes an error that might be
> recoverable:
>
> protocol RecoverableError : Error {
>   /// Provides a set of possible recovery options to present to the user.
>   var recoveryOptions: [String] { get }
>
>   /// Attempt to recover from this error when the user selected the
>   /// option at the given index. This routine must call resultHandler and
>   /// indicate whether recovery was successful (or not).
>   ///
>   /// This entry point is used for recovery of errors handled at a
>   /// "document" granularity, that do not affect the entire
>   /// application.
>   func attemptRecovery(optionIndex recoveryOptionIndex: Int,
>                        andThen resultHandler: (recovered: Bool) -> Void)
>
>   /// Attempt to recover from this error when the user selected the
>   /// option at the given index. Returns true to indicate
>   /// successful recovery, and false otherwise.
>   ///
>   /// This entry point is used for recovery of errors handled at
>   /// the "application" granularity, where nothing else in the
>   /// application can proceed until the attmpted error recovery
>   /// completes.
>   func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool
> }
> extension RecoverableError {
>   /// By default, implements document-modal recovery via application-model
>   /// recovery.
>   func attemptRecovery(optionIndex recoveryOptionIndex: Int,
>                        andThen resultHandler: (Bool) -> Void) {
>     resultHandler(recovered: attemptRecovery(optionIndex: recoveryOptionIndex))
>   }
> }
>
> Error types that conform to RecoverableError may be given an opportunity
> to recover from the error. The user can be presented with some number of
> (localized) recovery options, described by recoveryOptions, and the
> selected option will be passed to the appropriate attemptRecovery method.
>
> The CustomNSError protocol describes an error that wants to provide
> custom NSError information. This can be used, e.g., to provide a specific
> domain/code or to populate NSError's userInfo dictionary with values for
> custom keys that can be accessed from Objective-C code but are not covered
> by the other protocols.
>
> /// Describes an error type that fills in the userInfo directly.protocol CustomNSError : Error {
>   var errorDomain: String { get }
>   var errorCode: Int { get }
>   var errorUserInfo: [String : AnyObject] { get }
> }
>
> Note that, unlike with NSError, the provided errorUserInfo requires String keys.
> This is in line with common practice for NSError and is important for the
> implementation (see below). All of these properties are defaulted, so one
> can provide any subset:
>
> extension CustomNSError {
>   var errorDomain: String { ... }
>   var errorCode: Int { ... }
>   var errorUserInfo: [String : AnyObject] { ... }
> }
>
>
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#mapping-error-types-to-nserror>Mapping
> error types to NSError
>
> Every type that conforms to the Error protocol is implicitly bridged to
> NSError. This has been the case since Swift 2, where the compiler
> provides a domain (i.e., the mangled name of the type) and code (based on
> the discriminator of the enumeration type). This proposal also allows for
> the userInfo dictionary to be populated by the runtime, which will check
> for conformance to the various protocols (LocalizedError, RecoverableError,
> or CustomNSError) to retrieve information.
>
> Conceptually, this could be implemented by eagerly creating a userInfo dictionary
> for a given instance of Error:
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160628/a157592a/attachment.html>


More information about the swift-evolution mailing list