Skip to content
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

Pass Rust stack traces to Dart side during a panic #1586

Merged
merged 29 commits into from
Jan 3, 2024
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
18 changes: 18 additions & 0 deletions frb_example/pure_dart/test/api/exception_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,22 @@ Future<void> main({bool skipRustLibInit = false}) async {
});
});
});

group('has backtraces', skip: kIsWeb, () {
final matcher =
anyOf(contains('.rs'), contains('::'), contains('<unknown>'));

test('when error (Result::Err)', () async {
await expectLater(
() async => funcReturnErrorTwinNormal(),
throwsA(isA<AnyhowException>()
.having((x) => x.message, 'message', matcher)));
});

test('when panic', () async {
await expectRustPanic(
() async => funcTypeFalliblePanicTwinNormal(), 'TwinNormal',
messageMatcherOnNative: matcher);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,22 @@ Future<void> main({bool skipRustLibInit = false}) async {
});
});
});

group('has backtraces', skip: kIsWeb, () {
final matcher =
anyOf(contains('.rs'), contains('::'), contains('<unknown>'));

test('when error (Result::Err)', () async {
await expectLater(
() async => funcReturnErrorTwinRustAsyncSse(),
throwsA(isA<AnyhowException>()
.having((x) => x.message, 'message', matcher)));
});

test('when panic', () async {
await expectRustPanic(() async => funcTypeFalliblePanicTwinRustAsyncSse(),
'TwinRustAsyncSse',
messageMatcherOnNative: matcher);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,22 @@ Future<void> main({bool skipRustLibInit = false}) async {
});
});
});

group('has backtraces', skip: kIsWeb, () {
final matcher =
anyOf(contains('.rs'), contains('::'), contains('<unknown>'));

test('when error (Result::Err)', () async {
await expectLater(
() async => funcReturnErrorTwinRustAsync(),
throwsA(isA<AnyhowException>()
.having((x) => x.message, 'message', matcher)));
});

test('when panic', () async {
await expectRustPanic(
() async => funcTypeFalliblePanicTwinRustAsync(), 'TwinRustAsync',
messageMatcherOnNative: matcher);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,22 @@ Future<void> main({bool skipRustLibInit = false}) async {
});
});
});

group('has backtraces', skip: kIsWeb, () {
final matcher =
anyOf(contains('.rs'), contains('::'), contains('<unknown>'));

test('when error (Result::Err)', () async {
await expectLater(
() async => funcReturnErrorTwinSse(),
throwsA(isA<AnyhowException>()
.having((x) => x.message, 'message', matcher)));
});

test('when panic', () async {
await expectRustPanic(
() async => funcTypeFalliblePanicTwinSse(), 'TwinSse',
messageMatcherOnNative: matcher);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,22 @@ Future<void> main({bool skipRustLibInit = false}) async {
});
});
});

group('has backtraces', skip: kIsWeb, () {
final matcher =
anyOf(contains('.rs'), contains('::'), contains('<unknown>'));

test('when error (Result::Err)', () async {
await expectLater(
() async => funcReturnErrorTwinSyncSse(),
throwsA(isA<AnyhowException>()
.having((x) => x.message, 'message', matcher)));
});

test('when panic', () async {
await expectRustPanic(
() async => funcTypeFalliblePanicTwinSyncSse(), 'TwinSyncSse',
messageMatcherOnNative: matcher);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,22 @@ Future<void> main({bool skipRustLibInit = false}) async {
});
});
});

group('has backtraces', skip: kIsWeb, () {
final matcher =
anyOf(contains('.rs'), contains('::'), contains('<unknown>'));

test('when error (Result::Err)', () async {
await expectLater(
() async => funcReturnErrorTwinSync(),
throwsA(isA<AnyhowException>()
.having((x) => x.message, 'message', matcher)));
});

test('when panic', () async {
await expectRustPanic(
() async => funcTypeFalliblePanicTwinSync(), 'TwinSync',
messageMatcherOnNative: matcher);
});
});
}
18 changes: 13 additions & 5 deletions frb_example/pure_dart/test/test_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,20 @@ bool get releaseMode {
return ans;
}

Future<void> expectRustPanic(FutureOr<void> Function() body, String mode,
{String? messageOnNative}) async {
Future<void> expectRustPanic(
FutureOr<void> Function() body,
String mode, {
String? messageOnNative,
Matcher? messageMatcherOnNative,
}) async {
if (messageOnNative != null) {
assert(messageMatcherOnNative == null);
messageMatcherOnNative = startsWith(messageOnNative);
}

var inner = isA<PanicException>();
if (!kIsWeb && messageOnNative != null) {
inner =
inner.having((x) => x.message, 'message', startsWith(messageOnNative));
if (!kIsWeb && messageMatcherOnNative != null) {
inner = inner.having((x) => x.message, 'message', messageMatcherOnNative);
}
await expectRustPanicRaw(body, mode, throwsA(inner));
}
Expand Down
3 changes: 2 additions & 1 deletion frb_rust/src/codec/cst.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::{BaseCodec, Rust2DartMessageTrait};
use crate::platform_types::DartAbi;
use backtrace::Backtrace;
use std::any::Any;

// frb-coverage:ignore-start
Expand All @@ -11,7 +12,7 @@ impl BaseCodec for CstCodec {
type Message = Rust2DartMessageCst;

// frb-coverage:ignore-start
fn encode_panic(_error: &Box<dyn Any + Send>) -> Self::Message {
fn encode_panic(_error: &Box<dyn Any + Send>, _backtrace: &Option<Backtrace>) -> Self::Message {
unreachable!()
}

Expand Down
5 changes: 3 additions & 2 deletions frb_rust/src/codec/dco.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::handler::error::error_to_string;
use crate::misc::into_into_dart::IntoIntoDart;
use crate::platform_types::{DartAbi, WireSyncRust2DartDco};
use crate::rust2dart::action::Rust2DartAction;
use backtrace::Backtrace;
use std::any::Any;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand All @@ -12,8 +13,8 @@ pub struct DcoCodec;
impl BaseCodec for DcoCodec {
type Message = Rust2DartMessageDco;

fn encode_panic(error: &Box<dyn Any + Send>) -> Self::Message {
Self::encode(Rust2DartAction::Panic, error_to_string(error))
fn encode_panic(error: &Box<dyn Any + Send>, backtrace: &Option<Backtrace>) -> Self::Message {
Self::encode(Rust2DartAction::Panic, error_to_string(error, backtrace))
}

fn encode_close_stream() -> Self::Message {
Expand Down
3 changes: 2 additions & 1 deletion frb_rust/src/codec/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::platform_types::DartAbi;
use backtrace::Backtrace;
use std::any::Any;

pub(crate) mod cst;
Expand All @@ -8,7 +9,7 @@ pub(crate) mod sse;
pub trait BaseCodec: Clone + Copy + Send {
type Message: Rust2DartMessageTrait;

fn encode_panic(error: &Box<dyn Any + Send>) -> Self::Message;
fn encode_panic(error: &Box<dyn Any + Send>, backtrace: &Option<Backtrace>) -> Self::Message;

fn encode_close_stream() -> Self::Message;
}
Expand Down
5 changes: 3 additions & 2 deletions frb_rust/src/codec/sse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::generalized_isolate::IntoDart;
use crate::handler::error::error_to_string;
use crate::platform_types::{DartAbi, PlatformGeneralizedUint8ListPtr, WireSyncRust2DartSse};
use crate::rust2dart::action::Rust2DartAction;
use backtrace::Backtrace;
use byteorder::NativeEndian;
use byteorder::WriteBytesExt;
use std::any::Any;
Expand All @@ -15,8 +16,8 @@ pub struct SseCodec;
impl BaseCodec for SseCodec {
type Message = Rust2DartMessageSse;

fn encode_panic(error: &Box<dyn Any + Send>) -> Self::Message {
let msg = error_to_string(error);
fn encode_panic(error: &Box<dyn Any + Send>, backtrace: &Option<Backtrace>) -> Self::Message {
let msg = error_to_string(error, backtrace);
Self::encode(Rust2DartAction::Panic, |serializer| {
// NOTE roughly copied from the auto-generated serialization of String
let bytes = msg.into_bytes();
Expand Down
17 changes: 13 additions & 4 deletions frb_rust/src/handler/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use backtrace::Backtrace;
use std::any::Any;

/// Errors that occur from normal code execution.
Expand All @@ -13,20 +14,28 @@ impl Error {
pub fn message(&self) -> String {
match self {
Error::CustomError => "CustomError".to_string(),
Error::Panic(panic_err) => error_to_string(panic_err),
Error::Panic(panic_err) => error_to_string(panic_err, &None),
}
}
}

pub(crate) fn error_to_string(panic_err: &Box<dyn Any + Send>) -> String {
match panic_err.downcast_ref::<&'static str>() {
pub(crate) fn error_to_string(
panic_err: &Box<dyn Any + Send>,
backtrace: &Option<Backtrace>,
) -> String {
let err_string = match panic_err.downcast_ref::<&'static str>() {
Some(s) => *s,
None => match panic_err.downcast_ref::<String>() {
Some(s) => &s[..],
None => "Box<dyn Any>",
},
}
.to_string()
.to_string();
let backtrace_string = backtrace
.as_ref()
.map(|b| format!("{:?}", b))
.unwrap_or_default();
err_string + &backtrace_string
}

#[cfg(test)]
Expand Down
8 changes: 4 additions & 4 deletions frb_rust/src/handler/implementation/error_listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ use crate::codec::Rust2DartMessageTrait;
use crate::generalized_isolate::Channel;
use crate::handler::error::Error;
use crate::handler::error_listener::ErrorListener;
use crate::misc::panic_backtrace::CatchUnwindWithBacktrace;
use crate::platform_types::MessagePort;
use crate::rust2dart::sender::Rust2DartSender;
use std::any::Any;

/// The default one.
#[derive(Clone, Copy)]
Expand All @@ -20,10 +20,10 @@ impl ErrorListener for NoOpErrorListener {
pub(crate) fn handle_non_sync_panic_error<Rust2DartCodec: BaseCodec>(
error_listener: impl ErrorListener,
port: MessagePort,
error: Box<dyn Any + Send>,
error: CatchUnwindWithBacktrace,
) {
let message = Rust2DartCodec::encode_panic(&error).into_dart_abi();
error_listener.on_error(Error::Panic(error));
let message = Rust2DartCodec::encode_panic(&error.err, &error.backtrace).into_dart_abi();
error_listener.on_error(Error::Panic(error.err));
Rust2DartSender::new(Channel::new(port))
.send(message)
.unwrap();
Expand Down
9 changes: 5 additions & 4 deletions frb_rust/src/handler/implementation/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::handler::error_listener::ErrorListener;
use crate::handler::executor::Executor;
use crate::handler::handler::{FfiCallMode, TaskContext, TaskInfo, TaskRetFutTrait};
use crate::handler::implementation::error_listener::handle_non_sync_panic_error;
use crate::misc::panic_backtrace::{CatchUnwindWithBacktrace, PanicBacktrace};
use crate::platform_types::MessagePort;
use crate::rust2dart::context::TaskRust2DartContext;
use crate::rust2dart::sender::Rust2DartSender;
Expand All @@ -14,7 +15,6 @@ use crate::thread_pool::BaseThreadPool;
use crate::transfer;
use futures::FutureExt;
use std::future::Future;
use std::panic;
use std::panic::AssertUnwindSafe;

/// The default executor used.
Expand Down Expand Up @@ -62,7 +62,7 @@ impl<EL: ErrorListener + Sync, TP: BaseThreadPool, AR: BaseAsyncRuntime> Executo
self.thread_pool.execute(transfer!(|port: MessagePort| {
#[allow(clippy::clone_on_copy)]
let port2 = port.clone();
let thread_result = panic::catch_unwind(AssertUnwindSafe(|| {
let thread_result = PanicBacktrace::catch_unwind(AssertUnwindSafe(|| {
#[allow(clippy::clone_on_copy)]
let sender = Rust2DartSender::new(Channel::new(port2.clone()));
let task_context = TaskContext::new(TaskRust2DartContext::new(sender.clone()));
Expand Down Expand Up @@ -129,8 +129,9 @@ impl<EL: ErrorListener + Sync, TP: BaseThreadPool, AR: BaseAsyncRuntime> Executo
.catch_unwind()
.await;

if let Err(error) = async_result {
handle_non_sync_panic_error::<Rust2DartCodec>(el, port, error);
if let Err(err) = async_result {
let err = CatchUnwindWithBacktrace::new(err, PanicBacktrace::take_last());
handle_non_sync_panic_error::<Rust2DartCodec>(el, port, err);
}
});
}
Expand Down
9 changes: 5 additions & 4 deletions frb_rust/src/handler/implementation/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::handler::implementation::error_listener::{
handle_non_sync_panic_error, NoOpErrorListener,
};
use crate::handler::implementation::executor::SimpleExecutor;
use crate::misc::panic_backtrace::PanicBacktrace;
use crate::platform_types::DartAbi;
use crate::rust_async::SimpleAsyncRuntime;
use crate::thread_pool::BaseThreadPool;
Expand Down Expand Up @@ -90,13 +91,13 @@ impl<E: Executor, EL: ErrorListener> Handler for SimpleHandler<E, EL> {
// NOTE This extra [catch_unwind] **SHOULD** be put outside **ALL** code!
// For reason, see comments in [wrap]
panic::catch_unwind(AssertUnwindSafe(move || {
let catch_unwind_result = panic::catch_unwind(AssertUnwindSafe(move || {
let catch_unwind_result = PanicBacktrace::catch_unwind(AssertUnwindSafe(move || {
(self.executor).execute_sync::<Rust2DartCodec, _>(task_info, sync_task)
}));
catch_unwind_result
.unwrap_or_else(|error| {
let message = Rust2DartCodec::encode_panic(&error);
self.error_listener.on_error(Error::Panic(error));
let message = Rust2DartCodec::encode_panic(&error.err, &error.backtrace);
self.error_listener.on_error(Error::Panic(error.err));
message
})
.into_raw_wire_sync()
Expand Down Expand Up @@ -164,7 +165,7 @@ impl<E: Executor, EL: ErrorListener> SimpleHandler<E, EL> {
// ref https://doc.rust-lang.org/nomicon/unwinding.html
let _ = panic::catch_unwind(AssertUnwindSafe(move || {
let task_info2 = task_info.clone();
if let Err(error) = panic::catch_unwind(AssertUnwindSafe(move || {
if let Err(error) = PanicBacktrace::catch_unwind(AssertUnwindSafe(move || {
let task = prepare();
execute(task_info2, task);
})) {
Expand Down
1 change: 1 addition & 0 deletions frb_rust/src/misc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub(crate) mod dart_dynamic;
pub(crate) mod into_into_dart;
pub(crate) mod logs;
pub(crate) mod manual_impl;
pub(crate) mod panic_backtrace;
pub(crate) mod rust_arc;
pub(crate) mod rust_auto_opaque;
#[cfg(feature = "user-utils")]
Expand Down
Loading
Loading