#[cfg(feature = "exception")]
use core::ffi::c_void;
use core::ffi::CStr;
use core::fmt;
#[cfg(feature = "exception")]
use core::mem;
use core::ops::Deref;
use core::panic::RefUnwindSafe;
use core::panic::UnwindSafe;
#[cfg(feature = "exception")]
use core::ptr;
use std::error::Error;
use crate::encode::{Encoding, RefEncode};
#[cfg(feature = "exception")]
use crate::ffi;
#[cfg(feature = "catch-all")]
use crate::ffi::NSUInteger;
#[cfg(feature = "catch-all")]
use crate::msg_send;
use crate::rc::{autoreleasepool_leaking, Retained};
use crate::runtime::__nsstring::nsstring_to_str;
use crate::runtime::{AnyClass, AnyObject, NSObject, NSObjectProtocol};
use crate::{extern_methods, sel, Message};
#[repr(transparent)]
pub struct Exception(AnyObject);
unsafe impl RefEncode for Exception {
const ENCODING_REF: Encoding = Encoding::Object;
}
unsafe impl Message for Exception {}
impl Deref for Exception {
type Target = AnyObject;
#[inline]
fn deref(&self) -> &AnyObject {
&self.0
}
}
impl AsRef<AnyObject> for Exception {
#[inline]
fn as_ref(&self) -> &AnyObject {
self
}
}
impl Exception {
fn is_nsexception(&self) -> Option<bool> {
if self.class().responds_to(sel!(isKindOfClass:)) {
let obj: *const Exception = self;
let obj = unsafe { obj.cast::<NSObject>().as_ref().unwrap() };
let name = CStr::from_bytes_with_nul(b"NSException\0").unwrap();
Some(obj.isKindOfClass(AnyClass::get(name)?))
} else {
Some(false)
}
}
#[cfg(feature = "catch-all")]
pub(crate) fn stack_trace(&self) -> impl fmt::Display + '_ {
struct Helper<'a>(&'a Exception);
impl fmt::Display for Helper<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(true) = self.0.is_nsexception() {
autoreleasepool_leaking(|pool| {
let call_stack_symbols: Option<Retained<NSObject>> =
unsafe { msg_send![self.0, callStackSymbols] };
if let Some(call_stack_symbols) = call_stack_symbols {
writeln!(f, "stack backtrace:")?;
let count: NSUInteger =
unsafe { msg_send![&call_stack_symbols, count] };
let mut i = 0;
while i < count {
let symbol: Retained<NSObject> =
unsafe { msg_send![&call_stack_symbols, objectAtIndex: i] };
let symbol = unsafe { nsstring_to_str(&symbol, pool) };
writeln!(f, "{symbol}")?;
i += 1;
}
}
Ok(())
})
} else {
Ok(())
}
}
}
Helper(self)
}
}
impl Exception {
extern_methods!(
#[unsafe(method(name))]
#[unsafe(method_family = none)]
unsafe fn name(&self) -> Option<Retained<NSObject>>;
#[unsafe(method(reason))]
#[unsafe(method_family = none)]
unsafe fn reason(&self) -> Option<Retained<NSObject>>;
);
}
impl fmt::Debug for Exception {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "exception ")?;
if let Some(true) = self.is_nsexception() {
autoreleasepool_leaking(|pool| {
let (name, reason) = unsafe { (self.name(), self.reason()) };
let name = name
.as_deref()
.map(|name| unsafe { nsstring_to_str(name, pool) });
let reason = reason
.as_deref()
.map(|reason| unsafe { nsstring_to_str(reason, pool) });
let obj: &AnyObject = self.as_ref();
write!(f, "{obj:?} '{}'", name.unwrap_or_default())?;
if let Some(reason) = reason {
write!(f, " reason: {reason}")?;
} else {
write!(f, " reason: (NULL)")?;
}
Ok(())
})
} else {
write!(f, "{:?}", self.0)
}
}
}
impl fmt::Display for Exception {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
autoreleasepool_leaking(|pool| {
if let Some(true) = self.is_nsexception() {
let reason = unsafe { self.reason() };
if let Some(reason) = &reason {
let reason = unsafe { nsstring_to_str(reason, pool) };
return write!(f, "{reason}");
}
}
write!(f, "unknown exception")
})
}
}
impl Error for Exception {}
impl UnwindSafe for Exception {}
impl RefUnwindSafe for Exception {}
#[inline]
#[cfg(feature = "exception")] pub fn throw(exception: Retained<Exception>) -> ! {
let ptr: *const AnyObject = &exception.0;
let ptr = ptr as *mut AnyObject;
unsafe { ffi::objc_exception_throw(ptr) }
}
#[cfg(feature = "exception")]
fn try_no_ret<F: FnOnce()>(closure: F) -> Result<(), Option<Retained<Exception>>> {
let f = {
extern "C-unwind" fn try_objc_execute_closure<F>(closure: &mut Option<F>)
where
F: FnOnce(),
{
let closure = closure.take().unwrap();
closure();
}
let f: extern "C-unwind" fn(&mut Option<F>) = try_objc_execute_closure;
let f: extern "C-unwind" fn(*mut c_void) = unsafe { mem::transmute(f) };
f
};
let mut closure = Some(closure);
let context: *mut Option<F> = &mut closure;
let context = context.cast();
let mut exception = ptr::null_mut();
let success = unsafe { objc2_exception_helper::try_catch(f, context, &mut exception) };
if success == 0 {
Ok(())
} else {
Err(unsafe { Retained::from_raw(exception.cast()) })
}
}
#[cfg(feature = "exception")]
pub fn catch<R>(
closure: impl FnOnce() -> R + UnwindSafe,
) -> Result<R, Option<Retained<Exception>>> {
let mut value = None;
let value_ref = &mut value;
let closure = move || {
*value_ref = Some(closure());
};
let result = try_no_ret(closure);
result.map(|()| value.unwrap_or_else(|| unreachable!()))
}
#[cfg(test)]
#[cfg(feature = "exception")]
mod tests {
use alloc::format;
use alloc::string::ToString;
use core::panic::AssertUnwindSafe;
use std::panic::catch_unwind;
use super::*;
use crate::msg_send;
#[test]
fn test_catch() {
let mut s = "Hello".to_string();
let result = catch(move || {
s.push_str(", World!");
s
});
assert_eq!(result.unwrap(), "Hello, World!");
}
#[test]
#[cfg_attr(
all(target_os = "macos", target_arch = "x86"),
ignore = "`NULL` exceptions are invalid on 32-bit / w. fragile runtime"
)]
fn test_catch_null() {
let s = "Hello".to_string();
let result = catch(move || {
if !s.is_empty() {
unsafe { ffi::objc_exception_throw(ptr::null_mut()) }
}
s.len()
});
assert!(result.unwrap_err().is_none());
}
#[test]
#[cfg_attr(
feature = "catch-all",
ignore = "Panics inside `catch` when catch-all is enabled"
)]
fn test_catch_unknown_selector() {
let obj = AssertUnwindSafe(NSObject::new());
let ptr = Retained::as_ptr(&obj);
let result = catch(|| {
let _: Retained<NSObject> = unsafe { msg_send![&*obj, copy] };
});
let err = result.unwrap_err().unwrap();
assert_eq!(
format!("{err}"),
format!("-[NSObject copyWithZone:]: unrecognized selector sent to instance {ptr:?}"),
);
}
#[test]
fn test_throw_catch_object() {
let obj = NSObject::new();
let _obj2 = obj.clone();
let obj: Retained<Exception> = unsafe { Retained::cast_unchecked(obj) };
let ptr: *const Exception = &*obj;
let result = catch(|| throw(obj));
let obj = result.unwrap_err().unwrap();
assert_eq!(format!("{obj:?}"), format!("exception <NSObject: {ptr:p}>"));
assert!(ptr::eq(&*obj, ptr));
}
#[test]
#[ignore = "currently aborts"]
fn throw_catch_unwind() {
let obj = NSObject::new();
let obj: Retained<Exception> = unsafe { Retained::cast_unchecked(obj) };
let result = catch_unwind(|| throw(obj));
let _ = result.unwrap_err();
}
#[test]
#[should_panic = "test"]
#[cfg_attr(
all(target_os = "macos", target_arch = "x86", panic = "unwind"),
ignore = "panic won't start on 32-bit / w. fragile runtime, it'll just abort, since the runtime uses setjmp/longjump unwinding"
)]
fn does_not_catch_panic() {
let _ = catch(|| panic!("test"));
}
}