summaryrefslogtreecommitdiff
path: root/src/ffi/rustcalls.rs
blob: 51204ef261a0ebef98758685131b99a61fa16108 (plain)
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 successful 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"
            );
        }
    }
}