Skip to content

Add #[track_caller] test #410

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 3 additions & 14 deletions crates/objc2/src/__macro_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ pub const fn retain_semantics(selector: &str) -> u8 {
}

pub trait MsgSendId<T, U> {
#[track_caller]
unsafe fn send_message_id<A: MessageArguments, R: MaybeUnwrap<Input = U>>(
obj: T,
sel: Sel,
Expand Down Expand Up @@ -164,7 +165,6 @@ unsafe fn encountered_error<E: Message>(err: *mut E) -> Id<E, Shared> {

impl<T: MessageReceiver, U: ?Sized + Message, O: Ownership> MsgSendId<T, Id<U, O>> for New {
#[inline]
#[track_caller]
unsafe fn send_message_id<A: MessageArguments, R: MaybeUnwrap<Input = Id<U, O>>>(
obj: T,
sel: Sel,
Expand All @@ -184,7 +184,6 @@ impl<T: MessageReceiver, U: ?Sized + Message, O: Ownership> MsgSendId<T, Id<U, O

impl<T: ?Sized + Message> MsgSendId<&'_ Class, Allocated<T>> for Alloc {
#[inline]
#[track_caller]
unsafe fn send_message_id<A: MessageArguments, R: MaybeUnwrap<Input = Allocated<T>>>(
cls: &Class,
sel: Sel,
Expand All @@ -200,7 +199,6 @@ impl<T: ?Sized + Message> MsgSendId<&'_ Class, Allocated<T>> for Alloc {

impl<T: ?Sized + Message, O: Ownership> MsgSendId<Option<Allocated<T>>, Id<T, O>> for Init {
#[inline]
#[track_caller]
unsafe fn send_message_id<A: MessageArguments, R: MaybeUnwrap<Input = Id<T, O>>>(
obj: Option<Allocated<T>>,
sel: Sel,
Expand All @@ -224,7 +222,6 @@ impl<T: MessageReceiver, U: ?Sized + Message, O: Ownership> MsgSendId<T, Id<U, O
for CopyOrMutCopy
{
#[inline]
#[track_caller]
unsafe fn send_message_id<A: MessageArguments, R: MaybeUnwrap<Input = Id<U, O>>>(
obj: T,
sel: Sel,
Expand All @@ -241,7 +238,6 @@ impl<T: MessageReceiver, U: ?Sized + Message, O: Ownership> MsgSendId<T, Id<U, O

impl<T: MessageReceiver, U: Message, O: Ownership> MsgSendId<T, Id<U, O>> for Other {
#[inline]
#[track_caller]
unsafe fn send_message_id<A: MessageArguments, R: MaybeUnwrap<Input = Id<U, O>>>(
obj: T,
sel: Sel,
Expand All @@ -265,14 +261,14 @@ impl<T: MessageReceiver, U: Message, O: Ownership> MsgSendId<T, Id<U, O>> for Ot

pub trait MaybeUnwrap {
type Input;
#[track_caller]
fn maybe_unwrap<'a, F: MsgSendIdFailed<'a>>(obj: Option<Self::Input>, args: F::Args) -> Self;
}

impl<T: ?Sized, O: Ownership> MaybeUnwrap for Option<Id<T, O>> {
type Input = Id<T, O>;

#[inline]
#[track_caller]
fn maybe_unwrap<'a, F: MsgSendIdFailed<'a>>(obj: Option<Id<T, O>>, _args: F::Args) -> Self {
obj
}
Expand All @@ -282,7 +278,6 @@ impl<T: ?Sized, O: Ownership> MaybeUnwrap for Id<T, O> {
type Input = Id<T, O>;

#[inline]
#[track_caller]
fn maybe_unwrap<'a, F: MsgSendIdFailed<'a>>(obj: Option<Id<T, O>>, args: F::Args) -> Self {
match obj {
Some(obj) => obj,
Expand All @@ -295,7 +290,6 @@ impl<T: ?Sized> MaybeUnwrap for Option<Allocated<T>> {
type Input = Allocated<T>;

#[inline]
#[track_caller]
fn maybe_unwrap<'a, F: MsgSendIdFailed<'a>>(obj: Option<Allocated<T>>, _args: F::Args) -> Self {
obj
}
Expand All @@ -305,7 +299,6 @@ impl<T: ?Sized> MaybeUnwrap for Allocated<T> {
type Input = Allocated<T>;

#[inline]
#[track_caller]
fn maybe_unwrap<'a, F: MsgSendIdFailed<'a>>(obj: Option<Allocated<T>>, args: F::Args) -> Self {
match obj {
Some(obj) => obj,
Expand All @@ -323,14 +316,14 @@ impl<T: ?Sized> MaybeUnwrap for Allocated<T> {
pub trait MsgSendIdFailed<'a> {
type Args;

#[track_caller]
fn failed(args: Self::Args) -> !;
}

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();
Expand All @@ -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)
Expand All @@ -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")
Expand All @@ -387,7 +378,6 @@ impl MsgSendIdFailed<'_> for CopyOrMutCopy {
type Args = ();

#[cold]
#[track_caller]
fn failed(_: Self::Args) -> ! {
panic!("failed copying object")
}
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion crates/objc2/src/declare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>(T);
Expand Down
8 changes: 4 additions & 4 deletions crates/objc2/src/macros/declare_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
///
/// If the `#[method_id(...)]` attribute is used, the return type must be
/// `Option<Id<T, O>>` or `Id<T, O>`. Additionally, if the selector is in the
/// "init"-family, the "self"/"this" argument must be `Allocated<Self>`.
/// "init"-family, the `self`/`this` argument must be `Allocated<Self>`.
///
/// Putting other attributes on the method such as `cfg`, `allow`, `doc`,
/// `deprecated` and so on is supported. However, note that `cfg_attr` may not
Expand Down Expand Up @@ -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<Self, Owned> {
/// 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
Expand Down
213 changes: 213 additions & 0 deletions crates/objc2/tests/track_caller.rs
Original file line number Diff line number Diff line change
@@ -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<String> = Mutex::new(String::new());
static EXPECTED_LINE: Mutex<u32> = 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::<String>() {
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<Id<Object, Shared>> = 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<Id<Object, Shared>> = 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<NSObject, Shared>> =
unsafe { msg_send![nil, someSelectorWithError: _] };
});
checker.assert_panics(msg, line!() + 2, || {
let _: Result<(), Id<NSObject, Shared>> =
unsafe { msg_send![super(nil, NSObject::class()), someSelectorWithError: _] };
});
checker.assert_panics(msg, line!() + 2, || {
let _: Result<Id<Object, Shared>, Id<NSObject, Shared>> =
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<NSObject, Shared>> =
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<NSObject, Shared> = 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<Object, Shared> = 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<Object, Shared> = unsafe { msg_send_id![PanickingClass::class(), panic] };
});
}