From c4401c9259a8c58d596b98e17ae0a1947b19984a Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 31 Aug 2021 19:57:18 +0200 Subject: [PATCH] Remove extra build stage by handling unwinds in `try` directly in Rust Uses the same implementation as `std::panic::catch_unwind`, with small modifications to extract the exception object from Objective-C. --- objc-sys/src/exception.rs | 2 +- objc-sys/src/lib.rs | 1 + objc2/Cargo.toml | 4 +- objc2/build.rs | 14 ----- objc2/extern/exception.m | 23 -------- objc2/src/exception.rs | 108 ++++++++++++++++++++------------------ objc2/src/lib.rs | 3 +- 7 files changed, 63 insertions(+), 92 deletions(-) delete mode 100644 objc2/extern/exception.m diff --git a/objc-sys/src/exception.rs b/objc-sys/src/exception.rs index d288185b1..c6f69ef0b 100644 --- a/objc-sys/src/exception.rs +++ b/objc-sys/src/exception.rs @@ -31,7 +31,7 @@ pub type objc_uncaught_exception_handler = unsafe extern "C" fn(exception: *mut pub type objc_exception_handler = unsafe extern "C" fn(unused: *mut objc_object, context: *mut c_void); -extern "C" { +extern "C-unwind" { pub fn objc_begin_catch(exc_buf: *mut c_void) -> *mut objc_object; pub fn objc_end_catch(); /// See [`objc-exception.h`][objc-exception]. diff --git a/objc-sys/src/lib.rs b/objc-sys/src/lib.rs index 1ef68dd2b..15850df2e 100644 --- a/objc-sys/src/lib.rs +++ b/objc-sys/src/lib.rs @@ -17,6 +17,7 @@ #![allow(non_camel_case_types)] #![allow(non_upper_case_globals)] #![doc(html_root_url = "https://docs.rs/objc-sys/0.0.0")] +#![feature(c_unwind)] // TODO: Replace `extern "C"` with `extern "C-unwind"` where applicable. // See https://rust-lang.github.io/rfcs/2945-c-unwind-abi.html. diff --git a/objc2/Cargo.toml b/objc2/Cargo.toml index 29a06b2c4..ad38506b9 100644 --- a/objc2/Cargo.toml +++ b/objc2/Cargo.toml @@ -24,8 +24,8 @@ exclude = [ build = "build.rs" [features] -# Enables `objc2::exception::throw` and `objc2::exception::catch` -exception = ["cc"] +# Deprecated; does nothing +exception = [] # Wrap every `objc2::msg_send` call in a `@try/@catch` block catch_all = ["exception"] diff --git a/objc2/build.rs b/objc2/build.rs index 2b2fa06b3..70fdc688a 100644 --- a/objc2/build.rs +++ b/objc2/build.rs @@ -6,18 +6,4 @@ fn main() { let runtime = env::var("DEP_OBJC_RUNTIME").unwrap(); println!("cargo:rustc-cfg={}", runtime); - - #[cfg(feature = "exception")] - { - println!("cargo:rerun-if-changed=extern/exception.m"); - - let mut builder = cc::Build::new(); - builder.file("extern/exception.m"); - - for flag in env::var("DEP_OBJC_CC_ARGS").unwrap().split(' ') { - builder.flag(flag); - } - - builder.compile("librust_objc_try_catch_exception.a"); - } } diff --git a/objc2/extern/exception.m b/objc2/extern/exception.m deleted file mode 100644 index 9a16342b2..000000000 --- a/objc2/extern/exception.m +++ /dev/null @@ -1,23 +0,0 @@ -// Don't include any headers, cross compilation is difficult to set up -// properly in such situations. - -/// We're linking to `libobjc` in build.rs, so this should be available. -/// -/// See . -id objc_retain(id value); - -// We return `unsigned char`, since it is guaranteed to be an `u8` on all platforms -unsigned char rust_objc_try_catch_exception(void (*f)(void *), void *context, id *error) { - @try { - f(context); - if (error) { - *error = (id)0; // nil - } - return 0; - } @catch (id exception) { - if (error) { - *error = objc_retain(exception); - } - return 1; - } -} diff --git a/objc2/src/exception.rs b/objc2/src/exception.rs index f787a86ad..85c0ae029 100644 --- a/objc2/src/exception.rs +++ b/objc2/src/exception.rs @@ -9,23 +9,15 @@ //! - use core::ffi::c_void; -use core::mem; +use core::intrinsics; +use core::mem::ManuallyDrop; use core::ptr; use core::ptr::NonNull; -use std::os::raw::c_uchar; use crate::rc::{Id, Shared}; use crate::runtime::Object; -use objc_sys::{objc_exception_throw, objc_object}; - -extern "C" { - fn rust_objc_try_catch_exception( - f: extern "C" fn(*mut c_void), - context: *mut c_void, - error: *mut *mut objc_object, - ) -> c_uchar; -} +use objc_sys::{objc_begin_catch, objc_end_catch, objc_exception_throw, objc_object}; /// Throws an Objective-C exception. /// @@ -51,38 +43,6 @@ pub unsafe fn throw(exception: Option<&Id>) -> ! { unsafe { objc_exception_throw(exception) } } -unsafe fn try_no_ret(closure: F) -> Result<(), Option>> { - extern "C" fn try_objc_execute_closure(closure: &mut Option) { - // This is always passed Some, so it's safe to unwrap - let closure = closure.take().unwrap(); - closure(); - } - - let f: extern "C" fn(&mut Option) = try_objc_execute_closure; - let f: extern "C" fn(*mut c_void) = unsafe { mem::transmute(f) }; - // Wrap the closure in an Option so it can be taken - let mut closure = Some(closure); - let context = &mut closure as *mut _ as *mut c_void; - - let mut exception = ptr::null_mut(); - let success = unsafe { rust_objc_try_catch_exception(f, context, &mut exception) }; - - if success == 0 { - Ok(()) - } else { - // SAFETY: - // The exception is always a valid object (or NULL, but that has been - // checked). - // - // The ownership is safe as Shared; Objective-C code throwing an - // exception knows that they don't hold sole access to that exception - // instance any more, and Rust code is forbidden by requiring a Shared - // Id in `throw` (instead of just a shared reference, which could have - // come from an Owned Id). - Err(NonNull::new(exception as *mut Object).map(|e| unsafe { Id::new(e) })) - } -} - /// Tries to execute the given closure and catches an Objective-C exception /// if one is thrown. /// @@ -99,15 +59,61 @@ unsafe fn try_no_ret(closure: F) -> Result<(), Option(closure: impl FnOnce() -> R) -> Result>> { - let mut value = None; - let value_ref = &mut value; - let closure = move || { - *value_ref = Some(closure()); +pub unsafe fn catch R>(f: F) -> Result>> { + // Our implementation is just a copy of `std::panicking::r#try`: + // https://github.com/rust-lang/rust/blob/1.52.1/library/std/src/panicking.rs#L299-L408 + union Data { + f: ManuallyDrop, + r: ManuallyDrop, + p: ManuallyDrop>>, + } + + let mut data = Data { + f: ManuallyDrop::new(f), + }; + + let data_ptr = &mut data as *mut _ as *mut u8; + + return if unsafe { intrinsics::r#try(do_call::, data_ptr, do_catch::) } == 0 { + Ok(ManuallyDrop::into_inner(unsafe { data.r })) + } else { + Err(ManuallyDrop::into_inner(unsafe { data.p })) }; - let result = unsafe { try_no_ret(closure) }; - // If the try succeeded, this was set so it's safe to unwrap - result.map(|_| value.unwrap()) + + /// Only function that we've changed + #[cold] + unsafe fn cleanup(payload: *mut u8) -> Option> { + // We let Objective-C process the unwind payload, and hand us the + // exception object. Everything between this and `objc_end_catch` is + // treated as a `@catch` block. + let obj = unsafe { objc_begin_catch(payload as *mut c_void) }; + // We retain the exception since it might have been autoreleased. + // This cannot unwind, so we don't need extra guards here. + let obj = NonNull::new(obj.cast::()).map(|ptr| unsafe { Id::retain(ptr) }); + // End the `@catch` block. + unsafe { objc_end_catch() }; + obj + } + + #[inline] + fn do_call R, R>(data: *mut u8) { + unsafe { + let data = data as *mut Data; + let data = &mut (*data); + let f = ManuallyDrop::take(&mut data.f); + data.r = ManuallyDrop::new(f()); + } + } + + #[inline] + fn do_catch R, R>(data: *mut u8, payload: *mut u8) { + unsafe { + let data = data as *mut Data; + let data = &mut (*data); + let obj = cleanup(payload); + data.p = ManuallyDrop::new(obj); + } + } } #[cfg(test)] diff --git a/objc2/src/lib.rs b/objc2/src/lib.rs index c81c3ef40..c757524c2 100644 --- a/objc2/src/lib.rs +++ b/objc2/src/lib.rs @@ -63,6 +63,8 @@ The bindings can be used on Linux or *BSD utilizing the feature = "unstable_autoreleasesafe", feature(negative_impls, auto_traits) )] +#![feature(c_unwind)] +#![feature(core_intrinsics)] #![warn(elided_lifetimes_in_paths)] #![warn(missing_docs)] #![deny(non_ascii_idents)] @@ -92,7 +94,6 @@ mod bool; mod cache; pub mod declare; mod encode; -#[cfg(feature = "exception")] pub mod exception; mod message; pub mod rc;