Skip to content

Commit b0f9317

Browse files
committed
[SE-0112] Add error protocols LocalizedError, RecoverableError, CustomNSError
An error type can conform to one or more of these new protocols to customize its behavior and representation. From an implementation standpoint, the protocol conformances are used to fill in the user-info dictionary in NSError to interoperate with the Cocoa error-handling system. There are a few outstanding problems with this implementation, although it is fully functional: * Population of the userInfo dictionary is currently eager; we should use user info providers on platforms where they are available. * At present, the Swift dynamic casting machinery is unable to unbox a _SwiftNativeNSError when trying to cast from it to (e.g.) an existential, which makes it impossible to retrieve the RecoverableError from the NSError. Instead, just capture the original error---hey, they're supposed to be value types anyway!---and use that to implement the entry points for the informal NSErrorRecoveryAttempting protocol. This is part (1) of the proposal solution.
1 parent abf73d3 commit b0f9317

File tree

5 files changed

+396
-13
lines changed

5 files changed

+396
-13
lines changed

stdlib/public/SDK/Foundation/NSError.swift

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,185 @@
1313
import CoreFoundation
1414
import Darwin
1515

16+
/// Describes an error that provides localized messages describing why
17+
/// an error occurred and provides more information about the error.
18+
public protocol LocalizedError : ErrorProtocol {
19+
/// A localized message describing what error occurred.
20+
var errorDescription: String? { get }
21+
22+
/// A localized message describing the reason for the failure.
23+
var failureReason: String? { get }
24+
25+
/// A localized message describing how one might recover from the failure.
26+
var recoverySuggestion: String? { get }
27+
28+
/// A localized message providing "help" text if the user requests help.
29+
var helpAnchor: String? { get }
30+
}
31+
32+
public extension LocalizedError {
33+
var errorDescription: String? { return nil }
34+
var failureReason: String? { return nil }
35+
var recoverySuggestion: String? { return nil }
36+
var helpAnchor: String? { return nil }
37+
}
38+
39+
@_silgen_name("NS_Swift_performErrorRecoverySelector")
40+
internal func NS_Swift_performErrorRecoverySelector(
41+
delegate: AnyObject?,
42+
selector: Selector,
43+
success: ObjCBool,
44+
contextInfo: UnsafeMutablePointer<Void>?)
45+
46+
/// Class that implements the informal protocol
47+
/// NSErrorRecoveryAttempting, which is used by NSError when it
48+
/// attempts recovery from an error.
49+
class _NSErrorRecoveryAttempter {
50+
// FIXME: If we could meaningfully cast the nsError back to RecoverableError,
51+
// we wouldn't need to capture this and could use the user-info
52+
// domain providers even for recoverable errors.
53+
let error: RecoverableError
54+
55+
init(error: RecoverableError) {
56+
self.error = error
57+
}
58+
59+
@objc(attemptRecoveryFromError:optionIndex:delegate:didRecoverSelector:contextInfo:)
60+
func attemptRecovery(fromError nsError: NSError,
61+
optionIndex recoveryOptionIndex: Int,
62+
delegate: AnyObject?,
63+
didRecoverSelector: Selector,
64+
contextInfo: UnsafeMutablePointer<Void>?) {
65+
error.attemptRecovery(optionIndex: recoveryOptionIndex) { success in
66+
NS_Swift_performErrorRecoverySelector(
67+
delegate: delegate,
68+
selector: didRecoverSelector,
69+
success: ObjCBool(success),
70+
contextInfo: contextInfo)
71+
}
72+
}
73+
74+
@objc(attemptRecoveryFromError:optionIndex:)
75+
func attemptRecovery(fromError nsError: NSError,
76+
optionIndex recoveryOptionIndex: Int) -> Bool {
77+
return error.attemptRecovery(optionIndex: recoveryOptionIndex)
78+
}
79+
}
80+
81+
/// Describes an error that may be recoverably by presenting several
82+
/// potential recovery options to the user.
83+
public protocol RecoverableError : ErrorProtocol {
84+
/// Provides a set of possible recovery options to present to the user.
85+
var recoveryOptions: [String] { get }
86+
87+
/// Attempt to recover from this error when the user selected the
88+
/// option at the given index. This routine must call resultHandler and
89+
/// indicate whether recovery was successful (or not).
90+
///
91+
/// This entry point is used for recovery of errors handled at a
92+
/// "document" granularity, that do not affect the entire
93+
/// application.
94+
func attemptRecovery(optionIndex recoveryOptionIndex: Int,
95+
andThen resultHandler: (recovered: Bool) -> Void)
96+
97+
/// Attempt to recover from this error when the user selected the
98+
/// option at the given index. Returns true to indicate
99+
/// successful recovery, and false otherwise.
100+
///
101+
/// This entry point is used for recovery of errors handled at
102+
/// the "application" granularity, where nothing else in the
103+
/// application can proceed until the attmpted error recovery
104+
/// completes.
105+
func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool
106+
}
107+
108+
public extension RecoverableError {
109+
/// By default, implements document-modal recovery via application-model
110+
/// recovery.
111+
func attemptRecovery(optionIndex recoveryOptionIndex: Int,
112+
andThen resultHandler: (recovered: Bool) -> Void) {
113+
resultHandler(recovered: attemptRecovery(optionIndex: recoveryOptionIndex))
114+
}
115+
}
116+
117+
/// Describes an error type that specifically provides a domain, code,
118+
/// and user-info dictionary.
119+
public protocol CustomNSError : ErrorProtocol {
120+
/// The domain of the error.
121+
var errorDomain: String { get }
122+
123+
/// The error code within the given domain.
124+
var errorCode: Int { get }
125+
126+
/// The user-info dictionary.
127+
var errorUserInfo: [String : AnyObject] { get }
128+
}
129+
130+
public extension ErrorProtocol where Self : CustomNSError {
131+
/// Default implementation for customized NSErrors.
132+
var _domain: String { return self.errorDomain }
133+
134+
/// Default implementation for customized NSErrors.
135+
var _code: Int { return self.errorCode }
136+
}
137+
138+
/// Retrieve the default userInfo dictionary for a given error.
139+
@_silgen_name("swift_Foundation_getErrorDefaultUserInfo")
140+
public func _swift_Foundation_getErrorDefaultUserInfo(_ error: ErrorProtocol)
141+
-> AnyObject? {
142+
// If the OS supports value user info value providers, use those
143+
// to lazily populate the user-info dictionary for this domain.
144+
if #available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *) {
145+
// FIXME: This is not implementable until we can recover the
146+
// original error from an NSError.
147+
}
148+
149+
// Populate the user-info dictionary
150+
var result: [String : AnyObject]
151+
152+
// Initialize with custom user-info.
153+
if let customNSError = error as? CustomNSError {
154+
result = customNSError.errorUserInfo
155+
} else {
156+
result = [:]
157+
}
158+
159+
if let localizedError = error as? LocalizedError {
160+
if let description = localizedError.errorDescription {
161+
result[NSLocalizedDescriptionKey] = description as AnyObject
162+
}
163+
164+
if let reason = localizedError.failureReason {
165+
result[NSLocalizedFailureReasonErrorKey] = reason as AnyObject
166+
}
167+
168+
if let suggestion = localizedError.recoverySuggestion {
169+
result[NSLocalizedRecoverySuggestionErrorKey] = suggestion as AnyObject
170+
}
171+
172+
if let helpAnchor = localizedError.helpAnchor {
173+
result[NSHelpAnchorErrorKey] = helpAnchor as AnyObject
174+
}
175+
}
176+
177+
if let recoverableError = error as? RecoverableError {
178+
result[NSLocalizedRecoveryOptionsErrorKey] =
179+
recoverableError.recoveryOptions as AnyObject
180+
result[NSRecoveryAttempterErrorKey] =
181+
_NSErrorRecoveryAttempter(error: recoverableError)
182+
}
183+
184+
return result as AnyObject
185+
}
186+
16187
// NSError and CFError conform to the standard ErrorProtocol protocol. Compiler
17188
// magic allows this to be done as a "toll-free" conversion when an NSError
18189
// or CFError is used as an ErrorProtocol existential.
19190

20191
extension NSError : ErrorProtocol {
21192
public var _domain: String { return domain }
22193
public var _code: Int { return code }
194+
public var _userInfo: AnyObject? { return userInfo as AnyObject }
23195
}
24196

25197
extension CFError : ErrorProtocol {
@@ -30,6 +202,10 @@ extension CFError : ErrorProtocol {
30202
public var _code: Int {
31203
return CFErrorGetCode(self)
32204
}
205+
206+
public var _userInfo: AnyObject? {
207+
return CFErrorCopyUserInfo(self) as AnyObject?
208+
}
33209
}
34210

35211
// An error value to use when an Objective-C API indicates error

stdlib/public/SDK/Foundation/Thunks.mm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
#import <Foundation/Foundation.h>
14+
#include <objc/message.h>
1415

1516
#include "swift/Runtime/Config.h"
1617

@@ -118,3 +119,12 @@
118119
return [result retain];
119120
}
120121

122+
// -- NSError
123+
SWIFT_CC(swift)
124+
extern "C" void
125+
NS_Swift_performErrorRecoverySelector(_Nullable id delegate,
126+
SEL selector,
127+
BOOL success,
128+
void * _Nullable contextInfo) {
129+
objc_msgSend(delegate, selector, success, contextInfo);
130+
}

stdlib/public/core/ErrorType.swift

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
1010
//
1111
//===----------------------------------------------------------------------===//
12+
import SwiftShims
1213

1314
// TODO: API review
1415
/// A type representing an error value that can be thrown.
@@ -111,17 +112,12 @@
111112
public protocol ErrorProtocol {
112113
var _domain: String { get }
113114
var _code: Int { get }
114-
}
115-
116-
extension ErrorProtocol {
117-
public var _domain: String {
118-
return String(reflecting: self.dynamicType)
119-
}
115+
var _userInfo: AnyObject? { get }
120116
}
121117

122118
#if _runtime(_ObjC)
123-
// Helper functions for the C++ runtime to have easy access to domain and
124-
// code as Objective-C values.
119+
// Helper functions for the C++ runtime to have easy access to domain,
120+
// code, and userInfo as Objective-C values.
125121
@_silgen_name("swift_stdlib_getErrorDomainNSString")
126122
public func _stdlib_getErrorDomainNSString<T : ErrorProtocol>(_ x: UnsafePointer<T>)
127123
-> AnyObject {
@@ -133,6 +129,17 @@ public func _stdlib_getErrorCode<T : ErrorProtocol>(_ x: UnsafePointer<T>) -> In
133129
return x.pointee._code
134130
}
135131

132+
// Helper functions for the C++ runtime to have easy access to domain and
133+
// code as Objective-C values.
134+
@_silgen_name("swift_stdlib_getErrorUserInfoNSDictionary")
135+
public func _stdlib_getErrorUserInfoNSDictionary<T : ErrorProtocol>(_ x: UnsafePointer<T>)
136+
-> AnyObject? {
137+
return x.pointee._userInfo
138+
}
139+
140+
@_silgen_name("swift_stdlib_getErrorDefaultUserInfo")
141+
public func _stdlib_getErrorDefaultUserInfo(_ error: ErrorProtocol) -> AnyObject?
142+
136143
// Known function for the compiler to use to coerce `ErrorProtocol` instances
137144
// to `NSError`.
138145
@_silgen_name("swift_bridgeErrorProtocolToNSError")
@@ -154,3 +161,17 @@ public func _errorInMain(_ error: ErrorProtocol) {
154161

155162
@available(*, unavailable, renamed: "ErrorProtocol")
156163
public typealias ErrorType = ErrorProtocol
164+
165+
extension ErrorProtocol {
166+
public var _domain: String {
167+
return String(reflecting: self.dynamicType)
168+
}
169+
170+
public var _userInfo: AnyObject? {
171+
#if _runtime(_ObjC)
172+
return _stdlib_getErrorDefaultUserInfo(self)
173+
#else
174+
return nil
175+
#endif
176+
}
177+
}

stdlib/public/runtime/ErrorObject.mm

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,34 @@ static Class getSwiftNativeNSErrorClass() {
285285
const Metadata *T,
286286
const WitnessTable *ErrorProtocol);
287287

288+
//@_silgen_name("swift_stdlib_getErrorUserInfoNSDictionary")
289+
//public func _stdlib_getErrorUserInfoNSDictionary<T : ErrorProtocol>(_ x: UnsafePointer<T>) -> AnyObject
290+
SWIFT_CC(swift)
291+
extern "C" NSDictionary *swift_stdlib_getErrorUserInfoNSDictionary(
292+
const OpaqueValue *error,
293+
const Metadata *T,
294+
const WitnessTable *ErrorProtocol);
295+
296+
//@_silgen_name("swift_stdlib_getErrorDefaultUserInfo")
297+
//public func _stdlib_getErrorDefaultUserInfo<T : ErrorProtocol>(_ x: UnsafePointer<T>) -> AnyObject
298+
SWIFT_CC(swift) SWIFT_RT_ENTRY_VISIBILITY
299+
extern "C" NSDictionary *swift_stdlib_getErrorDefaultUserInfo(
300+
const OpaqueValue *error,
301+
const Metadata *T,
302+
const WitnessTable *ErrorProtocol) {
303+
typedef SWIFT_CC(swift)
304+
NSDictionary *GetDefaultFn(const OpaqueValue *error,
305+
const Metadata *T,
306+
const WitnessTable *ErrorProtocol);
307+
308+
auto foundationGetDefaultUserInfo = SWIFT_LAZY_CONSTANT(
309+
reinterpret_cast<GetDefaultFn*>
310+
(dlsym(RTLD_DEFAULT, "swift_Foundation_getErrorDefaultUserInfo")));
311+
if (!foundationGetDefaultUserInfo) { return nullptr; }
312+
313+
return foundationGetDefaultUserInfo(error, T, ErrorProtocol);
314+
}
315+
288316
/// Take an ErrorProtocol box and turn it into a valid NSError instance.
289317
SWIFT_CC(swift)
290318
static id _swift_bridgeErrorProtocolToNSError_(SwiftError *errorObject) {
@@ -302,21 +330,30 @@ static id _swift_bridgeErrorProtocolToNSError_(SwiftError *errorObject) {
302330

303331
NSString *domain = swift_stdlib_getErrorDomainNSString(value, type, witness);
304332
NSInteger code = swift_stdlib_getErrorCode(value, type, witness);
305-
// TODO: user info?
306-
333+
NSDictionary *userInfo =
334+
swift_stdlib_getErrorUserInfoNSDictionary(value, type, witness);
335+
307336
// The error code shouldn't change, so we can store it blindly, even if
308337
// somebody beat us to it. The store can be relaxed, since we'll do a
309338
// store(release) of the domain last thing to publish the initialized
310339
// NSError.
311340
errorObject->code.store(code, std::memory_order_relaxed);
312341

313-
// However, we need to cmpxchg in the domain; if somebody beat us to it,
342+
// However, we need to cmpxchg the userInfo; if somebody beat us to it,
343+
// we need to release.
344+
CFDictionaryRef expectedUserInfo = nullptr;
345+
if (!errorObject->userInfo.compare_exchange_strong(expectedUserInfo,
346+
(CFDictionaryRef)userInfo,
347+
std::memory_order_acq_rel))
348+
objc_release(userInfo);
349+
350+
// We also need to cmpxchg in the domain; if somebody beat us to it,
314351
// we need to release.
315352
//
316353
// Storing the domain must be the LAST THING we do, since it's
317354
// the signal that the NSError has been initialized.
318-
CFStringRef expected = nullptr;
319-
if (!errorObject->domain.compare_exchange_strong(expected,
355+
CFStringRef expectedDomain = nullptr;
356+
if (!errorObject->domain.compare_exchange_strong(expectedDomain,
320357
(CFStringRef)domain,
321358
std::memory_order_acq_rel))
322359
objc_release(domain);

0 commit comments

Comments
 (0)