11//! # `@throw` and `@try/@catch` exceptions.
22//!
3- //! By default, if the [`msg_send!`] macro causes an exception to be thrown,
4- //! this will unwind into Rust, resulting in undefined behavior. However, this
5- //! crate has an `"catch-all"` feature which, when enabled, wraps each
6- //! [`msg_send!`] in a `@catch` and panics if an exception is caught,
7- //! preventing Objective-C from unwinding into Rust.
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.
812//!
913//! Most of the functionality in this module is only available when the
1014//! `"exception"` feature is enabled.
@@ -239,7 +243,7 @@ pub fn throw(exception: Retained<Exception>) -> ! {
239243}
240244
241245#[ cfg( feature = "exception" ) ]
242- unsafe fn try_no_ret < F : FnOnce ( ) > ( closure : F ) -> Result < ( ) , Option < Retained < Exception > > > {
246+ fn try_no_ret < F : FnOnce ( ) > ( closure : F ) -> Result < ( ) , Option < Retained < Exception > > > {
243247 let f = {
244248 extern "C-unwind" fn try_objc_execute_closure < F > ( closure : & mut Option < F > )
245249 where
@@ -261,6 +265,18 @@ unsafe fn try_no_ret<F: FnOnce()>(closure: F) -> Result<(), Option<Retained<Exce
261265 let context = context. cast ( ) ;
262266
263267 let mut exception = ptr:: null_mut ( ) ;
268+ // SAFETY: The function pointer and context are valid.
269+ //
270+ // The exception catching itself is sound on the Rust side, because we
271+ // correctly use `extern "C-unwind"`. Objective-C does not completely
272+ // specify how foreign unwinds are handled, though they do have the
273+ // `@catch (...)` construct intended for catching C++ exceptions, so it is
274+ // likely that they intend to support Rust panics (and it works in
275+ // practice).
276+ //
277+ // See also:
278+ // https://github.com/rust-lang/rust/pull/128321
279+ // https://github.com/rust-lang/reference/pull/1226
264280 let success = unsafe { objc2_exception_helper:: try_catch ( f, context, & mut exception) } ;
265281
266282 if success == 0 {
@@ -269,12 +285,8 @@ unsafe fn try_no_ret<F: FnOnce()>(closure: F) -> Result<(), Option<Retained<Exce
269285 // SAFETY:
270286 // The exception is always a valid object or NULL.
271287 //
272- // Since we do a retain inside `extern/exception.m`, the object has
273- // +1 retain count.
274- //
275- // Code throwing an exception know that they don't hold sole access to
276- // that object any more, so even if the type was originally mutable,
277- // it is okay to create a new `Retained` to it here.
288+ // Since we do a retain in `objc2_exception_helper/src/try_catch.m`,
289+ // the object has +1 retain count.
278290 Err ( unsafe { Retained :: from_raw ( exception. cast ( ) ) } )
279291 }
280292}
@@ -283,8 +295,9 @@ unsafe fn try_no_ret<F: FnOnce()>(closure: F) -> Result<(), Option<Retained<Exce
283295/// if one is thrown.
284296///
285297/// This is the Objective-C equivalent of Rust's [`catch_unwind`].
286- /// Accordingly, if your Rust code is compiled with `panic=abort` this cannot
287- /// catch the exception.
298+ /// Accordingly, if your Rust code is compiled with `panic=abort`, or your
299+ /// Objective-C code with `-fno-objc-exceptions`, this cannot catch the
300+ /// exception.
288301///
289302/// [`catch_unwind`]: std::panic::catch_unwind
290303///
@@ -301,20 +314,26 @@ unsafe fn try_no_ret<F: FnOnce()>(closure: F) -> Result<(), Option<Retained<Exce
301314/// situations.
302315///
303316///
304- /// # Safety
317+ /// # Panics
318+ ///
319+ /// This panics if the given closure panics.
320+ ///
321+ /// That is, it completely ignores Rust unwinding and simply lets that pass
322+ /// through unchanged.
305323///
306- /// The given closure must not panic (e.g. normal Rust unwinding into this
307- /// causes undefined behaviour).
324+ /// It may also not catch all Objective-C exceptions (such as exceptions
325+ /// thrown when handling the memory management of the exception). These are
326+ /// mostly theoretical, and should only happen in utmost exceptional cases.
308327#[ cfg( feature = "exception" ) ]
309- pub unsafe fn catch < R > (
328+ pub fn catch < R > (
310329 closure : impl FnOnce ( ) -> R + UnwindSafe ,
311330) -> Result < R , Option < Retained < Exception > > > {
312331 let mut value = None ;
313332 let value_ref = & mut value;
314333 let closure = move || {
315334 * value_ref = Some ( closure ( ) ) ;
316335 } ;
317- let result = unsafe { try_no_ret ( closure) } ;
336+ let result = try_no_ret ( closure) ;
318337 // If the try succeeded, value was set so it's safe to unwrap
319338 result. map ( |( ) | value. unwrap_or_else ( || unreachable ! ( ) ) )
320339}
@@ -333,12 +352,10 @@ mod tests {
333352 #[ test]
334353 fn test_catch ( ) {
335354 let mut s = "Hello" . to_string ( ) ;
336- let result = unsafe {
337- catch ( move || {
338- s. push_str ( ", World!" ) ;
339- s
340- } )
341- } ;
355+ let result = catch ( move || {
356+ s. push_str ( ", World!" ) ;
357+ s
358+ } ) ;
342359 assert_eq ! ( result. unwrap( ) , "Hello, World!" ) ;
343360 }
344361
@@ -349,14 +366,12 @@ mod tests {
349366 ) ]
350367 fn test_catch_null ( ) {
351368 let s = "Hello" . to_string ( ) ;
352- let result = unsafe {
353- catch ( move || {
354- if !s. is_empty ( ) {
355- ffi:: objc_exception_throw ( ptr:: null_mut ( ) )
356- }
357- s. len ( )
358- } )
359- } ;
369+ let result = catch ( move || {
370+ if !s. is_empty ( ) {
371+ unsafe { ffi:: objc_exception_throw ( ptr:: null_mut ( ) ) }
372+ }
373+ s. len ( )
374+ } ) ;
360375 assert ! ( result. unwrap_err( ) . is_none( ) ) ;
361376 }
362377
@@ -368,11 +383,9 @@ mod tests {
368383 fn test_catch_unknown_selector ( ) {
369384 let obj = AssertUnwindSafe ( NSObject :: new ( ) ) ;
370385 let ptr = Retained :: as_ptr ( & obj) ;
371- let result = unsafe {
372- catch ( || {
373- let _: Retained < NSObject > = msg_send_id ! [ & * obj, copy] ;
374- } )
375- } ;
386+ let result = catch ( || {
387+ let _: Retained < NSObject > = unsafe { msg_send_id ! [ & * obj, copy] } ;
388+ } ) ;
376389 let err = result. unwrap_err ( ) . unwrap ( ) ;
377390
378391 assert_eq ! (
@@ -389,7 +402,7 @@ mod tests {
389402 let obj: Retained < Exception > = unsafe { Retained :: cast_unchecked ( obj) } ;
390403 let ptr: * const Exception = & * obj;
391404
392- let result = unsafe { catch ( || throw ( obj) ) } ;
405+ let result = catch ( || throw ( obj) ) ;
393406 let obj = result. unwrap_err ( ) . unwrap ( ) ;
394407
395408 assert_eq ! ( format!( "{obj:?}" ) , format!( "exception <NSObject: {ptr:p}>" ) ) ;
@@ -406,4 +419,14 @@ mod tests {
406419 let result = catch_unwind ( || throw ( obj) ) ;
407420 let _ = result. unwrap_err ( ) ;
408421 }
422+
423+ #[ test]
424+ #[ should_panic = "test" ]
425+ #[ cfg_attr(
426+ all( target_os = "macos" , target_arch = "x86" , panic = "unwind" ) ,
427+ ignore = "panic won't start on 32-bit / w. fragile runtime, it'll just abort, since the runtime uses setjmp/longjump unwinding"
428+ ) ]
429+ fn does_not_catch_panic ( ) {
430+ let _ = catch ( || panic ! ( "test" ) ) ;
431+ }
409432}
0 commit comments