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}