Skip to content

Commit

Permalink
Add test for the backtrace after a message send
Browse files Browse the repository at this point in the history
  • Loading branch information
madsmtm committed Jan 21, 2025
1 parent 830e9d3 commit 2d42180
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 0 deletions.
55 changes: 55 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/objc2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ objc2-exception-helper = { path = "../objc2-exception-helper", version = "0.1.0"
[dev-dependencies]
iai = { version = "0.1", git = "https://github.com/madsmtm/iai", branch = "callgrind" }
static_assertions = "1.1.0"
backtrace = "0.3.74"
memoffset = "0.9.0"
block2 = { path = "../block2" }
objc2-core-foundation = { path = "../../framework-crates/objc2-core-foundation", default-features = false, features = [
Expand Down
201 changes: 201 additions & 0 deletions crates/objc2/tests/backtrace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
#![cfg(target_vendor = "apple")] // The test is very Apple centric
use std::ffi::c_void;

use objc2::{define_class, extern_methods, msg_send, ClassType};
use objc2_foundation::{NSException, NSObject};

#[allow(dead_code)]
fn merge_objc_symbols(exc: &NSException) -> Vec<String> {
// Objective-C and Rust have different mangling schemes, and don't really
// understand each other. So we use both `NSException`'s backtrace, and
// `backtrace`' resolving mechanism, to figure out the full list of
// demangled symbols.
let mut demangled_symbols = vec![];

let nssymbols = unsafe { exc.callStackSymbols() };
let return_addrs = unsafe { exc.callStackReturnAddresses() };

for (nssymbol, addr) in nssymbols.into_iter().zip(return_addrs) {
let addr = addr.as_usize() as *mut c_void;
let mut call_count = 0;
backtrace::resolve(addr, |symbol| {
if let Some(name) = symbol.name() {
demangled_symbols.push(name.to_string());
} else {
demangled_symbols.push(format!("{nssymbol} ({:?})", symbol.addr()));
}
call_count += 1;
});
if call_count == 0 {
demangled_symbols.push(nssymbol.to_string());
}
}

demangled_symbols
}

#[test]
#[cfg(feature = "exception")]
#[cfg_attr(feature = "catch-all", ignore = "catch-all interferes with our catch")]
fn array_exception() {
use objc2::rc::Retained;
use objc2_foundation::NSArray;

#[no_mangle]
fn array_exception_via_msg_send() {
let arr = NSArray::<NSObject>::new();
let _: Retained<NSObject> = unsafe { msg_send![&arr, objectAtIndex: 0usize] };
}
let expected_msg_send = [
"__exceptionPreprocess",
"objc_exception_throw",
"CFArrayApply",
"<(A,) as objc2::encode::EncodeArguments>::__invoke",
"objc2::runtime::message_receiver::msg_send_primitive::send",
"objc2::runtime::message_receiver::MessageReceiver::send_message",
"<MethodFamily as objc2::__macro_helpers::msg_send_retained::MsgSend<Receiver,Return>>::send_message",
"array_exception_via_msg_send",
];

#[no_mangle]
fn array_exception_via_extern_methods() {
let arr = NSArray::<NSObject>::new();
let _ = arr.objectAtIndex(0);
}
let expected_extern_methods = [
"__exceptionPreprocess",
"objc_exception_throw",
"CFArrayApply",
"<(A,) as objc2::encode::EncodeArguments>::__invoke",
"objc2::runtime::message_receiver::msg_send_primitive::send",
"objc2::runtime::message_receiver::MessageReceiver::send_message",
"<MethodFamily as objc2::__macro_helpers::msg_send_retained::MsgSend<Receiver,Return>>::send_message",
"objc2_foundation::generated::__NSArray::NSArray<ObjectType>::objectAtIndex",
"array_exception_via_extern_methods",
];

for (fnptr, expected) in [
(
array_exception_via_msg_send as fn(),
&expected_msg_send as &[_],
),
(array_exception_via_extern_methods, &expected_extern_methods),
] {
let res = objc2::exception::catch(fnptr);
let exc = res.unwrap_err().unwrap();
let exc = exc.downcast::<NSException>().unwrap();

let symbols = merge_objc_symbols(&exc);

// No debug info available, such as when using `--release`.
if symbols[3] == "__mh_execute_header" {
continue;
}

if symbols.len() < expected.len() {
panic!("did not find enough symbols: {symbols:?}");
}

for (expected, actual) in expected.into_iter().zip(&symbols) {
assert!(
actual.contains(expected),
"{expected:?} must be in {actual:?}:\n{symbols:#?}",
);
}
}
}

define_class!(
#[unsafe(super = NSObject)]
#[name = "Thrower"]
struct Thrower;

unsafe impl Thrower {
#[method(backtrace)]
fn __backtrace() -> *mut c_void {
let backtrace = backtrace::Backtrace::new();
Box::into_raw(Box::new(backtrace)).cast()
}
}
);

extern_methods!(
unsafe impl Thrower {
#[method(backtrace)]
fn backtrace() -> *mut c_void;
}
);

#[test]
#[cfg_attr(feature = "catch-all", ignore = "catch-all changes the backtrace")]
fn capture_backtrace() {
#[no_mangle]
fn rust_backtrace_via_msg_send() -> backtrace::Backtrace {
let ptr: *mut c_void = unsafe { msg_send![Thrower::class(), backtrace] };
*unsafe { Box::from_raw(ptr.cast()) }
}
let expected_msg_send: &[_] = &[
"Backtrace::new",
"Thrower::__backtrace",
"<() as objc2::encode::EncodeArguments>::__invoke",
"objc2::runtime::message_receiver::msg_send_primitive::send",
"objc2::runtime::message_receiver::MessageReceiver::send_message",
"<MethodFamily as objc2::__macro_helpers::msg_send_retained::MsgSend<Receiver,Return>>::send_message",
"rust_backtrace_via_msg_send",
];

#[no_mangle]
fn rust_backtrace_via_extern_methods() -> backtrace::Backtrace {
let ptr = Thrower::backtrace();
*unsafe { Box::from_raw(ptr.cast()) }
}
let expected_extern_methods: &[_] = &[
"Backtrace::new",
"Thrower::__backtrace",
"<() as objc2::encode::EncodeArguments>::__invoke",
"objc2::runtime::message_receiver::msg_send_primitive::send",
"objc2::runtime::message_receiver::MessageReceiver::send_message",
"<MethodFamily as objc2::__macro_helpers::msg_send_retained::MsgSend<Receiver,Return>>::send_message",
"Thrower::backtrace",
"rust_backtrace_via_extern_methods",
];

for (backtrace, expected) in [
(rust_backtrace_via_msg_send(), expected_msg_send),
(rust_backtrace_via_extern_methods(), expected_extern_methods),
] {
let symbols: Vec<_> = backtrace
.frames()
.into_iter()
.flat_map(|frame| frame.symbols())
.map(|symbol| {
symbol
.name()
.map(|name| name.to_string())
.unwrap_or_default()
})
.skip_while(|name| !name.contains("Backtrace::new"))
.collect();

// No debug info available, such as when using `--release`.
if backtrace.frames()[0].symbols()[0]
.name()
.unwrap()
.to_string()
== "__mh_execute_header"
{
continue;
}

if symbols.len() < expected.len() {
panic!("did not find enough symbols: {backtrace:?}");
}

for (expected, actual) in expected.into_iter().zip(&symbols) {
assert!(
actual.contains(expected),
"{expected:?} must be in {actual:?}:\n{symbols:#?}",
);
}
}
}

0 comments on commit 2d42180

Please sign in to comment.