objc2/exception.rs
1//! # `@throw` and `@try/@catch` exceptions.
2//!
3//! By default, if a message send (such as those generated with the
4//! [`msg_send!`] and [`extern_methods!`] macros) causes an exception to be
5//! thrown, `objc2` will simply let it unwind into Rust.
6//!
7//! While not UB, it will likely end up aborting the process, since Rust
8//! cannot catch foreign exceptions like Objective-C's. However, `objc2` has
9//! the `"catch-all"` Cargo feature, which, when enabled, wraps each message
10//! send in a `@catch` and instead panics if an exception is caught, which
11//! might lead to slightly better error messages.
12//!
13//! Most of the functionality in this module is only available when the
14//! `"exception"` feature is enabled.
15//!
16//! See the following links for more information:
17//! - [Exception Programming Topics for Cocoa](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Exceptions/Exceptions.html)
18//! - [The Objective-C Programming Language - Exception Handling](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocExceptionHandling.html)
19//! - [Exception Handling in LLVM](https://llvm.org/docs/ExceptionHandling.html)
20//!
21//! [`msg_send!`]: crate::msg_send
22
23// TODO: Test this with panic=abort, and ensure that the code-size is
24// reasonable in that case.
25
26#[cfg(feature = "exception")]
27use core::ffi::c_void;
28use core::ffi::CStr;
29use core::fmt;
30#[cfg(feature = "exception")]
31use core::mem;
32use core::ops::Deref;
33use core::panic::RefUnwindSafe;
34use core::panic::UnwindSafe;
35#[cfg(feature = "exception")]
36use core::ptr;
37use std::error::Error;
38
39use crate::encode::{Encoding, RefEncode};
40#[cfg(feature = "exception")]
41use crate::ffi;
42#[cfg(feature = "catch-all")]
43use crate::ffi::NSUInteger;
44#[cfg(feature = "catch-all")]
45use crate::msg_send;
46use crate::rc::{autoreleasepool_leaking, Retained};
47use crate::runtime::__nsstring::nsstring_to_str;
48use crate::runtime::{AnyClass, AnyObject, NSObject, NSObjectProtocol};
49use crate::{extern_methods, sel, Message};
50
51/// An Objective-C exception.
52///
53/// While highly recommended that any exceptions you intend to throw are
54/// subclasses of `NSException`, this is not required by the runtime (similar
55/// to how Rust can panic with arbitrary payloads using [`panic_any`]).
56///
57/// [`panic_any`]: std::panic::panic_any
58#[repr(transparent)]
59pub struct Exception(AnyObject);
60
61unsafe impl RefEncode for Exception {
62 const ENCODING_REF: Encoding = Encoding::Object;
63}
64
65unsafe impl Message for Exception {}
66
67impl Deref for Exception {
68 type Target = AnyObject;
69
70 #[inline]
71 fn deref(&self) -> &AnyObject {
72 &self.0
73 }
74}
75
76impl AsRef<AnyObject> for Exception {
77 #[inline]
78 fn as_ref(&self) -> &AnyObject {
79 self
80 }
81}
82
83impl Exception {
84 fn is_nsexception(&self) -> Option<bool> {
85 if self.class().responds_to(sel!(isKindOfClass:)) {
86 // SAFETY: We only use `isKindOfClass:` on NSObject
87 let obj: *const Exception = self;
88 let obj = unsafe { obj.cast::<NSObject>().as_ref().unwrap() };
89 // Get class dynamically instead of with `class!` macro
90 let name = CStr::from_bytes_with_nul(b"NSException\0").unwrap();
91 Some(obj.isKindOfClass(AnyClass::get(name)?))
92 } else {
93 Some(false)
94 }
95 }
96
97 #[cfg(feature = "catch-all")]
98 pub(crate) fn stack_trace(&self) -> impl fmt::Display + '_ {
99 struct Helper<'a>(&'a Exception);
100
101 impl fmt::Display for Helper<'_> {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 if let Some(true) = self.0.is_nsexception() {
104 autoreleasepool_leaking(|pool| {
105 // SAFETY: The object is an `NSException`.
106 // Returns `NSArray<NSString *>`.
107 let call_stack_symbols: Option<Retained<NSObject>> =
108 unsafe { msg_send![self.0, callStackSymbols] };
109 if let Some(call_stack_symbols) = call_stack_symbols {
110 writeln!(f, "stack backtrace:")?;
111
112 // SAFETY: `call_stack_symbols` is an `NSArray`, and
113 // `count` returns `NSUInteger`.
114 let count: NSUInteger =
115 unsafe { msg_send![&call_stack_symbols, count] };
116 let mut i = 0;
117 while i < count {
118 // SAFETY: The index is in-bounds (so no exception will be thrown).
119 let symbol: Retained<NSObject> =
120 unsafe { msg_send![&call_stack_symbols, objectAtIndex: i] };
121 // SAFETY: The symbol is an NSString, and is not used
122 // beyond this scope.
123 let symbol = unsafe { nsstring_to_str(&symbol, pool) };
124 writeln!(f, "{symbol}")?;
125 i += 1;
126 }
127 }
128 Ok(())
129 })
130 } else {
131 Ok(())
132 }
133 }
134 }
135
136 Helper(self)
137 }
138}
139
140impl Exception {
141 extern_methods!(
142 // Only safe on NSException
143 // Returns NSString
144 #[unsafe(method(name))]
145 #[unsafe(method_family = none)]
146 unsafe fn name(&self) -> Option<Retained<NSObject>>;
147
148 // Only safe on NSException
149 // Returns NSString
150 #[unsafe(method(reason))]
151 #[unsafe(method_family = none)]
152 unsafe fn reason(&self) -> Option<Retained<NSObject>>;
153 );
154}
155
156// Note: We can't implement `Send` nor `Sync` since the exception could be
157// anything!
158
159impl fmt::Debug for Exception {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 write!(f, "exception ")?;
162
163 // Attempt to present a somewhat usable error message if the exception
164 // is an instance of NSException.
165 if let Some(true) = self.is_nsexception() {
166 autoreleasepool_leaking(|pool| {
167 // SAFETY: Just checked that object is an NSException
168 let (name, reason) = unsafe { (self.name(), self.reason()) };
169
170 // SAFETY:
171 // - `name` and `reason` are guaranteed to be `NSString`s.
172 // - We control the scope in which they are alive, so we know
173 // they are not moved outside the current autorelease pool.
174 //
175 // Note that these strings are immutable (`NSException` is
176 // immutable, and the properties are marked as `readonly` and
177 // `copy` and are copied upon creation), so we also don't have
178 // to worry about the string being mutated under our feet.
179 let name = name
180 .as_deref()
181 .map(|name| unsafe { nsstring_to_str(name, pool) });
182 let reason = reason
183 .as_deref()
184 .map(|reason| unsafe { nsstring_to_str(reason, pool) });
185
186 let obj: &AnyObject = self.as_ref();
187 write!(f, "{obj:?} '{}'", name.unwrap_or_default())?;
188 if let Some(reason) = reason {
189 write!(f, " reason: {reason}")?;
190 } else {
191 write!(f, " reason: (NULL)")?;
192 }
193 Ok(())
194 })
195 } else {
196 // Fall back to `AnyObject` Debug
197 write!(f, "{:?}", self.0)
198 }
199 }
200}
201
202impl fmt::Display for Exception {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 autoreleasepool_leaking(|pool| {
205 if let Some(true) = self.is_nsexception() {
206 // SAFETY: Just checked that object is an NSException
207 let reason = unsafe { self.reason() };
208
209 if let Some(reason) = &reason {
210 // SAFETY: Same as above in `Debug`.
211 let reason = unsafe { nsstring_to_str(reason, pool) };
212 return write!(f, "{reason}");
213 }
214 }
215
216 write!(f, "unknown exception")
217 })
218 }
219}
220
221impl Error for Exception {}
222
223impl UnwindSafe for Exception {}
224impl RefUnwindSafe for Exception {}
225
226/// Throws an Objective-C exception.
227///
228/// This is the Objective-C equivalent of Rust's [`panic!`].
229///
230/// This unwinds from Objective-C, and the exception should be caught using an
231/// Objective-C exception handler like [`catch`]. It _may_ be caught by
232/// [`catch_unwind`], though the error message is unlikely to be great.
233///
234/// [`catch_unwind`]: std::panic::catch_unwind
235#[inline]
236#[cfg(feature = "exception")] // For consistency, not strictly required
237pub fn throw(exception: Retained<Exception>) -> ! {
238 // We consume the exception object since we can't make any guarantees
239 // about its mutability.
240 let ptr: *const AnyObject = &exception.0;
241 let ptr = ptr as *mut AnyObject;
242 // SAFETY: The object is valid and non-null (nil exceptions are not valid
243 // in the old runtime).
244 unsafe { ffi::objc_exception_throw(ptr) }
245}
246
247#[cfg(feature = "exception")]
248fn try_no_ret<F: FnOnce()>(closure: F) -> Result<(), Option<Retained<Exception>>> {
249 let f = {
250 extern "C-unwind" fn try_objc_execute_closure<F>(closure: &mut Option<F>)
251 where
252 F: FnOnce(),
253 {
254 // This is always passed Some, so it's safe to unwrap
255 let closure = closure.take().unwrap();
256 closure();
257 }
258
259 let f: extern "C-unwind" fn(&mut Option<F>) = try_objc_execute_closure;
260 let f: extern "C-unwind" fn(*mut c_void) = unsafe { mem::transmute(f) };
261 f
262 };
263
264 // Wrap the closure in an Option so it can be taken
265 let mut closure = Some(closure);
266 let context: *mut Option<F> = &mut closure;
267 let context = context.cast();
268
269 let mut exception = ptr::null_mut();
270 // SAFETY: The function pointer and context are valid.
271 //
272 // The exception catching itself is sound on the Rust side, because we
273 // correctly use `extern "C-unwind"`. Objective-C does not completely
274 // specify how foreign unwinds are handled, though they do have the
275 // `@catch (...)` construct intended for catching C++ exceptions, so it is
276 // likely that they intend to support Rust panics (and it works in
277 // practice).
278 //
279 // See also:
280 // https://github.com/rust-lang/rust/pull/128321
281 // https://github.com/rust-lang/reference/pull/1226
282 let success = unsafe { objc2_exception_helper::try_catch(f, context, &mut exception) };
283
284 if success == 0 {
285 Ok(())
286 } else {
287 // SAFETY:
288 // The exception is always a valid object or NULL.
289 //
290 // Since we do a retain in `objc2_exception_helper/src/try_catch.m`,
291 // the object has +1 retain count.
292 Err(unsafe { Retained::from_raw(exception.cast()) })
293 }
294}
295
296/// Tries to execute the given closure and catches an Objective-C exception
297/// if one is thrown.
298///
299/// This is the Objective-C equivalent of Rust's [`catch_unwind`].
300/// Accordingly, if your Rust code is compiled with `panic=abort`, or your
301/// Objective-C code with `-fno-objc-exceptions`, this cannot catch the
302/// exception.
303///
304/// [`catch_unwind`]: std::panic::catch_unwind
305///
306///
307/// # Errors
308///
309/// Returns a `Result` that is either `Ok` if the closure succeeded without an
310/// exception being thrown, or an `Err` with the exception. The exception is
311/// automatically released.
312///
313/// The exception is `None` in the extremely exceptional case that the
314/// exception object is `nil`. This should basically never happen, but is
315/// technically possible on some systems with `@throw nil`, or in OOM
316/// situations.
317///
318///
319/// # Panics
320///
321/// This panics if the given closure panics.
322///
323/// That is, it completely ignores Rust unwinding and simply lets that pass
324/// through unchanged.
325///
326/// It may also not catch all Objective-C exceptions (such as exceptions
327/// thrown when handling the memory management of the exception). These are
328/// mostly theoretical, and should only happen in utmost exceptional cases.
329#[cfg(feature = "exception")]
330pub fn catch<R>(
331 closure: impl FnOnce() -> R + UnwindSafe,
332) -> Result<R, Option<Retained<Exception>>> {
333 let mut value = None;
334 let value_ref = &mut value;
335 let closure = move || {
336 *value_ref = Some(closure());
337 };
338 let result = try_no_ret(closure);
339 // If the try succeeded, value was set so it's safe to unwrap
340 result.map(|()| value.unwrap_or_else(|| unreachable!()))
341}
342
343#[cfg(test)]
344#[cfg(feature = "exception")]
345mod tests {
346 use alloc::format;
347 use alloc::string::ToString;
348 use core::panic::AssertUnwindSafe;
349 use std::panic::catch_unwind;
350
351 use super::*;
352 use crate::msg_send;
353
354 #[test]
355 fn test_catch() {
356 let mut s = "Hello".to_string();
357 let result = catch(move || {
358 s.push_str(", World!");
359 s
360 });
361 assert_eq!(result.unwrap(), "Hello, World!");
362 }
363
364 #[test]
365 #[cfg_attr(
366 all(target_os = "macos", target_arch = "x86"),
367 ignore = "`NULL` exceptions are invalid on 32-bit / w. fragile runtime"
368 )]
369 fn test_catch_null() {
370 let s = "Hello".to_string();
371 let result = catch(move || {
372 if !s.is_empty() {
373 unsafe { ffi::objc_exception_throw(ptr::null_mut()) }
374 }
375 s.len()
376 });
377 assert!(result.unwrap_err().is_none());
378 }
379
380 #[test]
381 #[cfg_attr(
382 feature = "catch-all",
383 ignore = "Panics inside `catch` when catch-all is enabled"
384 )]
385 fn test_catch_unknown_selector() {
386 let obj = AssertUnwindSafe(NSObject::new());
387 let ptr = Retained::as_ptr(&obj);
388 let result = catch(|| {
389 let _: Retained<NSObject> = unsafe { msg_send![&*obj, copy] };
390 });
391 let err = result.unwrap_err().unwrap();
392
393 assert_eq!(
394 format!("{err}"),
395 format!("-[NSObject copyWithZone:]: unrecognized selector sent to instance {ptr:?}"),
396 );
397 }
398
399 #[test]
400 fn test_throw_catch_object() {
401 let obj = NSObject::new();
402 // TODO: Investigate why this is required on GNUStep!
403 let _obj2 = obj.clone();
404 let obj: Retained<Exception> = unsafe { Retained::cast_unchecked(obj) };
405 let ptr: *const Exception = &*obj;
406
407 let result = catch(|| throw(obj));
408 let obj = result.unwrap_err().unwrap();
409
410 assert_eq!(format!("{obj:?}"), format!("exception <NSObject: {ptr:p}>"));
411
412 assert!(ptr::eq(&*obj, ptr));
413 }
414
415 #[test]
416 #[ignore = "currently aborts"]
417 fn throw_catch_unwind() {
418 let obj = NSObject::new();
419 let obj: Retained<Exception> = unsafe { Retained::cast_unchecked(obj) };
420
421 let result = catch_unwind(|| throw(obj));
422 let _ = result.unwrap_err();
423 }
424
425 #[test]
426 #[should_panic = "test"]
427 #[cfg_attr(
428 all(target_os = "macos", target_arch = "x86", panic = "unwind"),
429 ignore = "panic won't start on 32-bit / w. fragile runtime, it'll just abort, since the runtime uses setjmp/longjump unwinding"
430 )]
431 fn does_not_catch_panic() {
432 let _ = catch(|| panic!("test"));
433 }
434}