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
137/// Handle a scaffolding calls
138///
139/// `callback` is responsible for making the actual Rust call and returning a special result type:
140///   - For successful calls, return `Ok(value)`
141///   - For errors that should be translated into thrown exceptions in the foreign code, serialize
142///     the error into a `RustBuffer`, then return `Ok(buf)`
143///   - The success type, must implement `FfiDefault`.
144///   - `Return::lower_return` returns `Result<>` types that meet the above criteria>
145/// - If the function returns a `Ok` value it will be unwrapped and returned
146/// - If the function returns a `Err` value:
147///     - `out_status.code` will be set to [RustCallStatusCode::Error].
148///     - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing the error.  The calling
149///       code is responsible for freeing the `RustBuffer`
150///     - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value
151/// - If the function panics:
152///     - `out_status.code` will be set to `CALL_PANIC`
153///     - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing a
154///       serialized error message.  The calling code is responsible for freeing the `RustBuffer`
155///     - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value
156pub fn rust_call<F, R>(out_status: &mut RustCallStatus, callback: F) -> R
157where
158    F: panic::UnwindSafe + FnOnce() -> Result<R, RustCallError>,
159    R: FfiDefault,
160{
161    rust_call_with_out_status(out_status, callback).unwrap_or_else(R::ffi_default)
162}
163
164/// Make a Rust call and update `RustCallStatus` based on the result.
165///
166/// If the call succeeds this returns Some(v) and doesn't touch out_status
167/// If the call fails (including Err results), this returns None and updates out_status
168///
169/// This contains the shared code between `rust_call` and `rustfuture::do_wake`.
170pub(crate) fn rust_call_with_out_status<F, R>(
171    out_status: &mut RustCallStatus,
172    callback: F,
173) -> Option<R>
174where
175    F: panic::UnwindSafe + FnOnce() -> Result<R, RustCallError>,
176{
177    let result = panic::catch_unwind(callback);
178    match result {
179        // Happy path.  Note: no need to update out_status in this case because the calling code
180        // initializes it to [RustCallStatusCode::Success]
181        Ok(Ok(v)) => Some(v),
182        // Callback returned an Err.
183        Ok(Err(RustCallError::Error(buf))) => {
184            out_status.code = RustCallStatusCode::Error;
185            *out_status.error_buf = buf;
186            None
187        }
188        Ok(Err(RustCallError::InternalError(msg))) => {
189            out_status.code = RustCallStatusCode::UnexpectedError;
190            *out_status.error_buf = <String as Lower<UniFfiTag>>::lower(msg);
191            None
192        }
193        // Callback panicked
194        Err(cause) => {
195            out_status.code = RustCallStatusCode::UnexpectedError;
196            // Try to coerce the cause into a RustBuffer containing a String.  Since this code can
197            // panic, we need to use a second catch_unwind().
198            let message_result = panic::catch_unwind(panic::AssertUnwindSafe(move || {
199                // The documentation suggests that it will *usually* be a str or String.
200                let message = if let Some(s) = cause.downcast_ref::<&'static str>() {
201                    (*s).to_string()
202                } else if let Some(s) = cause.downcast_ref::<String>() {
203                    s.clone()
204                } else {
205                    "Unknown panic!".to_string()
206                };
207                trace!("Caught a panic calling rust code: {:?}", message);
208                <String as Lower<UniFfiTag>>::lower(message)
209            }));
210            if let Ok(buf) = message_result {
211                // If this was ever set twice we'd leak the old value - but because this is the only
212                // place where it is set, and this is only called once, no leaks should exist in practice.
213                *out_status.error_buf = buf;
214            }
215            // Ignore the error case.  We've done all that we can at this point.  In the bindings
216            // code, we handle this by checking if `error_buf` still has an empty `RustBuffer` and
217            // using a generic message.
218            None
219        }
220    }
221}
222
223#[cfg(test)]
224mod test {
225    use super::*;
226    use crate::{test_util::TestError, Lift, LowerReturn};
227    use anyhow::anyhow;
228
229    #[test]
230    fn test_rust_call() {
231        // Successful call
232        let mut status = RustCallStatus::default();
233        let return_value = rust_call(&mut status, || {
234            <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(Ok(100))
235        });
236
237        assert_eq!(status.code, RustCallStatusCode::Success);
238        assert_eq!(return_value, 100);
239
240        // Successful call that returns an Err value
241        let mut status = RustCallStatus::default();
242        rust_call(&mut status, || {
243            <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(Err(TestError(
244                "Error".into(),
245            )))
246        });
247        assert_eq!(status.code, RustCallStatusCode::Error);
248        assert_eq!(
249            <TestError as Lift<UniFfiTag>>::try_lift(ManuallyDrop::into_inner(status.error_buf))
250                .unwrap(),
251            TestError("Error".to_owned())
252        );
253
254        // Internal error while trying to make the call
255        let mut status = RustCallStatus::default();
256        rust_call(&mut status, || {
257            <Result<i8, TestError> as LowerReturn<UniFfiTag>>::handle_failed_lift(LiftArgsError {
258                arg_name: "foo",
259                error: anyhow!("invalid handle"),
260            })
261        });
262        assert_eq!(status.code, RustCallStatusCode::UnexpectedError);
263        assert_eq!(
264            <String as Lift<UniFfiTag>>::try_lift(ManuallyDrop::into_inner(status.error_buf))
265                .unwrap(),
266            "Failed to convert arg 'foo': invalid handle"
267        );
268
269        // Panic inside the call
270        let mut status = RustCallStatus::default();
271        rust_call(&mut status, || -> Result<i8, RustCallError> {
272            panic!("I crashed")
273        });
274        assert_eq!(status.code, RustCallStatusCode::UnexpectedError);
275        assert_eq!(
276            <String as Lift<UniFfiTag>>::try_lift(ManuallyDrop::into_inner(status.error_buf))
277                .unwrap(),
278            "I crashed"
279        );
280    }
281}