Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  rustcalls.rs   Sprache: unbekannt

 
/* 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::ManuallyDrop;
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,
    // error_buf is owned by the foreign side.
    // - Whatever we are passed, we must never free. This however implies we must be passed
    //   an empty `RustBuffer` otherwise it would leak when we replace it with our own.
    // - On error we will set it to a `RustBuffer` we expect the foreign side to free.
    // We assume initialization, which means we can use `ManuallyDrop` instead of
    // `MaybeUninit`, which avoids unsafe code and clarifies ownership.
    // We must take care to not set this twice to avoid leaking the first `RustBuffer`.
    pub error_buf: ManuallyDrop<RustBuffer>,
}

impl Default for RustCallStatus {
    fn default() -> Self {
        Self {
            code: RustCallStatusCode::Success,
            error_buf: Default::default(),
        }
    }
}

impl RustCallStatus {
    pub fn cancelled() -> Self {
        Self {
            code: RustCallStatusCode::Cancelled,
            error_buf: Default::default(),
        }
    }

    pub fn error(message: impl Into<String>) -> Self {
        Self {
            code: RustCallStatusCode::UnexpectedError,
            error_buf: ManuallyDrop::new(<String as Lower<UniFfiTag>>::lower(message.into())),
        }
    }
}

/// Result of a FFI call to a Rust function
/// Value is signed to avoid Kotlin's experimental unsigned types.
#[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,
}

impl TryFrom<i8> for RustCallStatusCode {
    type Error = i8;

    fn try_from(value: i8) -> Result<Self, i8> {
        match value {
            0 => Ok(Self::Success),
            1 => Ok(Self::Error),
            2 => Ok(Self::UnexpectedError),
            3 => Ok(Self::Cancelled),
            n => Err(n),
        }
    }
}

/// Error type for Rust scaffolding calls
///
/// This enum represents the fact that there are two ways for a scaffolding call to fail:
/// - Expected errors (the Rust function returned an `Err` value).
/// - Unexpected errors (there was a failure calling the Rust function, for example the failure to
///   lift the arguments).
pub enum RustCallError {
    /// The Rust function returned an `Err` value.
    ///
    /// The associated value is the serialized `Err` value.
    Error(RustBuffer),
    /// There was a failure to call the Rust function, for example a failure to lift the arguments.
    ///
    /// The associated value is a message string for the error.
    InternalError(String),
}

/// Error when trying to lift arguments to pass to the scaffolding call
pub struct LiftArgsError {
    pub arg_name: &'static str,
    pub error: anyhow::Error,
}

/// 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, RustCallError>,
    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, RustCallError>,
{
    let result = panic::catch_unwind(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(RustCallError::Error(buf))) => {
            out_status.code = RustCallStatusCode::Error;
            *out_status.error_buf = buf;
            None
        }
        Ok(Err(RustCallError::InternalError(msg))) => {
            out_status.code = RustCallStatusCode::UnexpectedError;
            *out_status.error_buf = <String as Lower<UniFfiTag>>::lower(msg);
            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 {
                // If this was ever set twice we'd leak the old value - but because this is the only
                // place where it is set, and this is only called once, no leaks should exist in practice.
                *out_status.error_buf = 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};
    use anyhow::anyhow;

    #[test]
    fn test_rust_call() {
        // Successful call
        let mut status = RustCallStatus::default();
        let return_value = rust_call(&mut status, || {
            <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(Ok(100))
        });

        assert_eq!(status.code, RustCallStatusCode::Success);
        assert_eq!(return_value, 100);

        // Successful call that returns an Err value
        let mut status = RustCallStatus::default();
        rust_call(&mut status, || {
            <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(Err(TestError(
                "Error".into(),
            )))
        });
        assert_eq!(status.code, RustCallStatusCode::Error);
        assert_eq!(
            <TestError as Lift<UniFfiTag>>::try_lift(ManuallyDrop::into_inner(status.error_buf))
                .unwrap(),
            TestError("Error".to_owned())
        );

        // Internal error while trying to make the call
        let mut status = RustCallStatus::default();
        rust_call(&mut status, || {
            <Result<i8, TestError> as LowerReturn<UniFfiTag>>::handle_failed_lift(LiftArgsError {
                arg_name: "foo",
                error: anyhow!("invalid handle"),
            })
        });
        assert_eq!(status.code, RustCallStatusCode::UnexpectedError);
        assert_eq!(
            <String as Lift<UniFfiTag>>::try_lift(ManuallyDrop::into_inner(status.error_buf))
                .unwrap(),
            "Failed to convert arg 'foo': invalid handle"
        );

        // Panic inside the call
        let mut status = RustCallStatus::default();
        rust_call(&mut status, || -> Result<i8, RustCallError> {
            panic!("I crashed")
        });
        assert_eq!(status.code, RustCallStatusCode::UnexpectedError);
        assert_eq!(
            <String as Lift<UniFfiTag>>::try_lift(ManuallyDrop::into_inner(status.error_buf))
                .unwrap(),
            "I crashed"
        );
    }
}

[ Dauer der Verarbeitung: 0.26 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge