uniffi_core/ffi/
rustcalls.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5//! # Low-level support for calling rust functions
6//!
7//! This module helps the scaffolding code make calls to rust functions and pass back the result to the FFI bindings code.
8//!
9//! It handles:
10//!    - Catching panics
11//!    - Adapting the result of `Return::lower_return()` into either a return value or an
12//!      exception
13
14use crate::{FfiDefault, Lower, RustBuffer, UniFfiTag};
15use std::mem::ManuallyDrop;
16use std::panic;
17
18/// Represents the success/error of a rust call
19///
20/// ## Usage
21///
22/// - The consumer code creates a [RustCallStatus] with an empty [RustBuffer] and
23///   [RustCallStatusCode::Success] (0) as the status code
24/// - A pointer to this object is passed to the rust FFI function.  This is an
25///   "out parameter" which will be updated with any error that occurred during the function's
26///   execution.
27/// - After the call, if `code` is [RustCallStatusCode::Error] or [RustCallStatusCode::UnexpectedError]
28///   then `error_buf` will be updated to contain a serialized error object.   See
29///   [RustCallStatusCode] for what gets serialized. The consumer is responsible for freeing `error_buf`.
30///
31/// ## Layout/fields
32///
33/// The layout of this struct is important since consumers on the other side of the FFI need to
34/// construct it.  If this were a C struct, it would look like:
35///
36/// ```c,no_run
37/// struct RustCallStatus {
38///     int8_t code;
39///     RustBuffer error_buf;
40/// };
41/// ```
42#[repr(C)]
43pub struct RustCallStatus {
44    pub code: RustCallStatusCode,
45    // error_buf is owned by the foreign side.
46    // - Whatever we are passed, we must never free. This however implies we must be passed
47    //   an empty `RustBuffer` otherwise it would leak when we replace it with our own.
48    // - On error we will set it to a `RustBuffer` we expect the foreign side to free.
49    // We assume initialization, which means we can use `ManuallyDrop` instead of
50    // `MaybeUninit`, which avoids unsafe code and clarifies ownership.
51    // We must take care to not set this twice to avoid leaking the first `RustBuffer`.
52    pub error_buf: ManuallyDrop<RustBuffer>,
53}
54
55impl Default for RustCallStatus {
56    fn default() -> Self {
57        Self {
58            code: RustCallStatusCode::Success,
59            error_buf: Default::default(),
60        }
61    }
62}
63
64impl RustCallStatus {
65    pub fn cancelled() -> Self {
66        Self {
67            code: RustCallStatusCode::Cancelled,
68            error_buf: Default::default(),
69        }
70    }
71
72    pub fn error(message: impl Into<String>) -> Self {
73        Self {
74            code: RustCallStatusCode::UnexpectedError,
75            error_buf: ManuallyDrop::new(<String as Lower<UniFfiTag>>::lower(message.into())),
76        }
77    }
78}
79
80/// Result of a FFI call to a Rust function
81/// Value is signed to avoid Kotlin's experimental unsigned types.
82#[repr(i8)]
83#[derive(Debug, PartialEq, Eq)]
84pub enum RustCallStatusCode {
85    /// Successful call.
86    Success = 0,
87    /// Expected error, corresponding to the `Result::Err` variant.  [RustCallStatus::error_buf]
88    /// will contain the serialized error.
89    Error = 1,
90    /// Unexpected error.  [RustCallStatus::error_buf] will contain a serialized message string
91    UnexpectedError = 2,
92    /// Async function cancelled.  [RustCallStatus::error_buf] will be empty and does not need to
93    /// be freed.
94    ///
95    /// This is only returned for async functions and only if the bindings code uses the
96    /// [rust_future_cancel] call.
97    Cancelled = 3,
98}
99
100impl TryFrom<i8> for RustCallStatusCode {
101    type Error = i8;
102
103    fn try_from(value: i8) -> Result<Self, i8> {
104        match value {
105            0 => Ok(Self::Success),
106            1 => Ok(Self::Error),
107            2 => Ok(Self::UnexpectedError),
108            3 => Ok(Self::Cancelled),
109            n => Err(n),
110        }
111    }
112}
113
114/// Error type for Rust scaffolding calls
115///
116/// This enum represents the fact that there are two ways for a scaffolding call to fail:
117/// - Expected errors (the Rust function returned an `Err` value).
118/// - Unexpected errors (there was a failure calling the Rust function, for example the failure to
119///   lift the arguments).
120pub enum RustCallError {
121    /// The Rust function returned an `Err` value.
122    ///
123    /// The associated value is the serialized `Err` value.
124    Error(RustBuffer),
125    /// There was a failure to call the Rust function, for example a failure to lift the arguments.
126    ///
127    /// The associated value is a message string for the error.
128    InternalError(String),
129}
130
131/// Error when trying to lift arguments to pass to the scaffolding call
132pub struct LiftArgsError {
133    pub arg_name: &'static str,
134    pub error: anyhow::Error,
135}
136
137impl LiftArgsError {
138    pub fn to_internal_error(self) -> RustCallError {
139        let LiftArgsError { arg_name, error } = self;
140
141        RustCallError::InternalError(format!("Failed to convert arg '{arg_name}':\n{error:?}"))
142    }
143}
144
145/// Handle a scaffolding calls
146///
147/// `callback` is responsible for making the actual Rust call and returning a special result type:
148///   - For successful calls, return `Ok(value)`
149///   - For errors that should be translated into thrown exceptions in the foreign code, serialize
150///     the error into a `RustBuffer`, then return `Ok(buf)`
151///   - The success type, must implement `FfiDefault`.
152///   - `Return::lower_return` returns `Result<>` types that meet the above criteria>
153/// - If the function returns a `Ok` value it will be unwrapped and returned
154/// - If the function returns a `Err` value:
155///     - `out_status.code` will be set to [RustCallStatusCode::Error].
156///     - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing the error.  The calling
157///       code is responsible for freeing the `RustBuffer`
158///     - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value
159/// - If the function panics:
160///     - `out_status.code` will be set to `CALL_PANIC`
161///     - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing a
162///       serialized error message.  The calling code is responsible for freeing the `RustBuffer`
163///     - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value
164pub fn rust_call<F, R>(out_status: &mut RustCallStatus, callback: F) -> R
165where
166    F: panic::UnwindSafe + FnOnce() -> Result<R, RustCallError>,
167    R: FfiDefault,
168{
169    rust_call_with_out_status(out_status, callback).unwrap_or_else(R::ffi_default)
170}
171
172/// Result of making a Rust call
173///
174/// The `Ok` side stores successful call results
175/// The `Err` side stores error results, both for `Err` returns and for unexpected errors.
176/// Errors are represented by a `RustCallStatus` which is easy to return across the FFI.
177pub type RustCallResult<T> = Result<T, RustCallStatus>;
178
179/// Try making a Rust call
180///
181/// If the call succeeds this returns Ok(v) with the result.
182/// If the call fails (including Err results), this returns Err(RustCallStatus).
183pub(crate) fn try_rust_call<F, R>(callback: F) -> Result<R, RustCallStatus>
184where
185    F: panic::UnwindSafe + FnOnce() -> Result<R, RustCallError>,
186{
187    let mut out_status = RustCallStatus::default();
188    match rust_call_with_out_status(&mut out_status, callback) {
189        Some(r) => Ok(r),
190        None => Err(out_status),
191    }
192}
193
194/// Make a Rust call and update `RustCallStatus` based on the result.
195///
196/// If the call succeeds this returns Some(v) and doesn't touch out_status
197/// If the call fails (including Err results), this returns None and updates out_status
198///
199/// This contains the shared code between `rust_call` and `rustfuture::do_wake`.
200pub(crate) fn rust_call_with_out_status<F, R>(
201    out_status: &mut RustCallStatus,
202    callback: F,
203) -> Option<R>
204where
205    F: panic::UnwindSafe + FnOnce() -> Result<R, RustCallError>,
206{
207    let result = panic::catch_unwind(callback);
208    match result {
209        // Happy path.  Note: no need to update out_status in this case because the calling code
210        // initializes it to [RustCallStatusCode::Success]
211        Ok(Ok(v)) => Some(v),
212        // Callback returned an Err.
213        Ok(Err(RustCallError::Error(buf))) => {
214            out_status.code = RustCallStatusCode::Error;
215            *out_status.error_buf = buf;
216            None
217        }
218        Ok(Err(RustCallError::InternalError(msg))) => {
219            out_status.code = RustCallStatusCode::UnexpectedError;
220            *out_status.error_buf = <String as Lower<UniFfiTag>>::lower(msg);
221            None
222        }
223        // Callback panicked
224        Err(cause) => {
225            out_status.code = RustCallStatusCode::UnexpectedError;
226            // Try to coerce the cause into a RustBuffer containing a String.  Since this code can
227            // panic, we need to use a second catch_unwind().
228            let message_result = panic::catch_unwind(panic::AssertUnwindSafe(move || {
229                // The documentation suggests that it will *usually* be a str or String.
230                let message = if let Some(s) = cause.downcast_ref::<&'static str>() {
231                    (*s).to_string()
232                } else if let Some(s) = cause.downcast_ref::<String>() {
233                    s.clone()
234                } else {
235                    "Unknown panic!".to_string()
236                };
237                trace!("Caught a panic calling rust code: {:?}", message);
238                <String as Lower<UniFfiTag>>::lower(message)
239            }));
240            if let Ok(buf) = message_result {
241                // If this was ever set twice we'd leak the old value - but because this is the only
242                // place where it is set, and this is only called once, no leaks should exist in practice.
243                *out_status.error_buf = buf;
244            }
245            // Ignore the error case.  We've done all that we can at this point.  In the bindings
246            // code, we handle this by checking if `error_buf` still has an empty `RustBuffer` and
247            // using a generic message.
248            None
249        }
250    }
251}
252
253#[cfg(test)]
254mod test {
255    use super::*;
256    use crate::{test_util::TestError, Lift, LowerReturn};
257    use anyhow::anyhow;
258
259    #[test]
260    fn test_rust_call() {
261        // Successful call
262        let mut status = RustCallStatus::default();
263        let return_value = rust_call(&mut status, || {
264            <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(Ok(100))
265        });
266
267        assert_eq!(status.code, RustCallStatusCode::Success);
268        assert_eq!(return_value, 100);
269
270        // Successful call that returns an Err value
271        let mut status = RustCallStatus::default();
272        rust_call(&mut status, || {
273            <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(Err(TestError(
274                "Error".into(),
275            )))
276        });
277        assert_eq!(status.code, RustCallStatusCode::Error);
278        assert_eq!(
279            <TestError as Lift<UniFfiTag>>::try_lift(ManuallyDrop::into_inner(status.error_buf))
280                .unwrap(),
281            TestError("Error".to_owned())
282        );
283
284        // Internal error while trying to make the call
285        let mut status = RustCallStatus::default();
286        rust_call(&mut status, || {
287            <Result<i8, TestError> as LowerReturn<UniFfiTag>>::handle_failed_lift(LiftArgsError {
288                arg_name: "foo",
289                error: anyhow!("invalid handle"),
290            })
291        });
292        assert_eq!(status.code, RustCallStatusCode::UnexpectedError);
293        assert_eq!(
294            <String as Lift<UniFfiTag>>::try_lift(ManuallyDrop::into_inner(status.error_buf))
295                .unwrap(),
296            "Failed to convert arg 'foo':\ninvalid handle"
297        );
298
299        // Panic inside the call
300        let mut status = RustCallStatus::default();
301        rust_call(&mut status, || -> Result<i8, RustCallError> {
302            panic!("I crashed")
303        });
304        assert_eq!(status.code, RustCallStatusCode::UnexpectedError);
305        assert_eq!(
306            <String as Lift<UniFfiTag>>::try_lift(ManuallyDrop::into_inner(status.error_buf))
307                .unwrap(),
308            "I crashed"
309        );
310    }
311}