diff --git a/stdlib/public/SDK/Foundation/NSError.swift b/stdlib/public/SDK/Foundation/NSError.swift index f7a34d0fba3..ce917c15bd8 100644 --- a/stdlib/public/SDK/Foundation/NSError.swift +++ b/stdlib/public/SDK/Foundation/NSError.swift @@ -13,6 +13,177 @@ import CoreFoundation import Darwin +/// Describes an error that provides localized messages describing why +/// an error occurred and provides more information about the error. +public protocol LocalizedError : ErrorProtocol { + /// 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 } +} + +public extension LocalizedError { + var errorDescription: String? { return nil } + var failureReason: String? { return nil } + var recoverySuggestion: String? { return nil } + var helpAnchor: String? { return nil } +} + +@_silgen_name("NS_Swift_performErrorRecoverySelector") +internal func NS_Swift_performErrorRecoverySelector( + delegate: AnyObject?, + selector: Selector, + success: ObjCBool, + contextInfo: UnsafeMutablePointer?) + +/// Class that implements the informal protocol +/// NSErrorRecoveryAttempting, which is used by NSError when it +/// attempts recovery from an error. +class _NSErrorRecoveryAttempter { + // FIXME: If we could meaningfully cast the nsError back to RecoverableError, + // we wouldn't need to capture this and could use the user-info + // domain providers even for recoverable errors. + let error: RecoverableError + + init(error: RecoverableError) { + self.error = error + } + + @objc(attemptRecoveryFromError:optionIndex:delegate:didRecoverSelector:contextInfo:) + func attemptRecovery(fromError nsError: NSError, + optionIndex recoveryOptionIndex: Int, + delegate: AnyObject?, + didRecoverSelector: Selector, + contextInfo: UnsafeMutablePointer?) { + error.attemptRecovery(optionIndex: recoveryOptionIndex) { success in + NS_Swift_performErrorRecoverySelector( + delegate: delegate, + selector: didRecoverSelector, + success: ObjCBool(success), + contextInfo: contextInfo) + } + } + + @objc(attemptRecoveryFromError:optionIndex:) + func attemptRecovery(fromError nsError: NSError, + optionIndex recoveryOptionIndex: Int) -> Bool { + return error.attemptRecovery(optionIndex: recoveryOptionIndex) + } +} + +/// Describes an error that may be recoverably by presenting several +/// potential recovery options to the user. +public protocol RecoverableError : ErrorProtocol { + /// 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 +} + +public extension RecoverableError { + /// By default, implements document-modal recovery via application-model + /// recovery. + func attemptRecovery(optionIndex recoveryOptionIndex: Int, + andThen resultHandler: (recovered: Bool) -> Void) { + resultHandler(recovered: attemptRecovery(optionIndex: recoveryOptionIndex)) + } +} + +/// Describes an error type that specifically provides a domain, code, +/// and user-info dictionary. +public protocol CustomNSError : ErrorProtocol { + /// The domain of the error. + var errorDomain: String { get } + + /// The error code within the given domain. + var errorCode: Int { get } + + /// The user-info dictionary. + var errorUserInfo: [String : AnyObject] { get } +} + +public extension ErrorProtocol where Self : CustomNSError { + /// Default implementation for customized NSErrors. + var _domain: String { return self.errorDomain } + + /// Default implementation for customized NSErrors. + var _code: Int { return self.errorCode } +} + +/// Retrieve the default userInfo dictionary for a given error. +@_silgen_name("swift_Foundation_getErrorDefaultUserInfo") +public func _swift_Foundation_getErrorDefaultUserInfo(_ error: ErrorProtocol) + -> AnyObject? { + // If the OS supports value user info value providers, use those + // to lazily populate the user-info dictionary for this domain. + if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) { + // FIXME: This is not implementable until we can recover the + // original error from an NSError. + } + + // Populate the user-info dictionary + var result: [String : AnyObject] + + // Initialize with custom user-info. + if let customNSError = error as? CustomNSError { + result = customNSError.errorUserInfo + } else { + result = [:] + } + + if let localizedError = error as? LocalizedError { + if let description = localizedError.errorDescription { + result[NSLocalizedDescriptionKey] = description as AnyObject + } + + if let reason = localizedError.failureReason { + result[NSLocalizedFailureReasonErrorKey] = reason as AnyObject + } + + if let suggestion = localizedError.recoverySuggestion { + result[NSLocalizedRecoverySuggestionErrorKey] = suggestion as AnyObject + } + + if let helpAnchor = localizedError.helpAnchor { + result[NSHelpAnchorErrorKey] = helpAnchor as AnyObject + } + } + + if let recoverableError = error as? RecoverableError { + result[NSLocalizedRecoveryOptionsErrorKey] = + recoverableError.recoveryOptions as AnyObject + result[NSRecoveryAttempterErrorKey] = + _NSErrorRecoveryAttempter(error: recoverableError) + } + + return result as AnyObject +} + // NSError and CFError conform to the standard ErrorProtocol protocol. Compiler // magic allows this to be done as a "toll-free" conversion when an NSError // or CFError is used as an ErrorProtocol existential. @@ -20,6 +191,7 @@ import Darwin extension NSError : ErrorProtocol { public var _domain: String { return domain } public var _code: Int { return code } + public var _userInfo: AnyObject? { return userInfo as AnyObject } } extension CFError : ErrorProtocol { @@ -30,6 +202,10 @@ extension CFError : ErrorProtocol { public var _code: Int { return CFErrorGetCode(self) } + + public var _userInfo: AnyObject? { + return CFErrorCopyUserInfo(self) as AnyObject? + } } // An error value to use when an Objective-C API indicates error diff --git a/stdlib/public/SDK/Foundation/Thunks.mm b/stdlib/public/SDK/Foundation/Thunks.mm index b5a0311afd0..620a0857428 100644 --- a/stdlib/public/SDK/Foundation/Thunks.mm +++ b/stdlib/public/SDK/Foundation/Thunks.mm @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// #import +#include #include "swift/Runtime/Config.h" @@ -118,3 +119,12 @@ NS_Swift_NSKeyedUnarchiver_unarchiveObjectWithData( return [result retain]; } +// -- NSError +SWIFT_CC(swift) +extern "C" void +NS_Swift_performErrorRecoverySelector(_Nullable id delegate, + SEL selector, + BOOL success, + void * _Nullable contextInfo) { + objc_msgSend(delegate, selector, success, contextInfo); +} diff --git a/stdlib/public/core/ErrorType.swift b/stdlib/public/core/ErrorType.swift index 737c3737654..664822b3a7c 100644 --- a/stdlib/public/core/ErrorType.swift +++ b/stdlib/public/core/ErrorType.swift @@ -9,6 +9,7 @@ // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// +import SwiftShims // TODO: API review /// A type representing an error value that can be thrown. @@ -111,17 +112,12 @@ public protocol ErrorProtocol { var _domain: String { get } var _code: Int { get } -} - -extension ErrorProtocol { - public var _domain: String { - return String(reflecting: self.dynamicType) - } + var _userInfo: AnyObject? { get } } #if _runtime(_ObjC) -// Helper functions for the C++ runtime to have easy access to domain and -// code as Objective-C values. +// Helper functions for the C++ runtime to have easy access to domain, +// code, and userInfo as Objective-C values. @_silgen_name("swift_stdlib_getErrorDomainNSString") public func _stdlib_getErrorDomainNSString(_ x: UnsafePointer) -> AnyObject { @@ -133,6 +129,17 @@ public func _stdlib_getErrorCode(_ x: UnsafePointer) -> In return x.pointee._code } +// Helper functions for the C++ runtime to have easy access to domain and +// code as Objective-C values. +@_silgen_name("swift_stdlib_getErrorUserInfoNSDictionary") +public func _stdlib_getErrorUserInfoNSDictionary(_ x: UnsafePointer) +-> AnyObject? { + return x.pointee._userInfo +} + +@_silgen_name("swift_stdlib_getErrorDefaultUserInfo") +public func _stdlib_getErrorDefaultUserInfo(_ error: ErrorProtocol) -> AnyObject? + // Known function for the compiler to use to coerce `ErrorProtocol` instances // to `NSError`. @_silgen_name("swift_bridgeErrorProtocolToNSError") @@ -154,3 +161,17 @@ public func _errorInMain(_ error: ErrorProtocol) { @available(*, unavailable, renamed: "ErrorProtocol") public typealias ErrorType = ErrorProtocol + +extension ErrorProtocol { + public var _domain: String { + return String(reflecting: self.dynamicType) + } + + public var _userInfo: AnyObject? { +#if _runtime(_ObjC) + return _stdlib_getErrorDefaultUserInfo(self) +#else + return nil +#endif + } +} diff --git a/stdlib/public/runtime/ErrorObject.mm b/stdlib/public/runtime/ErrorObject.mm index ec5ac336969..0be35ca4955 100644 --- a/stdlib/public/runtime/ErrorObject.mm +++ b/stdlib/public/runtime/ErrorObject.mm @@ -285,6 +285,34 @@ extern "C" NSInteger swift_stdlib_getErrorCode(const OpaqueValue *error, const Metadata *T, const WitnessTable *ErrorProtocol); +//@_silgen_name("swift_stdlib_getErrorUserInfoNSDictionary") +//public func _stdlib_getErrorUserInfoNSDictionary(_ x: UnsafePointer) -> AnyObject +SWIFT_CC(swift) +extern "C" NSDictionary *swift_stdlib_getErrorUserInfoNSDictionary( + const OpaqueValue *error, + const Metadata *T, + const WitnessTable *ErrorProtocol); + +//@_silgen_name("swift_stdlib_getErrorDefaultUserInfo") +//public func _stdlib_getErrorDefaultUserInfo(_ x: UnsafePointer) -> AnyObject +SWIFT_CC(swift) SWIFT_RT_ENTRY_VISIBILITY +extern "C" NSDictionary *swift_stdlib_getErrorDefaultUserInfo( + const OpaqueValue *error, + const Metadata *T, + const WitnessTable *ErrorProtocol) { + typedef SWIFT_CC(swift) + NSDictionary *GetDefaultFn(const OpaqueValue *error, + const Metadata *T, + const WitnessTable *ErrorProtocol); + + auto foundationGetDefaultUserInfo = SWIFT_LAZY_CONSTANT( + reinterpret_cast + (dlsym(RTLD_DEFAULT, "swift_Foundation_getErrorDefaultUserInfo"))); + if (!foundationGetDefaultUserInfo) { return nullptr; } + + return foundationGetDefaultUserInfo(error, T, ErrorProtocol); +} + /// Take an ErrorProtocol box and turn it into a valid NSError instance. SWIFT_CC(swift) static id _swift_bridgeErrorProtocolToNSError_(SwiftError *errorObject) { @@ -302,21 +330,30 @@ static id _swift_bridgeErrorProtocolToNSError_(SwiftError *errorObject) { NSString *domain = swift_stdlib_getErrorDomainNSString(value, type, witness); NSInteger code = swift_stdlib_getErrorCode(value, type, witness); - // TODO: user info? - + NSDictionary *userInfo = + swift_stdlib_getErrorUserInfoNSDictionary(value, type, witness); + // The error code shouldn't change, so we can store it blindly, even if // somebody beat us to it. The store can be relaxed, since we'll do a // store(release) of the domain last thing to publish the initialized // NSError. errorObject->code.store(code, std::memory_order_relaxed); - // However, we need to cmpxchg in the domain; if somebody beat us to it, + // However, we need to cmpxchg the userInfo; if somebody beat us to it, + // we need to release. + CFDictionaryRef expectedUserInfo = nullptr; + if (!errorObject->userInfo.compare_exchange_strong(expectedUserInfo, + (CFDictionaryRef)userInfo, + std::memory_order_acq_rel)) + objc_release(userInfo); + + // We also need to cmpxchg in the domain; if somebody beat us to it, // we need to release. // // Storing the domain must be the LAST THING we do, since it's // the signal that the NSError has been initialized. - CFStringRef expected = nullptr; - if (!errorObject->domain.compare_exchange_strong(expected, + CFStringRef expectedDomain = nullptr; + if (!errorObject->domain.compare_exchange_strong(expectedDomain, (CFStringRef)domain, std::memory_order_acq_rel)) objc_release(domain); diff --git a/test/1_stdlib/ErrorProtocolBridging.swift b/test/1_stdlib/ErrorProtocolBridging.swift index 17df9871733..2f29da115c3 100644 --- a/test/1_stdlib/ErrorProtocolBridging.swift +++ b/test/1_stdlib/ErrorProtocolBridging.swift @@ -314,4 +314,143 @@ ErrorProtocolBridgingTests.test("Thrown NSError identity is preserved") { } } +// Check errors customized via protocol. +struct MyCustomizedError : ErrorProtocol { + let domain: String + let code: Int +} + +extension MyCustomizedError : LocalizedError { + var errorDescription: String? { + return NSLocalizedString("something went horribly wrong", comment: "") + } + + var failureReason: String? { + return NSLocalizedString("because someone wrote 'throw'", comment: "") + } + + var recoverySuggestion: String? { + return NSLocalizedString("delete the 'throw'", comment: "") + } + + var helpAnchor: String? { + return NSLocalizedString("there is no help when writing tests", comment: "") + } +} + +extension MyCustomizedError : CustomNSError { + var errorDomain: String { + return domain + } + + /// The error code within the given domain. + var errorCode: Int { + return code + } + + /// The user-info dictionary. + var errorUserInfo: [String : AnyObject] { + return [ NSURLErrorKey : URL(string: "https://swift.org")! as AnyObject] + } +} + +extension MyCustomizedError : RecoverableError { + var recoveryOptions: [String] { + return ["Delete 'throw'", "Disable the test" ] + } + + func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool { + return recoveryOptionIndex == 0 + } +} + +/// Fake definition of the informal protocol +/// "NSErrorRecoveryAttempting" that we use to poke at the object +/// produced for a RecoverableError. +@objc protocol FakeNSErrorRecoveryAttempting { + @objc(attemptRecoveryFromError:optionIndex:delegate:didRecoverSelector:contextInfo:) + func attemptRecovery(fromError nsError: ErrorProtocol, + optionIndex recoveryOptionIndex: Int, + delegate: AnyObject?, + didRecoverSelector: Selector, + contextInfo: UnsafeMutablePointer?) + + @objc(attemptRecoveryFromError:optionIndex:) + func attemptRecovery(fromError nsError: ErrorProtocol, + optionIndex recoveryOptionIndex: Int) -> Bool +} + +class RecoveryDelegate { + let expectedSuccess: Bool + let expectedContextInfo: UnsafeMutablePointer? + var called = false + + init(expectedSuccess: Bool, + expectedContextInfo: UnsafeMutablePointer?) { + self.expectedSuccess = expectedSuccess + self.expectedContextInfo = expectedContextInfo + } + + @objc func recover(success: Bool, contextInfo: UnsafeMutablePointer?) { + expectEqual(expectedSuccess, success) + expectEqual(expectedContextInfo, contextInfo) + called = true + } +} + +ErrorProtocolBridgingTests.test("Customizing NSError via protocols") { + let error = MyCustomizedError(domain: "custom", code: 12345) + let nsError = error as NSError + + // CustomNSError + expectEqual("custom", nsError.domain) + expectEqual(12345, nsError.code) + expectOptionalEqual(URL(string: "https://swift.org")!, + nsError.userInfo[NSURLErrorKey] as? URL) + + // LocalizedError + expectOptionalEqual("something went horribly wrong", + nsError.userInfo[NSLocalizedDescriptionKey] as? String) + expectOptionalEqual("because someone wrote 'throw'", + nsError.userInfo[NSLocalizedFailureReasonErrorKey] as? String) + expectOptionalEqual("delete the 'throw'", + nsError.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String) + expectOptionalEqual("there is no help when writing tests", + nsError.userInfo[NSHelpAnchorErrorKey] as? String) + + // RecoverableError + expectOptionalEqual(["Delete 'throw'", "Disable the test" ], + nsError.userInfo[NSLocalizedRecoveryOptionsErrorKey] as? [String]) + + // Directly recover. + let attempter = nsError.userInfo[NSRecoveryAttempterErrorKey]! + expectOptionalEqual(attempter.attemptRecovery(fromError: nsError, + optionIndex: 0), + true) + expectOptionalEqual(attempter.attemptRecovery(fromError: nsError, + optionIndex: 1), + false) + + // Recover through delegate. + let rd1 = RecoveryDelegate(expectedSuccess: true, expectedContextInfo: nil) + expectEqual(false, rd1.called) + attempter.attemptRecovery( + fromError: nsError, + optionIndex: 0, + delegate: rd1, + didRecoverSelector: #selector(RecoveryDelegate.recover(success:contextInfo:)), + contextInfo: nil) + expectEqual(true, rd1.called) + + let rd2 = RecoveryDelegate(expectedSuccess: false, expectedContextInfo: nil) + expectEqual(false, rd2.called) + attempter.attemptRecovery( + fromError: nsError, + optionIndex: 1, + delegate: rd2, + didRecoverSelector: #selector(RecoveryDelegate.recover(success:contextInfo:)), + contextInfo: nil) + expectEqual(true, rd2.called) +} + runAllTests()