From 8d87a8d89560c4a18618eac479904ea8e8ea52b8 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 31 Oct 2021 00:49:10 +0200 Subject: [PATCH 1/3] Move objc2_exception into objc2::exception --- .travis-disabled.yml | 4 +- Cargo.toml | 1 - objc2/Cargo.toml | 7 +- objc2/build.rs | 14 ++ {objc2_exception => objc2}/extern/exception.m | 2 +- objc2/src/exception.rs | 114 ++++++++++++++- objc2/src/lib.rs | 2 +- objc2/src/message/mod.rs | 2 +- objc2_exception/Cargo.toml | 26 ---- objc2_exception/README.md | 9 -- objc2_exception/build.rs | 14 -- objc2_exception/src/lib.rs | 138 ------------------ 12 files changed, 132 insertions(+), 201 deletions(-) rename {objc2_exception => objc2}/extern/exception.m (89%) delete mode 100644 objc2_exception/Cargo.toml delete mode 100644 objc2_exception/README.md delete mode 100644 objc2_exception/build.rs delete mode 100644 objc2_exception/src/lib.rs diff --git a/.travis-disabled.yml b/.travis-disabled.yml index c87cb6b09..df438a7d9 100644 --- a/.travis-disabled.yml +++ b/.travis-disabled.yml @@ -23,8 +23,8 @@ jobs: script: - cargo test --workspace --verbose - # TODO: cargo test --workspace --verbose --all-features - - # objc2_exception doesn't work on 32bit? - cargo test --workspace --exclude objc2_exception --verbose -Z build-std --target i686-apple-darwin + - # exception doesn't work on 32bit? + cargo test --workspace --verbose -Z build-std --target i686-apple-darwin - # TODO: cargo test --workspace --verbose --all-features -Z build-std --target i686-apple-darwin - name: MacOS 11.3 diff --git a/Cargo.toml b/Cargo.toml index b41ffca9c..c336b20bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "objc2_block", "objc2_block_sys", "objc2_encode", - "objc2_exception", "objc2_foundation", "objc2_sys", "objc2_test_utils", diff --git a/objc2/Cargo.toml b/objc2/Cargo.toml index cc9bac13d..8f2e039d3 100644 --- a/objc2/Cargo.toml +++ b/objc2/Cargo.toml @@ -24,7 +24,8 @@ exclude = [ build = "build.rs" [features] -exception = ["objc2_exception"] +# Enables `objc2::exception::throw` and `objc2::exception::catch` +exception = ["cc"] verify_message = [] unstable_autoreleasesafe = [] @@ -32,4 +33,6 @@ unstable_autoreleasesafe = [] malloc_buf = "1.0" objc2_sys = { path = "../objc2_sys" } objc2_encode = { path = "../objc2_encode" } -objc2_exception = { path = "../objc2_exception", optional = true } + +[build-dependencies] +cc = { version = "1", optional = true } diff --git a/objc2/build.rs b/objc2/build.rs index 70fdc688a..2b2fa06b3 100644 --- a/objc2/build.rs +++ b/objc2/build.rs @@ -6,4 +6,18 @@ 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_exception/extern/exception.m b/objc2/extern/exception.m similarity index 89% rename from objc2_exception/extern/exception.m rename to objc2/extern/exception.m index 9d42d4df0..9a16342b2 100644 --- a/objc2_exception/extern/exception.m +++ b/objc2/extern/exception.m @@ -1,7 +1,7 @@ // Don't include any headers, cross compilation is difficult to set up // properly in such situations. -/// We're linking to `libobjc` so this should be available. +/// We're linking to `libobjc` in build.rs, so this should be available. /// /// See . id objc_retain(id value); diff --git a/objc2/src/exception.rs b/objc2/src/exception.rs index 2465e3aa0..8b20322d5 100644 --- a/objc2/src/exception.rs +++ b/objc2/src/exception.rs @@ -1,10 +1,72 @@ +//! Objective-C's @throw and @try/@catch. +//! +//! This is only available when the `exception` feature is enabled. +//! +//! See the following links for more information: +//! - +//! - +//! - +//! - + +use core::ffi::c_void; +use core::mem; +use core::ptr; use core::ptr::NonNull; +use std::os::raw::c_uchar; use crate::rc::{Id, Shared}; use crate::runtime::Object; -use objc2_exception::r#try; -// Comment copied from `objc2_exception` +use objc2_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; +} + +/// Throws an Objective-C exception. +/// +/// The argument must be a pointer to an Objective-C object. +/// +/// # Safety +/// +/// This unwinds from Objective-C, and the exception must be caught using an +/// Objective-C exception handler. +/// +/// This also invokes undefined behaviour until `C-unwind` is stabilized, see +/// [RFC-2945]. +/// +/// [RFC-2945]: https://rust-lang.github.io/rfcs/2945-c-unwind-abi.html +#[inline] +pub unsafe fn throw(exception: *mut Object) -> ! { + objc_exception_throw(exception as *mut objc_object) +} + +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) = 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 = rust_objc_try_catch_exception(f, context, &mut exception); + + if success == 0 { + Ok(()) + } else { + Err(NonNull::new(exception as *mut Object).map(|e| Id::new(e))) + } +} /// Tries to execute the given closure and catches an Objective-C exception /// if one is thrown. @@ -21,8 +83,48 @@ use objc2_exception::r#try; /// undefined behaviour until `C-unwind` is stabilized, see [RFC-2945]. /// /// [RFC-2945]: https://rust-lang.github.io/rfcs/2945-c-unwind-abi.html -pub unsafe fn catch_exception( - closure: impl FnOnce() -> R, -) -> Result>> { - r#try(closure).map_err(|e| NonNull::new(e).map(|e| Id::new(e.cast()))) +pub unsafe fn catch(closure: impl FnOnce() -> R) -> Result>> { + let mut value = None; + let result = { + let value_ref = &mut value; + try_no_ret(move || { + *value_ref = Some(closure()); + }) + }; + // If the try succeeded, this was set so it's safe to unwrap + result.map(|_| value.unwrap()) +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + use core::ptr; + + use super::{catch, throw}; + + #[test] + fn test_catch() { + let mut s = "Hello".to_string(); + let result = unsafe { + catch(move || { + s.push_str(", World!"); + s + }) + }; + assert_eq!(result.unwrap(), "Hello, World!"); + } + + #[test] + fn test_throw_catch() { + let s = "Hello".to_string(); + let result = unsafe { + catch(move || { + if !s.is_empty() { + throw(ptr::null_mut()); + } + s.len() + }) + }; + assert!(result.unwrap_err().is_none()); + } } diff --git a/objc2/src/lib.rs b/objc2/src/lib.rs index 271c94148..212dd34e2 100644 --- a/objc2/src/lib.rs +++ b/objc2/src/lib.rs @@ -90,7 +90,7 @@ mod cache; pub mod declare; mod encode; #[cfg(feature = "exception")] -mod exception; +pub mod exception; mod message; pub mod rc; pub mod runtime; diff --git a/objc2/src/message/mod.rs b/objc2/src/message/mod.rs index eb16d5d49..2a59fff11 100644 --- a/objc2/src/message/mod.rs +++ b/objc2/src/message/mod.rs @@ -11,7 +11,7 @@ use crate::{Encode, EncodeArguments, RefEncode}; #[cfg(feature = "exception")] unsafe fn conditional_try(f: impl FnOnce() -> R) -> Result { use alloc::borrow::ToOwned; - crate::exception::catch_exception(f).map_err(|exception| { + crate::exception::catch(f).map_err(|exception| { if let Some(exception) = exception { MessageError(alloc::format!("Uncaught exception {:?}", exception)) } else { diff --git a/objc2_exception/Cargo.toml b/objc2_exception/Cargo.toml deleted file mode 100644 index 493c35d7a..000000000 --- a/objc2_exception/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "objc2_exception" -version = "0.1.2" # Remember to update html_root_url in lib.rs -authors = ["Steven Sheldon", "Mads Marquart "] -edition = "2018" - -description = "Objective-C's @throw and @try/@catch statements" -keywords = ["objective-c", "macos", "ios", "try", "catch"] -categories = [ - "api-bindings", - "development-tools::ffi", - # "no-std", # TODO - "os::macos-apis", -] -readme = "README.md" -repository = "https://github.com/madsmtm/objc2" -documentation = "https://docs.rs/objc2_exception/" -license = "MIT" - -build = "build.rs" - -[dependencies] -objc2_sys = { path = "../objc2_sys" } - -[build-dependencies] -cc = "1" diff --git a/objc2_exception/README.md b/objc2_exception/README.md deleted file mode 100644 index da957817f..000000000 --- a/objc2_exception/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# `objc2_exception` - -[![Latest version](https://badgen.net/crates/v/objc2_exception)](https://crates.io/crates/objc2_exception) -[![License](https://badgen.net/badge/license/MIT/blue)](../LICENSE.txt) -[![Documentation](https://docs.rs/objc2_exception/badge.svg)](https://docs.rs/objc2_exception/) -[![Apple CI](https://github.com/madsmtm/objc2/actions/workflows/apple.yml/badge.svg)](https://github.com/madsmtm/objc2/actions/workflows/apple.yml) -[![GNUStep CI](https://github.com/madsmtm/objc2/actions/workflows/gnustep.yml/badge.svg)](https://github.com/madsmtm/objc2/actions/workflows/gnustep.yml) - -Objective-C's @throw and @try/@catch statements in Rust. diff --git a/objc2_exception/build.rs b/objc2_exception/build.rs deleted file mode 100644 index 1dbed0aab..000000000 --- a/objc2_exception/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use std::env; - -fn main() { - 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_exception/src/lib.rs b/objc2_exception/src/lib.rs deleted file mode 100644 index 77bc78c9f..000000000 --- a/objc2_exception/src/lib.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! Rust interface for Objective-C's `@throw` and `@try`/`@catch` statements. -//! -//! See the following links for more information: -//! - -//! - -//! - -//! - - -#![no_std] -#![warn(missing_docs)] -// Update in Cargo.toml as well. -#![doc(html_root_url = "https://docs.rs/objc2_exception/0.1.2")] - -#[cfg(test)] -extern crate alloc; - -#[cfg(doctest)] -#[doc = include_str!("../README.md")] -extern "C" {} - -use core::ffi::c_void; -use core::mem; -use core::ptr; - -use objc2_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, - ) -> u8; // std::os::raw::c_uchar -} - -/// An opaque type representing any Objective-C object thrown as an exception. -/// -/// In the future this will be an [`extern type`][rfc-1861], if that gets -/// stabilized. -/// -/// [rfc-1861]: https://rust-lang.github.io/rfcs/1861-extern-types.html -pub type Exception = objc_object; - -/// Throws an Objective-C exception. -/// -/// The argument must be a pointer to an Objective-C object. -/// -/// # Safety -/// -/// This unwinds from Objective-C, and the exception must be caught using an -/// Objective-C exception handler. -/// -/// This also invokes undefined behaviour until `C-unwind` is stabilized, see -/// [RFC-2945]. -/// -/// [RFC-2945]: https://rust-lang.github.io/rfcs/2945-c-unwind-abi.html -#[inline] -pub unsafe fn throw(exception: *mut Exception) -> ! { - objc_exception_throw(exception) -} - -unsafe fn try_no_ret(closure: F) -> Result<(), *mut Exception> { - 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) = 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 = rust_objc_try_catch_exception(f, context, &mut exception); - - if success == 0 { - Ok(()) - } else { - Err(exception) - } -} - -/// Tries to execute the given closure and catches an Objective-C exception -/// if one is thrown. -/// -/// Returns a `Result` that is either `Ok` if the closure succeeded without an -/// exception being thrown, or an `Err` with a pointer to an exception if one -/// was thrown. The exception is retained and so must be released. -/// -/// # Safety -/// -/// The given closure must not panic. -/// -/// Additionally, this unwinds through the closure from Objective-C, which is -/// undefined behaviour until `C-unwind` is stabilized, see [RFC-2945]. -/// -/// [RFC-2945]: https://rust-lang.github.io/rfcs/2945-c-unwind-abi.html -pub unsafe fn r#try(closure: impl FnOnce() -> R) -> Result { - let mut value = None; - let result = { - let value_ref = &mut value; - try_no_ret(move || { - *value_ref = Some(closure()); - }) - }; - // If the try succeeded, this was set so it's safe to unwrap - result.map(|_| value.unwrap()) -} - -#[cfg(test)] -mod tests { - use alloc::string::ToString; - use core::ptr; - - use super::{r#try, throw}; - - #[test] - fn test_try() { - unsafe { - let s = "Hello".to_string(); - let result = r#try(move || { - if !s.is_empty() { - throw(ptr::null_mut()); - } - s.len() - }); - assert!(result.unwrap_err() == ptr::null_mut()); - - let mut s = "Hello".to_string(); - let result = r#try(move || { - s.push_str(", World!"); - s - }); - assert!(result.unwrap() == "Hello, World!"); - } - } -} From 12ac1c1cfba0950d5279a9ef1f8cb4de79a5665b Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 31 Oct 2021 00:34:54 +0200 Subject: [PATCH 2/3] Add catch_all feature, allowing freestanding usage of `catch` Otherwise the user would be forced to catch all `msg_send!`s, while they may only have wanted to catch them in a specific place. --- .github/workflows/apple.yml | 2 +- .github/workflows/gnustep.yml | 2 +- objc2/Cargo.toml | 3 +++ objc2/README.md | 2 +- objc2/src/lib.rs | 2 +- objc2/src/macros.rs | 2 +- objc2/src/message/mod.rs | 6 +++--- 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/apple.yml b/.github/workflows/apple.yml index 46c70bff3..a20d1c733 100644 --- a/.github/workflows/apple.yml +++ b/.github/workflows/apple.yml @@ -76,4 +76,4 @@ jobs: with: command: test # Not using --all-features because some features are nightly-only - args: --verbose --no-fail-fast --features block,exception,verify_message + args: --verbose --no-fail-fast --features block,exception,catch_all,verify_message diff --git a/.github/workflows/gnustep.yml b/.github/workflows/gnustep.yml index 6c40f3f14..e43c2806f 100644 --- a/.github/workflows/gnustep.yml +++ b/.github/workflows/gnustep.yml @@ -114,4 +114,4 @@ jobs: with: command: test # Not using --all-features because some features are nightly-only - args: --verbose --no-fail-fast --features gnustep-1-9,block,exception,verify_message + args: --verbose --no-fail-fast --features gnustep-1-9,block,exception,catch_all,verify_message diff --git a/objc2/Cargo.toml b/objc2/Cargo.toml index 8f2e039d3..58baac370 100644 --- a/objc2/Cargo.toml +++ b/objc2/Cargo.toml @@ -26,6 +26,9 @@ build = "build.rs" [features] # Enables `objc2::exception::throw` and `objc2::exception::catch` exception = ["cc"] + +# Wrap every `objc2::msg_send` call in a `@try/@catch` block +catch_all = ["exception"] verify_message = [] unstable_autoreleasesafe = [] diff --git a/objc2/README.md b/objc2/README.md index 2c97ecda8..cd2bda13e 100644 --- a/objc2/README.md +++ b/objc2/README.md @@ -94,7 +94,7 @@ decl.register(); By default, if the `msg_send!` macro causes an exception to be thrown, this will unwind into Rust resulting in unsafe, undefined behavior. -However, this crate has an `"exception"` feature which, when enabled, wraps +However, this crate has an `"catch_all"` feature which, when enabled, wraps each `msg_send!` in a `@try`/`@catch` and panics if an exception is caught, preventing Objective-C from unwinding into Rust. diff --git a/objc2/src/lib.rs b/objc2/src/lib.rs index 212dd34e2..240599a1f 100644 --- a/objc2/src/lib.rs +++ b/objc2/src/lib.rs @@ -32,7 +32,7 @@ the [`declare`](declare/index.html) module. By default, if the `msg_send!` macro causes an exception to be thrown, this will unwind into Rust resulting in unsafe, undefined behavior. -However, this crate has an `"exception"` feature which, when enabled, wraps +However, this crate has an `"catch_all"` feature which, when enabled, wraps each `msg_send!` in a `@try`/`@catch` and panics if an exception is caught, preventing Objective-C from unwinding into Rust. diff --git a/objc2/src/macros.rs b/objc2/src/macros.rs index 282498d29..b94afa017 100644 --- a/objc2/src/macros.rs +++ b/objc2/src/macros.rs @@ -78,7 +78,7 @@ Variadic arguments are not currently supported. # Panics -Panics if the `exception` feature is enabled and the Objective-C method throws +Panics if the `catch_all` feature is enabled and the Objective-C method throws an exception. And panics if the `verify_message` feature is enabled and the Objective-C diff --git a/objc2/src/message/mod.rs b/objc2/src/message/mod.rs index 2a59fff11..45d34d20b 100644 --- a/objc2/src/message/mod.rs +++ b/objc2/src/message/mod.rs @@ -8,7 +8,7 @@ use crate::rc::{Id, Ownership}; use crate::runtime::{Class, Imp, Object, Sel}; use crate::{Encode, EncodeArguments, RefEncode}; -#[cfg(feature = "exception")] +#[cfg(feature = "catch_all")] unsafe fn conditional_try(f: impl FnOnce() -> R) -> Result { use alloc::borrow::ToOwned; crate::exception::catch(f).map_err(|exception| { @@ -20,7 +20,7 @@ unsafe fn conditional_try(f: impl FnOnce() -> R) -> Result(f: impl FnOnce() -> R) -> Result { Ok(f()) @@ -300,7 +300,7 @@ An error encountered while attempting to send a message. Currently, an error may be returned in two cases: -* an Objective-C exception is thrown and the `exception` feature is enabled +* an Objective-C exception is thrown and the `catch_all` feature is enabled * the encodings of the arguments do not match the encoding of the method and the `verify_message` feature is enabled */ From ff865df57ee0e3577171b120e0a2beba29e217a5 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 31 Oct 2021 01:27:12 +0200 Subject: [PATCH 3/3] Make objc2::exception `throw` and `catch` a bit safer --- objc2/src/exception.rs | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/objc2/src/exception.rs b/objc2/src/exception.rs index 8b20322d5..0e08dcf9a 100644 --- a/objc2/src/exception.rs +++ b/objc2/src/exception.rs @@ -34,15 +34,21 @@ extern "C" { /// # Safety /// /// This unwinds from Objective-C, and the exception must be caught using an -/// Objective-C exception handler. +/// Objective-C exception handler like [`catch`] (and specifically not +/// [`catch_unwind`]). /// /// This also invokes undefined behaviour until `C-unwind` is stabilized, see /// [RFC-2945]. /// +/// [`catch_unwind`]: std::panic::catch_unwind /// [RFC-2945]: https://rust-lang.github.io/rfcs/2945-c-unwind-abi.html #[inline] -pub unsafe fn throw(exception: *mut Object) -> ! { - objc_exception_throw(exception as *mut objc_object) +pub unsafe fn throw(exception: Option<&Id>) -> ! { + let exception = match exception { + Some(id) => &**id as *const Object as *mut objc_object, + None => ptr::null_mut(), + }; + objc_exception_throw(exception) } unsafe fn try_no_ret(closure: F) -> Result<(), Option>> { @@ -64,6 +70,15 @@ unsafe fn try_no_ret(closure: F) -> Result<(), Option(closure: F) -> Result<(), Option(closure: impl FnOnce() -> R) -> Result = unsafe { Id::new(msg_send![class!(NSObject), new]) }; + + let result = unsafe { catch(|| throw(Some(&obj))) }; + let e = result.unwrap_err().unwrap(); + // Compare pointers + assert_eq!(&*e as *const Object, &*obj as *const Object); + } }