use crate::{FfiDefault, Lower, RustBuffer, UniFfiTag};
use std::mem::ManuallyDrop;
use std::panic;
#[repr(C)]
pub struct RustCallStatus {
pub code: RustCallStatusCode,
pub error_buf: ManuallyDrop<RustBuffer>,
}
impl Default for RustCallStatus {
fn default() -> Self {
Self {
code: RustCallStatusCode::Success,
error_buf: Default::default(),
}
}
}
impl RustCallStatus {
pub fn cancelled() -> Self {
Self {
code: RustCallStatusCode::Cancelled,
error_buf: Default::default(),
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
code: RustCallStatusCode::UnexpectedError,
error_buf: ManuallyDrop::new(<String as Lower<UniFfiTag>>::lower(message.into())),
}
}
}
#[repr(i8)]
#[derive(Debug, PartialEq, Eq)]
pub enum RustCallStatusCode {
Success = 0,
Error = 1,
UnexpectedError = 2,
Cancelled = 3,
}
impl TryFrom<i8> for RustCallStatusCode {
type Error = i8;
fn try_from(value: i8) -> Result<Self, i8> {
match value {
0 => Ok(Self::Success),
1 => Ok(Self::Error),
2 => Ok(Self::UnexpectedError),
3 => Ok(Self::Cancelled),
n => Err(n),
}
}
}
pub enum RustCallError {
Error(RustBuffer),
InternalError(String),
}
pub struct LiftArgsError {
pub arg_name: &'static str,
pub error: anyhow::Error,
}
impl LiftArgsError {
pub fn to_internal_error(self) -> RustCallError {
let LiftArgsError { arg_name, error } = self;
RustCallError::InternalError(format!("Failed to convert arg '{arg_name}':\n{error:?}"))
}
}
pub fn rust_call<F, R>(out_status: &mut RustCallStatus, callback: F) -> R
where
F: panic::UnwindSafe + FnOnce() -> Result<R, RustCallError>,
R: FfiDefault,
{
rust_call_with_out_status(out_status, callback).unwrap_or_else(R::ffi_default)
}
pub type RustCallResult<T> = Result<T, RustCallStatus>;
pub(crate) fn try_rust_call<F, R>(callback: F) -> Result<R, RustCallStatus>
where
F: panic::UnwindSafe + FnOnce() -> Result<R, RustCallError>,
{
let mut out_status = RustCallStatus::default();
match rust_call_with_out_status(&mut out_status, callback) {
Some(r) => Ok(r),
None => Err(out_status),
}
}
pub(crate) fn rust_call_with_out_status<F, R>(
out_status: &mut RustCallStatus,
callback: F,
) -> Option<R>
where
F: panic::UnwindSafe + FnOnce() -> Result<R, RustCallError>,
{
let result = panic::catch_unwind(callback);
match result {
Ok(Ok(v)) => Some(v),
Ok(Err(RustCallError::Error(buf))) => {
out_status.code = RustCallStatusCode::Error;
*out_status.error_buf = buf;
None
}
Ok(Err(RustCallError::InternalError(msg))) => {
out_status.code = RustCallStatusCode::UnexpectedError;
*out_status.error_buf = <String as Lower<UniFfiTag>>::lower(msg);
None
}
Err(cause) => {
out_status.code = RustCallStatusCode::UnexpectedError;
let message_result = panic::catch_unwind(panic::AssertUnwindSafe(move || {
let message = if let Some(s) = cause.downcast_ref::<&'static str>() {
(*s).to_string()
} else if let Some(s) = cause.downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic!".to_string()
};
trace!("Caught a panic calling rust code: {:?}", message);
<String as Lower<UniFfiTag>>::lower(message)
}));
if let Ok(buf) = message_result {
*out_status.error_buf = buf;
}
None
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{test_util::TestError, Lift, LowerReturn};
use anyhow::anyhow;
#[test]
fn test_rust_call() {
let mut status = RustCallStatus::default();
let return_value = rust_call(&mut status, || {
<Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(Ok(100))
});
assert_eq!(status.code, RustCallStatusCode::Success);
assert_eq!(return_value, 100);
let mut status = RustCallStatus::default();
rust_call(&mut status, || {
<Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(Err(TestError(
"Error".into(),
)))
});
assert_eq!(status.code, RustCallStatusCode::Error);
assert_eq!(
<TestError as Lift<UniFfiTag>>::try_lift(ManuallyDrop::into_inner(status.error_buf))
.unwrap(),
TestError("Error".to_owned())
);
let mut status = RustCallStatus::default();
rust_call(&mut status, || {
<Result<i8, TestError> as LowerReturn<UniFfiTag>>::handle_failed_lift(LiftArgsError {
arg_name: "foo",
error: anyhow!("invalid handle"),
})
});
assert_eq!(status.code, RustCallStatusCode::UnexpectedError);
assert_eq!(
<String as Lift<UniFfiTag>>::try_lift(ManuallyDrop::into_inner(status.error_buf))
.unwrap(),
"Failed to convert arg 'foo':\ninvalid handle"
);
let mut status = RustCallStatus::default();
rust_call(&mut status, || -> Result<i8, RustCallError> {
panic!("I crashed")
});
assert_eq!(status.code, RustCallStatusCode::UnexpectedError);
assert_eq!(
<String as Lift<UniFfiTag>>::try_lift(ManuallyDrop::into_inner(status.error_buf))
.unwrap(),
"I crashed"
);
}
}