Backed out 5 changesets (bug 1890092, bug 1888683) for causing build bustages & crash...
[gecko.git] / third_party / rust / uniffi_core / src / ffi / rustcalls.rs
blob53265393c0598d33fc736071f153b66477ef4f32
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/. */
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
14 use crate::{FfiDefault, Lower, RustBuffer, UniFfiTag};
15 use std::mem::MaybeUninit;
16 use std::panic;
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)]
43 pub struct RustCallStatus {
44     pub code: RustCallStatusCode,
45     // code is signed because unsigned types are experimental in Kotlin
46     pub error_buf: MaybeUninit<RustBuffer>,
47     // error_buf is MaybeUninit to avoid dropping the value that the consumer code sends in:
48     //   - Consumers should send in a zeroed out RustBuffer.  In this case dropping is a no-op and
49     //     avoiding the drop is a small optimization.
50     //   - If consumers pass in invalid data, then we should avoid trying to drop it.  In
51     //     particular, we don't want to try to free any data the consumer has allocated.
52     //
53     // `MaybeUninit` requires unsafe code, since we are preventing rust from dropping the value.
54     // To use this safely we need to make sure that no code paths set this twice, since that will
55     // leak the first `RustBuffer`.
58 impl RustCallStatus {
59     pub fn cancelled() -> Self {
60         Self {
61             code: RustCallStatusCode::Cancelled,
62             error_buf: MaybeUninit::new(RustBuffer::new()),
63         }
64     }
66     pub fn error(message: impl Into<String>) -> Self {
67         Self {
68             code: RustCallStatusCode::UnexpectedError,
69             error_buf: MaybeUninit::new(<String as Lower<UniFfiTag>>::lower(message.into())),
70         }
71     }
74 impl Default for RustCallStatus {
75     fn default() -> Self {
76         Self {
77             code: RustCallStatusCode::Success,
78             error_buf: MaybeUninit::uninit(),
79         }
80     }
83 /// Result of a FFI call to a Rust function
84 #[repr(i8)]
85 #[derive(Debug, PartialEq, Eq)]
86 pub enum RustCallStatusCode {
87     /// Successful call.
88     Success = 0,
89     /// Expected error, corresponding to the `Result::Err` variant.  [RustCallStatus::error_buf]
90     /// will contain the serialized error.
91     Error = 1,
92     /// Unexpected error.  [RustCallStatus::error_buf] will contain a serialized message string
93     UnexpectedError = 2,
94     /// Async function cancelled.  [RustCallStatus::error_buf] will be empty and does not need to
95     /// be freed.
96     ///
97     /// This is only returned for async functions and only if the bindings code uses the
98     /// [rust_future_cancel] call.
99     Cancelled = 3,
102 /// Handle a scaffolding calls
104 /// `callback` is responsible for making the actual Rust call and returning a special result type:
105 ///   - For successfull calls, return `Ok(value)`
106 ///   - For errors that should be translated into thrown exceptions in the foreign code, serialize
107 ///     the error into a `RustBuffer`, then return `Ok(buf)`
108 ///   - The success type, must implement `FfiDefault`.
109 ///   - `Return::lower_return` returns `Result<>` types that meet the above criteria>
110 /// - If the function returns a `Ok` value it will be unwrapped and returned
111 /// - If the function returns a `Err` value:
112 ///     - `out_status.code` will be set to [RustCallStatusCode::Error].
113 ///     - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing the error.  The calling
114 ///       code is responsible for freeing the `RustBuffer`
115 ///     - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value
116 /// - If the function panics:
117 ///     - `out_status.code` will be set to `CALL_PANIC`
118 ///     - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing a
119 ///       serialized error message.  The calling code is responsible for freeing the `RustBuffer`
120 ///     - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value
121 pub fn rust_call<F, R>(out_status: &mut RustCallStatus, callback: F) -> R
122 where
123     F: panic::UnwindSafe + FnOnce() -> Result<R, RustBuffer>,
124     R: FfiDefault,
126     rust_call_with_out_status(out_status, callback).unwrap_or_else(R::ffi_default)
129 /// Make a Rust call and update `RustCallStatus` based on the result.
131 /// If the call succeeds this returns Some(v) and doesn't touch out_status
132 /// If the call fails (including Err results), this returns None and updates out_status
134 /// This contains the shared code between `rust_call` and `rustfuture::do_wake`.
135 pub(crate) fn rust_call_with_out_status<F, R>(
136     out_status: &mut RustCallStatus,
137     callback: F,
138 ) -> Option<R>
139 where
140     F: panic::UnwindSafe + FnOnce() -> Result<R, RustBuffer>,
142     let result = panic::catch_unwind(|| {
143         crate::panichook::ensure_setup();
144         callback()
145     });
146     match result {
147         // Happy path.  Note: no need to update out_status in this case because the calling code
148         // initializes it to [RustCallStatusCode::Success]
149         Ok(Ok(v)) => Some(v),
150         // Callback returned an Err.
151         Ok(Err(buf)) => {
152             out_status.code = RustCallStatusCode::Error;
153             unsafe {
154                 // Unsafe because we're setting the `MaybeUninit` value, see above for safety
155                 // invariants.
156                 out_status.error_buf.as_mut_ptr().write(buf);
157             }
158             None
159         }
160         // Callback panicked
161         Err(cause) => {
162             out_status.code = RustCallStatusCode::UnexpectedError;
163             // Try to coerce the cause into a RustBuffer containing a String.  Since this code can
164             // panic, we need to use a second catch_unwind().
165             let message_result = panic::catch_unwind(panic::AssertUnwindSafe(move || {
166                 // The documentation suggests that it will *usually* be a str or String.
167                 let message = if let Some(s) = cause.downcast_ref::<&'static str>() {
168                     (*s).to_string()
169                 } else if let Some(s) = cause.downcast_ref::<String>() {
170                     s.clone()
171                 } else {
172                     "Unknown panic!".to_string()
173                 };
174                 log::error!("Caught a panic calling rust code: {:?}", message);
175                 <String as Lower<UniFfiTag>>::lower(message)
176             }));
177             if let Ok(buf) = message_result {
178                 unsafe {
179                     // Unsafe because we're setting the `MaybeUninit` value, see above for safety
180                     // invariants.
181                     out_status.error_buf.as_mut_ptr().write(buf);
182                 }
183             }
184             // Ignore the error case.  We've done all that we can at this point.  In the bindings
185             // code, we handle this by checking if `error_buf` still has an empty `RustBuffer` and
186             // using a generic message.
187             None
188         }
189     }
192 #[cfg(test)]
193 mod test {
194     use super::*;
195     use crate::{test_util::TestError, Lift, LowerReturn};
197     fn create_call_status() -> RustCallStatus {
198         RustCallStatus {
199             code: RustCallStatusCode::Success,
200             error_buf: MaybeUninit::new(RustBuffer::new()),
201         }
202     }
204     fn test_callback(a: u8) -> Result<i8, TestError> {
205         match a {
206             0 => Ok(100),
207             1 => Err(TestError("Error".to_owned())),
208             x => panic!("Unexpected value: {x}"),
209         }
210     }
212     #[test]
213     fn test_rust_call() {
214         let mut status = create_call_status();
215         let return_value = rust_call(&mut status, || {
216             <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(test_callback(0))
217         });
219         assert_eq!(status.code, RustCallStatusCode::Success);
220         assert_eq!(return_value, 100);
222         rust_call(&mut status, || {
223             <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(test_callback(1))
224         });
225         assert_eq!(status.code, RustCallStatusCode::Error);
226         unsafe {
227             assert_eq!(
228                 <TestError as Lift<UniFfiTag>>::try_lift(status.error_buf.assume_init()).unwrap(),
229                 TestError("Error".to_owned())
230             );
231         }
233         let mut status = create_call_status();
234         rust_call(&mut status, || {
235             <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(test_callback(2))
236         });
237         assert_eq!(status.code, RustCallStatusCode::UnexpectedError);
238         unsafe {
239             assert_eq!(
240                 <String as Lift<UniFfiTag>>::try_lift(status.error_buf.assume_init()).unwrap(),
241                 "Unexpected value: 2"
242             );
243         }
244     }