Skip to content

Commit

Permalink
Merge pull request #57 from madsmtm/exception-merge
Browse files Browse the repository at this point in the history
Move `objc2_exception` into `objc2::exception`
  • Loading branch information
madsmtm authored Nov 2, 2021
2 parents 854711e + ff865df commit ed8f828
Show file tree
Hide file tree
Showing 16 changed files with 169 additions and 210 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/apple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion .github/workflows/gnustep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions .travis-disabled.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ members = [
"objc2_block",
"objc2_block_sys",
"objc2_encode",
"objc2_exception",
"objc2_foundation",
"objc2_sys",
"objc2_test_utils",
Expand Down
10 changes: 8 additions & 2 deletions objc2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,18 @@ exclude = [
build = "build.rs"

[features]
exception = ["objc2_exception"]
# 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 = []

[dependencies]
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 }
2 changes: 1 addition & 1 deletion objc2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
14 changes: 14 additions & 0 deletions objc2/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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 <https://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-runtime-objc-retain>.
id objc_retain(id value);
Expand Down
141 changes: 134 additions & 7 deletions objc2/src/exception.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,87 @@
//! Objective-C's @throw and @try/@catch.
//!
//! This is only available when the `exception` feature is enabled.
//!
//! See the following links for more information:
//! - <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Exceptions/Tasks/HandlingExceptions.html>
//! - <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocExceptionHandling.html>
//! - <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Exceptions/Exceptions.html>
//! - <https://llvm.org/docs/ExceptionHandling.html>
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 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: Option<&Id<Object, Shared>>) -> ! {
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<F: FnOnce()>(closure: F) -> Result<(), Option<Id<Object, Shared>>> {
extern "C" fn try_objc_execute_closure<F: FnOnce()>(closure: &mut Option<F>) {
// This is always passed Some, so it's safe to unwrap
let closure = closure.take().unwrap();
closure();
}

let f: extern "C" fn(&mut Option<F>) = 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 {
// 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| Id::new(e)))
}
}

/// Tries to execute the given closure and catches an Objective-C exception
/// if one is thrown.
Expand All @@ -15,14 +92,64 @@ use objc2_exception::r#try;
///
/// # Safety
///
/// The given closure must not panic.
/// The given closure must not panic (e.g. normal Rust unwinding into this
/// causes undefined behaviour).
///
/// 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 catch_exception<R>(
closure: impl FnOnce() -> R,
) -> Result<R, Option<Id<Object, Shared>>> {
r#try(closure).map_err(|e| NonNull::new(e).map(|e| Id::new(e.cast())))
pub unsafe fn catch<R>(closure: impl FnOnce() -> R) -> Result<R, Option<Id<Object, Shared>>> {
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 super::*;

#[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_none() {
let s = "Hello".to_string();
let result = unsafe {
catch(move || {
if !s.is_empty() {
throw(None);
}
s.len()
})
};
assert!(result.unwrap_err().is_none());
}

#[test]
fn test_throw_catch_object() {
let obj: Id<Object, Shared> = 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);
}
}
4 changes: 2 additions & 2 deletions objc2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion objc2/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions objc2/src/message/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ 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<R: Encode>(f: impl FnOnce() -> R) -> Result<R, MessageError> {
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 {
Expand All @@ -20,7 +20,7 @@ unsafe fn conditional_try<R: Encode>(f: impl FnOnce() -> R) -> Result<R, Message
})
}

#[cfg(not(feature = "exception"))]
#[cfg(not(feature = "catch_all"))]
#[inline(always)]
unsafe fn conditional_try<R: Encode>(f: impl FnOnce() -> R) -> Result<R, MessageError> {
Ok(f())
Expand Down Expand Up @@ -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
*/
Expand Down
26 changes: 0 additions & 26 deletions objc2_exception/Cargo.toml

This file was deleted.

9 changes: 0 additions & 9 deletions objc2_exception/README.md

This file was deleted.

14 changes: 0 additions & 14 deletions objc2_exception/build.rs

This file was deleted.

Loading

0 comments on commit ed8f828

Please sign in to comment.