diff --git a/crates/objc2/src/__macro_helpers.rs b/crates/objc2/src/__macro_helpers.rs index e7e4831f5..1c6fb3911 100644 --- a/crates/objc2/src/__macro_helpers.rs +++ b/crates/objc2/src/__macro_helpers.rs @@ -105,6 +105,7 @@ pub const fn retain_semantics(selector: &str) -> u8 { } pub trait MsgSendId { + #[track_caller] unsafe fn send_message_id>( obj: T, sel: Sel, @@ -164,7 +165,6 @@ unsafe fn encountered_error(err: *mut E) -> Id { impl MsgSendId> for New { #[inline] - #[track_caller] unsafe fn send_message_id>>( obj: T, sel: Sel, @@ -184,7 +184,6 @@ impl MsgSendId MsgSendId<&'_ Class, Allocated> for Alloc { #[inline] - #[track_caller] unsafe fn send_message_id>>( cls: &Class, sel: Sel, @@ -200,7 +199,6 @@ impl MsgSendId<&'_ Class, Allocated> for Alloc { impl MsgSendId>, Id> for Init { #[inline] - #[track_caller] unsafe fn send_message_id>>( obj: Option>, sel: Sel, @@ -224,7 +222,6 @@ impl MsgSendId>>( obj: T, sel: Sel, @@ -241,7 +238,6 @@ impl MsgSendId MsgSendId> for Other { #[inline] - #[track_caller] unsafe fn send_message_id>>( obj: T, sel: Sel, @@ -265,6 +261,7 @@ impl MsgSendId> for Ot pub trait MaybeUnwrap { type Input; + #[track_caller] fn maybe_unwrap<'a, F: MsgSendIdFailed<'a>>(obj: Option, args: F::Args) -> Self; } @@ -272,7 +269,6 @@ impl MaybeUnwrap for Option> { type Input = Id; #[inline] - #[track_caller] fn maybe_unwrap<'a, F: MsgSendIdFailed<'a>>(obj: Option>, _args: F::Args) -> Self { obj } @@ -282,7 +278,6 @@ impl MaybeUnwrap for Id { type Input = Id; #[inline] - #[track_caller] fn maybe_unwrap<'a, F: MsgSendIdFailed<'a>>(obj: Option>, args: F::Args) -> Self { match obj { Some(obj) => obj, @@ -295,7 +290,6 @@ impl MaybeUnwrap for Option> { type Input = Allocated; #[inline] - #[track_caller] fn maybe_unwrap<'a, F: MsgSendIdFailed<'a>>(obj: Option>, _args: F::Args) -> Self { obj } @@ -305,7 +299,6 @@ impl MaybeUnwrap for Allocated { type Input = Allocated; #[inline] - #[track_caller] fn maybe_unwrap<'a, F: MsgSendIdFailed<'a>>(obj: Option>, args: F::Args) -> Self { match obj { Some(obj) => obj, @@ -323,6 +316,7 @@ impl MaybeUnwrap for Allocated { pub trait MsgSendIdFailed<'a> { type Args; + #[track_caller] fn failed(args: Self::Args) -> !; } @@ -330,7 +324,6 @@ impl<'a> MsgSendIdFailed<'a> for New { type Args = (Option<&'a Object>, Sel); #[cold] - #[track_caller] fn failed((obj, sel): Self::Args) -> ! { if let Some(obj) = obj { let cls = obj.class(); @@ -353,7 +346,6 @@ impl<'a> MsgSendIdFailed<'a> for Alloc { type Args = (&'a Class, Sel); #[cold] - #[track_caller] fn failed((cls, sel): Self::Args) -> ! { if sel == alloc_sel() { panic!("failed allocating {:?}", cls) @@ -367,7 +359,6 @@ impl MsgSendIdFailed<'_> for Init { type Args = (*const Object, Sel); #[cold] - #[track_caller] fn failed((ptr, sel): Self::Args) -> ! { if ptr.is_null() { panic!("failed allocating object") @@ -387,7 +378,6 @@ impl MsgSendIdFailed<'_> for CopyOrMutCopy { type Args = (); #[cold] - #[track_caller] fn failed(_: Self::Args) -> ! { panic!("failed copying object") } @@ -397,7 +387,6 @@ impl<'a> MsgSendIdFailed<'a> for Other { type Args = (Option<&'a Object>, Sel); #[cold] - #[track_caller] fn failed((obj, sel): Self::Args) -> ! { if let Some(obj) = obj { let cls = obj.class(); diff --git a/crates/objc2/src/declare.rs b/crates/objc2/src/declare.rs index e478623d4..cce301fc6 100644 --- a/crates/objc2/src/declare.rs +++ b/crates/objc2/src/declare.rs @@ -985,7 +985,7 @@ mod tests { let _cls = Custom::class(); } - // Proof-of-concept how we could make declare_class! accept generic. + // Proof-of-concept how we could make declare_class! accept generic types. #[test] fn test_generic() { struct GenericDeclareClass(T); diff --git a/crates/objc2/src/macros/declare_class.rs b/crates/objc2/src/macros/declare_class.rs index b836bc650..6d7a3168e 100644 --- a/crates/objc2/src/macros/declare_class.rs +++ b/crates/objc2/src/macros/declare_class.rs @@ -68,7 +68,7 @@ /// /// If the `#[method_id(...)]` attribute is used, the return type must be /// `Option>` or `Id`. Additionally, if the selector is in the -/// "init"-family, the "self"/"this" argument must be `Allocated`. +/// "init"-family, the `self`/`this` argument must be `Allocated`. /// /// Putting other attributes on the method such as `cfg`, `allow`, `doc`, /// `deprecated` and so on is supported. However, note that `cfg_attr` may not @@ -232,11 +232,11 @@ /// } /// /// unsafe impl NSCopying for MyCustomObject { -/// #[method(copyWithZone:)] -/// fn copy_with_zone(&self, _zone: *const NSZone) -> *mut Self { +/// #[method_id(copyWithZone:)] +/// fn copy_with_zone(&self, _zone: *const NSZone) -> Id { /// let mut obj = Self::new(*self.foo); /// *obj.bar = *self.bar; -/// obj.autorelease_return() +/// obj /// } /// /// // If we have tried to add other methods here, or had forgotten diff --git a/crates/objc2/tests/track_caller.rs b/crates/objc2/tests/track_caller.rs new file mode 100644 index 000000000..4e0d05c47 --- /dev/null +++ b/crates/objc2/tests/track_caller.rs @@ -0,0 +1,213 @@ +//! Test that our use of #[track_caller] is making the correct line number +//! show up. +use std::panic; +use std::process::abort; +use std::ptr; +use std::sync::Mutex; + +use objc2::encode::Encode; +use objc2::rc::{Allocated, Id, Shared, __RcTestObject}; +use objc2::runtime::{NSObject, Object}; +use objc2::{class, declare_class, msg_send, msg_send_id, ClassType}; + +static EXPECTED_MESSAGE: Mutex = Mutex::new(String::new()); +static EXPECTED_LINE: Mutex = Mutex::new(0); + +pub struct PanicChecker(()); + +impl PanicChecker { + fn new() -> Self { + panic::set_hook(Box::new(|info| { + let expected_message = EXPECTED_MESSAGE.lock().unwrap(); + let expected_line = EXPECTED_LINE.lock().unwrap(); + + let payload = info.payload(); + let message = if let Some(payload) = payload.downcast_ref::<&'static str>() { + payload.to_string() + } else if let Some(payload) = payload.downcast_ref::() { + payload.clone() + } else { + format!("could not extract message: {payload:?}") + }; + let location = info.location().expect("location"); + + if !message.contains(&*expected_message) { + eprintln!("expected {expected_message:?}, got: {message:?}"); + abort(); + } + if location.file() != file!() { + eprintln!("expected file {:?}, got: {:?}", file!(), location.file()); + abort(); + } + if location.line() != *expected_line { + eprintln!("expected line {expected_line}, got: {}", location.line()); + abort(); + } + })); + Self(()) + } + + fn assert_panics(&self, message: &str, line: u32, f: impl FnOnce()) { + *EXPECTED_MESSAGE.lock().unwrap() = message.to_string(); + *EXPECTED_LINE.lock().unwrap() = line; + + let res = panic::catch_unwind(panic::AssertUnwindSafe(|| { + f(); + })); + assert!(res.is_err()); + + *EXPECTED_MESSAGE.lock().unwrap() = "unknown".to_string(); + *EXPECTED_LINE.lock().unwrap() = 0; + } +} + +impl Drop for PanicChecker { + fn drop(&mut self) { + let _ = panic::take_hook(); + } +} + +#[test] +fn test_track_caller() { + let checker = PanicChecker::new(); + + #[cfg(debug_assertions)] + { + test_nil(&checker); + test_verify(&checker); + test_error_methods(&checker); + } + + test_id_unwrap(&checker); + + #[cfg(feature = "catch-all")] + test_catch_all(&checker); + + test_unwind(&checker); +} + +pub fn test_nil(checker: &PanicChecker) { + let nil: *mut Object = ptr::null_mut(); + + let msg = "messsaging description to nil"; + checker.assert_panics(msg, line!() + 1, || { + let _: *mut Object = unsafe { msg_send![nil, description] }; + }); + checker.assert_panics(msg, line!() + 1, || { + let _: *mut Object = unsafe { msg_send![super(nil, NSObject::class()), description] }; + }); + checker.assert_panics(msg, line!() + 1, || { + let _: Option> = unsafe { msg_send_id![nil, description] }; + }); +} + +pub fn test_verify(checker: &PanicChecker) { + let obj = NSObject::new(); + + let msg = "invalid message send to -[NSObject description]: expected return to have type code '@', but found 'v'"; + checker.assert_panics(msg, line!() + 1, || { + let _: () = unsafe { msg_send![&obj, description] }; + }); + + let msg = format!("invalid message send to -[NSObject hash]: expected return to have type code '{}', but found '@'", usize::ENCODING); + checker.assert_panics(&msg, line!() + 1, || { + let _: Option> = unsafe { msg_send_id![&obj, hash] }; + }); +} + +pub fn test_error_methods(checker: &PanicChecker) { + let nil: *mut Object = ptr::null_mut(); + + let msg = "messsaging someSelectorWithError: to nil"; + checker.assert_panics(msg, line!() + 2, || { + let _: Result<(), Id> = + unsafe { msg_send![nil, someSelectorWithError: _] }; + }); + checker.assert_panics(msg, line!() + 2, || { + let _: Result<(), Id> = + unsafe { msg_send![super(nil, NSObject::class()), someSelectorWithError: _] }; + }); + checker.assert_panics(msg, line!() + 2, || { + let _: Result, Id> = + unsafe { msg_send_id![nil, someSelectorWithError: _] }; + }); + + let msg = "invalid message send to -[NSObject someSelectorWithError:]: method not found"; + checker.assert_panics(msg, line!() + 3, || { + let obj = __RcTestObject::new(); + let _: Result<(), Id> = + unsafe { msg_send![super(&obj), someSelectorWithError: _] }; + }); +} + +pub fn test_id_unwrap(checker: &PanicChecker) { + let cls = __RcTestObject::class(); + let obj = __RcTestObject::new(); + + let msg = "failed creating new instance using +[__RcTestObject newReturningNull]"; + checker.assert_panics(msg, line!() + 1, || { + let _obj: Id<__RcTestObject, Shared> = unsafe { msg_send_id![cls, newReturningNull] }; + }); + + let msg = "failed allocating with +[__RcTestObject allocReturningNull]"; + checker.assert_panics(msg, line!() + 1, || { + let _obj: Allocated<__RcTestObject> = unsafe { msg_send_id![cls, allocReturningNull] }; + }); + + let msg = "failed initializing object with -initReturningNull"; + checker.assert_panics(msg, line!() + 2, || { + let _obj: Id<__RcTestObject, Shared> = + unsafe { msg_send_id![__RcTestObject::alloc(), initReturningNull] }; + }); + + let msg = "failed copying object"; + checker.assert_panics(msg, line!() + 1, || { + let _obj: Id<__RcTestObject, Shared> = unsafe { msg_send_id![&obj, copyReturningNull] }; + }); + + let msg = "unexpected NULL returned from -[__RcTestObject methodReturningNull]"; + checker.assert_panics(msg, line!() + 1, || { + let _obj: Id<__RcTestObject, Shared> = unsafe { msg_send_id![&obj, methodReturningNull] }; + }); +} + +pub fn test_catch_all(checker: &PanicChecker) { + let obj: Id = unsafe { msg_send_id![class!(NSArray), new] }; + + let msg = "NSRangeException"; + checker.assert_panics(msg, line!() + 1, || { + let _: *mut Object = unsafe { msg_send![&obj, objectAtIndex: 0usize] }; + }); + + let msg = "NSRangeException"; + checker.assert_panics(msg, line!() + 1, || { + let _: Id = unsafe { msg_send_id![&obj, objectAtIndex: 0usize] }; + }); +} + +declare_class!( + struct PanickingClass; + + unsafe impl ClassType for PanickingClass { + type Super = NSObject; + const NAME: &'static str = "PanickingClass"; + } + + unsafe impl PanickingClass { + #[method(panic)] + fn _panic() -> *mut Self { + panic!("panic in PanickingClass") + } + } +); + +pub fn test_unwind(checker: &PanicChecker) { + let msg = "panic in PanickingClass"; + let line = line!() - 7; + checker.assert_panics(msg, line, || { + let _: *mut Object = unsafe { msg_send![PanickingClass::class(), panic] }; + }); + checker.assert_panics(msg, line, || { + let _: Id = unsafe { msg_send_id![PanickingClass::class(), panic] }; + }); +}