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
14 use crate::{FfiDefault, Lower, RustBuffer, UniFfiTag};
15 use std::mem::MaybeUninit;
16 use 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)]
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`.
56 }
57
58 impl RustCallStatus {
new() -> Self59 pub fn new() -> Self {
60 Self {
61 code: RustCallStatusCode::Success,
62 error_buf: MaybeUninit::new(RustBuffer::new()),
63 }
64 }
65
cancelled() -> Self66 pub fn cancelled() -> Self {
67 Self {
68 code: RustCallStatusCode::Cancelled,
69 error_buf: MaybeUninit::new(RustBuffer::new()),
70 }
71 }
72
error(message: impl Into<String>) -> Self73 pub fn error(message: impl Into<String>) -> Self {
74 Self {
75 code: RustCallStatusCode::UnexpectedError,
76 error_buf: MaybeUninit::new(<String as Lower<UniFfiTag>>::lower(message.into())),
77 }
78 }
79 }
80
81 impl Default for RustCallStatus {
default() -> Self82 fn default() -> Self {
83 Self {
84 code: RustCallStatusCode::Success,
85 error_buf: MaybeUninit::uninit(),
86 }
87 }
88 }
89
90 /// Result of a FFI call to a Rust function
91 #[repr(i8)]
92 #[derive(Debug, PartialEq, Eq)]
93 pub enum RustCallStatusCode {
94 /// Successful call.
95 Success = 0,
96 /// Expected error, corresponding to the `Result::Err` variant. [RustCallStatus::error_buf]
97 /// will contain the serialized error.
98 Error = 1,
99 /// Unexpected error. [RustCallStatus::error_buf] will contain a serialized message string
100 UnexpectedError = 2,
101 /// Async function cancelled. [RustCallStatus::error_buf] will be empty and does not need to
102 /// be freed.
103 ///
104 /// This is only returned for async functions and only if the bindings code uses the
105 /// [rust_future_cancel] call.
106 Cancelled = 3,
107 }
108
109 /// Handle a scaffolding calls
110 ///
111 /// `callback` is responsible for making the actual Rust call and returning a special result type:
112 /// - For successful calls, return `Ok(value)`
113 /// - For errors that should be translated into thrown exceptions in the foreign code, serialize
114 /// the error into a `RustBuffer`, then return `Ok(buf)`
115 /// - The success type, must implement `FfiDefault`.
116 /// - `Return::lower_return` returns `Result<>` types that meet the above criteria>
117 /// - If the function returns a `Ok` value it will be unwrapped and returned
118 /// - If the function returns a `Err` value:
119 /// - `out_status.code` will be set to [RustCallStatusCode::Error].
120 /// - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing the error. The calling
121 /// code is responsible for freeing the `RustBuffer`
122 /// - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value
123 /// - If the function panics:
124 /// - `out_status.code` will be set to `CALL_PANIC`
125 /// - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing a
126 /// serialized error message. The calling code is responsible for freeing the `RustBuffer`
127 /// - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value
rust_call<F, R>(out_status: &mut RustCallStatus, callback: F) -> R where F: panic::UnwindSafe + FnOnce() -> Result<R, RustBuffer>, R: FfiDefault,128 pub fn rust_call<F, R>(out_status: &mut RustCallStatus, callback: F) -> R
129 where
130 F: panic::UnwindSafe + FnOnce() -> Result<R, RustBuffer>,
131 R: FfiDefault,
132 {
133 rust_call_with_out_status(out_status, callback).unwrap_or_else(R::ffi_default)
134 }
135
136 /// Make a Rust call and update `RustCallStatus` based on the result.
137 ///
138 /// If the call succeeds this returns Some(v) and doesn't touch out_status
139 /// If the call fails (including Err results), this returns None and updates out_status
140 ///
141 /// This contains the shared code between `rust_call` and `rustfuture::do_wake`.
rust_call_with_out_status<F, R>( out_status: &mut RustCallStatus, callback: F, ) -> Option<R> where F: panic::UnwindSafe + FnOnce() -> Result<R, RustBuffer>,142 pub(crate) fn rust_call_with_out_status<F, R>(
143 out_status: &mut RustCallStatus,
144 callback: F,
145 ) -> Option<R>
146 where
147 F: panic::UnwindSafe + FnOnce() -> Result<R, RustBuffer>,
148 {
149 let result = panic::catch_unwind(|| {
150 crate::panichook::ensure_setup();
151 callback()
152 });
153 match result {
154 // Happy path. Note: no need to update out_status in this case because the calling code
155 // initializes it to [RustCallStatusCode::Success]
156 Ok(Ok(v)) => Some(v),
157 // Callback returned an Err.
158 Ok(Err(buf)) => {
159 out_status.code = RustCallStatusCode::Error;
160 unsafe {
161 // Unsafe because we're setting the `MaybeUninit` value, see above for safety
162 // invariants.
163 out_status.error_buf.as_mut_ptr().write(buf);
164 }
165 None
166 }
167 // Callback panicked
168 Err(cause) => {
169 out_status.code = RustCallStatusCode::UnexpectedError;
170 // Try to coerce the cause into a RustBuffer containing a String. Since this code can
171 // panic, we need to use a second catch_unwind().
172 let message_result = panic::catch_unwind(panic::AssertUnwindSafe(move || {
173 // The documentation suggests that it will *usually* be a str or String.
174 let message = if let Some(s) = cause.downcast_ref::<&'static str>() {
175 (*s).to_string()
176 } else if let Some(s) = cause.downcast_ref::<String>() {
177 s.clone()
178 } else {
179 "Unknown panic!".to_string()
180 };
181 log::error!("Caught a panic calling rust code: {:?}", message);
182 <String as Lower<UniFfiTag>>::lower(message)
183 }));
184 if let Ok(buf) = message_result {
185 unsafe {
186 // Unsafe because we're setting the `MaybeUninit` value, see above for safety
187 // invariants.
188 out_status.error_buf.as_mut_ptr().write(buf);
189 }
190 }
191 // Ignore the error case. We've done all that we can at this point. In the bindings
192 // code, we handle this by checking if `error_buf` still has an empty `RustBuffer` and
193 // using a generic message.
194 None
195 }
196 }
197 }
198
199 #[cfg(test)]
200 mod test {
201 use super::*;
202 use crate::{test_util::TestError, Lift, LowerReturn};
203
create_call_status() -> RustCallStatus204 fn create_call_status() -> RustCallStatus {
205 RustCallStatus {
206 code: RustCallStatusCode::Success,
207 error_buf: MaybeUninit::new(RustBuffer::new()),
208 }
209 }
210
test_callback(a: u8) -> Result<i8, TestError>211 fn test_callback(a: u8) -> Result<i8, TestError> {
212 match a {
213 0 => Ok(100),
214 1 => Err(TestError("Error".to_owned())),
215 x => panic!("Unexpected value: {x}"),
216 }
217 }
218
219 #[test]
test_rust_call()220 fn test_rust_call() {
221 let mut status = create_call_status();
222 let return_value = rust_call(&mut status, || {
223 <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(test_callback(0))
224 });
225
226 assert_eq!(status.code, RustCallStatusCode::Success);
227 assert_eq!(return_value, 100);
228
229 rust_call(&mut status, || {
230 <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(test_callback(1))
231 });
232 assert_eq!(status.code, RustCallStatusCode::Error);
233 unsafe {
234 assert_eq!(
235 <TestError as Lift<UniFfiTag>>::try_lift(status.error_buf.assume_init()).unwrap(),
236 TestError("Error".to_owned())
237 );
238 }
239
240 let mut status = create_call_status();
241 rust_call(&mut status, || {
242 <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(test_callback(2))
243 });
244 assert_eq!(status.code, RustCallStatusCode::UnexpectedError);
245 unsafe {
246 assert_eq!(
247 <String as Lift<UniFfiTag>>::try_lift(status.error_buf.assume_init()).unwrap(),
248 "Unexpected value: 2"
249 );
250 }
251 }
252 }
253