uniffi_core/ffi/rustcalls.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
//! # Low-level support for calling rust functions
//!
//! This module helps the scaffolding code make calls to rust functions and pass back the result to the FFI bindings code.
//!
//! It handles:
//! - Catching panics
//! - Adapting the result of `Return::lower_return()` into either a return value or an
//! exception
use crate::{FfiDefault, Lower, RustBuffer, UniFfiTag};
use std::mem::MaybeUninit;
use std::panic;
/// Represents the success/error of a rust call
///
/// ## Usage
///
/// - The consumer code creates a [RustCallStatus] with an empty [RustBuffer] and
/// [RustCallStatusCode::Success] (0) as the status code
/// - A pointer to this object is passed to the rust FFI function. This is an
/// "out parameter" which will be updated with any error that occurred during the function's
/// execution.
/// - After the call, if `code` is [RustCallStatusCode::Error] or [RustCallStatusCode::UnexpectedError]
/// then `error_buf` will be updated to contain a serialized error object. See
/// [RustCallStatusCode] for what gets serialized. The consumer is responsible for freeing `error_buf`.
///
/// ## Layout/fields
///
/// The layout of this struct is important since consumers on the other side of the FFI need to
/// construct it. If this were a C struct, it would look like:
///
/// ```c,no_run
/// struct RustCallStatus {
/// int8_t code;
/// RustBuffer error_buf;
/// };
/// ```
#[repr(C)]
pub struct RustCallStatus {
pub code: RustCallStatusCode,
// code is signed because unsigned types are experimental in Kotlin
pub error_buf: MaybeUninit<RustBuffer>,
// error_buf is MaybeUninit to avoid dropping the value that the consumer code sends in:
// - Consumers should send in a zeroed out RustBuffer. In this case dropping is a no-op and
// avoiding the drop is a small optimization.
// - If consumers pass in invalid data, then we should avoid trying to drop it. In
// particular, we don't want to try to free any data the consumer has allocated.
//
// `MaybeUninit` requires unsafe code, since we are preventing rust from dropping the value.
// To use this safely we need to make sure that no code paths set this twice, since that will
// leak the first `RustBuffer`.
}
impl RustCallStatus {
pub fn cancelled() -> Self {
Self {
code: RustCallStatusCode::Cancelled,
error_buf: MaybeUninit::new(RustBuffer::new()),
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
code: RustCallStatusCode::UnexpectedError,
error_buf: MaybeUninit::new(<String as Lower<UniFfiTag>>::lower(message.into())),
}
}
}
impl Default for RustCallStatus {
fn default() -> Self {
Self {
code: RustCallStatusCode::Success,
error_buf: MaybeUninit::uninit(),
}
}
}
/// Result of a FFI call to a Rust function
#[repr(i8)]
#[derive(Debug, PartialEq, Eq)]
pub enum RustCallStatusCode {
/// Successful call.
Success = 0,
/// Expected error, corresponding to the `Result::Err` variant. [RustCallStatus::error_buf]
/// will contain the serialized error.
Error = 1,
/// Unexpected error. [RustCallStatus::error_buf] will contain a serialized message string
UnexpectedError = 2,
/// Async function cancelled. [RustCallStatus::error_buf] will be empty and does not need to
/// be freed.
///
/// This is only returned for async functions and only if the bindings code uses the
/// [rust_future_cancel] call.
Cancelled = 3,
}
/// Handle a scaffolding calls
///
/// `callback` is responsible for making the actual Rust call and returning a special result type:
/// - For successfull calls, return `Ok(value)`
/// - For errors that should be translated into thrown exceptions in the foreign code, serialize
/// the error into a `RustBuffer`, then return `Ok(buf)`
/// - The success type, must implement `FfiDefault`.
/// - `Return::lower_return` returns `Result<>` types that meet the above criteria>
/// - If the function returns a `Ok` value it will be unwrapped and returned
/// - If the function returns a `Err` value:
/// - `out_status.code` will be set to [RustCallStatusCode::Error].
/// - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing the error. The calling
/// code is responsible for freeing the `RustBuffer`
/// - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value
/// - If the function panics:
/// - `out_status.code` will be set to `CALL_PANIC`
/// - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing a
/// serialized error message. The calling code is responsible for freeing the `RustBuffer`
/// - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value
pub fn rust_call<F, R>(out_status: &mut RustCallStatus, callback: F) -> R
where
F: panic::UnwindSafe + FnOnce() -> Result<R, RustBuffer>,
R: FfiDefault,
{
rust_call_with_out_status(out_status, callback).unwrap_or_else(R::ffi_default)
}
/// Make a Rust call and update `RustCallStatus` based on the result.
///
/// If the call succeeds this returns Some(v) and doesn't touch out_status
/// If the call fails (including Err results), this returns None and updates out_status
///
/// This contains the shared code between `rust_call` and `rustfuture::do_wake`.
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, RustBuffer>,
{
let result = panic::catch_unwind(|| {
crate::panichook::ensure_setup();
callback()
});
match result {
// Happy path. Note: no need to update out_status in this case because the calling code
// initializes it to [RustCallStatusCode::Success]
Ok(Ok(v)) => Some(v),
// Callback returned an Err.
Ok(Err(buf)) => {
out_status.code = RustCallStatusCode::Error;
unsafe {
// Unsafe because we're setting the `MaybeUninit` value, see above for safety
// invariants.
out_status.error_buf.as_mut_ptr().write(buf);
}
None
}
// Callback panicked
Err(cause) => {
out_status.code = RustCallStatusCode::UnexpectedError;
// Try to coerce the cause into a RustBuffer containing a String. Since this code can
// panic, we need to use a second catch_unwind().
let message_result = panic::catch_unwind(panic::AssertUnwindSafe(move || {
// The documentation suggests that it will *usually* be a str or String.
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()
};
log::error!("Caught a panic calling rust code: {:?}", message);
<String as Lower<UniFfiTag>>::lower(message)
}));
if let Ok(buf) = message_result {
unsafe {
// Unsafe because we're setting the `MaybeUninit` value, see above for safety
// invariants.
out_status.error_buf.as_mut_ptr().write(buf);
}
}
// Ignore the error case. We've done all that we can at this point. In the bindings
// code, we handle this by checking if `error_buf` still has an empty `RustBuffer` and
// using a generic message.
None
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{test_util::TestError, Lift, LowerReturn};
fn create_call_status() -> RustCallStatus {
RustCallStatus {
code: RustCallStatusCode::Success,
error_buf: MaybeUninit::new(RustBuffer::new()),
}
}
fn test_callback(a: u8) -> Result<i8, TestError> {
match a {
0 => Ok(100),
1 => Err(TestError("Error".to_owned())),
x => panic!("Unexpected value: {x}"),
}
}
#[test]
fn test_rust_call() {
let mut status = create_call_status();
let return_value = rust_call(&mut status, || {
<Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(test_callback(0))
});
assert_eq!(status.code, RustCallStatusCode::Success);
assert_eq!(return_value, 100);
rust_call(&mut status, || {
<Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(test_callback(1))
});
assert_eq!(status.code, RustCallStatusCode::Error);
unsafe {
assert_eq!(
<TestError as Lift<UniFfiTag>>::try_lift(status.error_buf.assume_init()).unwrap(),
TestError("Error".to_owned())
);
}
let mut status = create_call_status();
rust_call(&mut status, || {
<Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(test_callback(2))
});
assert_eq!(status.code, RustCallStatusCode::UnexpectedError);
unsafe {
assert_eq!(
<String as Lift<UniFfiTag>>::try_lift(status.error_buf.assume_init()).unwrap(),
"Unexpected value: 2"
);
}
}
}