Skip to content

Commit 76567af

Browse files
committed
Make exception catching safe
1 parent 01088d6 commit 76567af

File tree

4 files changed

+48
-50
lines changed

4 files changed

+48
-50
lines changed

crates/objc2/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
9292
accept nullable function pointers.
9393
* **BREAKING**: Changed the signature of various `ffi` functions to use the
9494
proper `Bool` type instead of a typedef.
95+
* Made `exception::catch` safe.
9596

9697
### Deprecated
9798
* Merged and deprecated the following `ffi` types:
@@ -160,6 +161,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
160161
- `AnyClass::instance_variable`.
161162
- `AnyProtocol::get`.
162163
- `AnyProtocol::name`.
164+
* Clarified that `exception::catch` does not catch Rust panics.
163165

164166

165167
## 0.5.2 - 2024-05-21

crates/objc2/src/exception.rs

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ pub fn throw(exception: Retained<Exception>) -> ! {
239239
}
240240

241241
#[cfg(feature = "exception")]
242-
unsafe fn try_no_ret<F: FnOnce()>(closure: F) -> Result<(), Option<Retained<Exception>>> {
242+
fn try_no_ret<F: FnOnce()>(closure: F) -> Result<(), Option<Retained<Exception>>> {
243243
let f = {
244244
extern "C-unwind" fn try_objc_execute_closure<F>(closure: &mut Option<F>)
245245
where
@@ -291,8 +291,9 @@ unsafe fn try_no_ret<F: FnOnce()>(closure: F) -> Result<(), Option<Retained<Exce
291291
/// if one is thrown.
292292
///
293293
/// This is the Objective-C equivalent of Rust's [`catch_unwind`].
294-
/// Accordingly, if your Rust code is compiled with `panic=abort` this cannot
295-
/// catch the exception.
294+
/// Accordingly, if your Rust code is compiled with `panic=abort`, or your
295+
/// Objective-C code with `-fno-objc-exceptions`, this cannot catch the
296+
/// exception.
296297
///
297298
/// [`catch_unwind`]: std::panic::catch_unwind
298299
///
@@ -309,20 +310,26 @@ unsafe fn try_no_ret<F: FnOnce()>(closure: F) -> Result<(), Option<Retained<Exce
309310
/// situations.
310311
///
311312
///
312-
/// # Safety
313+
/// # Panics
313314
///
314-
/// The given closure must not panic (e.g. normal Rust unwinding into this
315-
/// causes undefined behaviour).
315+
/// This panics if the given closure panics.
316+
///
317+
/// That is, it completely ignores Rust unwinding and simply lets that pass
318+
/// through unchanged.
319+
///
320+
/// It may also not catch all Objective-C exceptions (such as exceptions
321+
/// thrown when handling the memory management of the exception). These are
322+
/// mostly theoretical, and should only happen in utmost exceptional cases.
316323
#[cfg(feature = "exception")]
317-
pub unsafe fn catch<R>(
324+
pub fn catch<R>(
318325
closure: impl FnOnce() -> R + UnwindSafe,
319326
) -> Result<R, Option<Retained<Exception>>> {
320327
let mut value = None;
321328
let value_ref = &mut value;
322329
let closure = move || {
323330
*value_ref = Some(closure());
324331
};
325-
let result = unsafe { try_no_ret(closure) };
332+
let result = try_no_ret(closure);
326333
// If the try succeeded, value was set so it's safe to unwrap
327334
result.map(|()| value.unwrap_or_else(|| unreachable!()))
328335
}
@@ -341,12 +348,10 @@ mod tests {
341348
#[test]
342349
fn test_catch() {
343350
let mut s = "Hello".to_string();
344-
let result = unsafe {
345-
catch(move || {
346-
s.push_str(", World!");
347-
s
348-
})
349-
};
351+
let result = catch(move || {
352+
s.push_str(", World!");
353+
s
354+
});
350355
assert_eq!(result.unwrap(), "Hello, World!");
351356
}
352357

@@ -357,14 +362,12 @@ mod tests {
357362
)]
358363
fn test_catch_null() {
359364
let s = "Hello".to_string();
360-
let result = unsafe {
361-
catch(move || {
362-
if !s.is_empty() {
363-
ffi::objc_exception_throw(ptr::null_mut())
364-
}
365-
s.len()
366-
})
367-
};
365+
let result = catch(move || {
366+
if !s.is_empty() {
367+
unsafe { ffi::objc_exception_throw(ptr::null_mut()) }
368+
}
369+
s.len()
370+
});
368371
assert!(result.unwrap_err().is_none());
369372
}
370373

@@ -376,11 +379,9 @@ mod tests {
376379
fn test_catch_unknown_selector() {
377380
let obj = AssertUnwindSafe(NSObject::new());
378381
let ptr = Retained::as_ptr(&obj);
379-
let result = unsafe {
380-
catch(|| {
381-
let _: Retained<NSObject> = msg_send_id![&*obj, copy];
382-
})
383-
};
382+
let result = catch(|| {
383+
let _: Retained<NSObject> = unsafe { msg_send_id![&*obj, copy] };
384+
});
384385
let err = result.unwrap_err().unwrap();
385386

386387
assert_eq!(
@@ -397,7 +398,7 @@ mod tests {
397398
let obj: Retained<Exception> = unsafe { Retained::cast_unchecked(obj) };
398399
let ptr: *const Exception = &*obj;
399400

400-
let result = unsafe { catch(|| throw(obj)) };
401+
let result = catch(|| throw(obj));
401402
let obj = result.unwrap_err().unwrap();
402403

403404
assert_eq!(format!("{obj:?}"), format!("exception <NSObject: {ptr:p}>"));
@@ -422,6 +423,6 @@ mod tests {
422423
ignore = "panic won't start on 32-bit / w. fragile runtime, it'll just abort, since the runtime uses setjmp/longjump unwinding"
423424
)]
424425
fn does_not_catch_panic() {
425-
let _ = unsafe { catch(|| panic!("test")) };
426+
let _ = catch(|| panic!("test"));
426427
}
427428
}

crates/objc2/src/runtime/message_receiver.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -424,10 +424,7 @@ pub unsafe trait MessageReceiver: private::Sealed + Sized {
424424
}
425425

426426
// SAFETY: Upheld by caller
427-
//
428-
// The @catch is safe since message sending primitives are guaranteed
429-
// to do Objective-C compatible unwinding.
430-
unsafe { conditional_try!(|| msg_send_primitive::send(receiver, sel, args)) }
427+
conditional_try!(|| unsafe { msg_send_primitive::send(receiver, sel, args) })
431428
}
432429

433430
/// Sends a message to a specific superclass with the given selector and
@@ -469,10 +466,10 @@ pub unsafe trait MessageReceiver: private::Sealed + Sized {
469466
msg_send_check_class(superclass, sel, A::ENCODINGS, &R::ENCODING_RETURN);
470467
}
471468

472-
// SAFETY: Same as in `send_message`
473-
unsafe {
474-
conditional_try!(|| msg_send_primitive::send_super(receiver, superclass, sel, args))
475-
}
469+
// SAFETY: Upheld by caller
470+
conditional_try!(|| unsafe {
471+
msg_send_primitive::send_super(receiver, superclass, sel, args)
472+
})
476473
}
477474
}
478475

crates/tests/src/exception.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ fn throw_catch_raise_catch() {
2222

2323
let exc = autoreleasepool(|_| {
2424
let exc = NSException::into_exception(exc);
25-
let res = unsafe { catch(|| throw(exc)) };
25+
let res = catch(|| throw(exc));
2626
let exc = res.unwrap_err().unwrap();
2727
let exc = NSException::from_exception(exc).unwrap();
2828

@@ -33,14 +33,13 @@ fn throw_catch_raise_catch() {
3333
assert_eq!(exc.retainCount(), 1);
3434

3535
let exc = autoreleasepool(|_| {
36-
let inner = || {
36+
let res = catch(|| {
3737
autoreleasepool(|pool| {
3838
let exc = unsafe { Retained::autorelease(exc, pool) };
3939
exc.raise()
4040
})
41-
};
41+
});
4242

43-
let res = unsafe { catch(inner) };
4443
let exc = NSException::from_exception(res.unwrap_err().unwrap()).unwrap();
4544

4645
// Undesired: The inner pool _should_ have been drained on unwind, but
@@ -92,14 +91,15 @@ fn raise_catch() {
9291

9392
let exc = autoreleasepool(|pool| {
9493
let exc = unsafe { Retained::autorelease(exc, pool) };
95-
let inner = || {
94+
let res = catch(|| {
9695
if exc.name() == name {
9796
exc.raise();
9897
} else {
9998
42
10099
}
101-
};
102-
let res = unsafe { catch(inner) }.unwrap_err().unwrap();
100+
})
101+
.unwrap_err()
102+
.unwrap();
103103
assert_eq!(exc.retainCount(), 2);
104104
res
105105
});
@@ -120,12 +120,10 @@ fn raise_catch() {
120120
ignore = "Panics inside `catch` when catch-all is enabled"
121121
)]
122122
fn catch_actual() {
123-
let res = unsafe {
124-
catch(|| {
125-
let arr: Retained<NSArray<NSObject>> = NSArray::new();
126-
let _obj: *mut NSObject = msg_send![&arr, objectAtIndex: 0usize];
127-
})
128-
};
123+
let res = catch(|| {
124+
let arr: Retained<NSArray<NSObject>> = NSArray::new();
125+
let _obj: *mut NSObject = unsafe { msg_send![&arr, objectAtIndex: 0usize] };
126+
});
129127
let exc = res.unwrap_err().unwrap();
130128

131129
let name = "NSRangeException";

0 commit comments

Comments
 (0)