pub fn call_with_result<R, E, F>(
    out_error: &mut ExternError,
    callback: F
) -> R::Valuewhere
    F: FnOnce() -> Result<R, E>,
    E: Into<ExternError> + Debug,
    R: IntoFfi,
Expand description

Call a callback that returns a Result<T, E> while:

  • Catching panics, and reporting them to C via ExternError.
  • Converting T to a C-compatible type using IntoFfi.
  • Converting E to a C-compatible error via Into<ExternError>.

This (or call_with_output) should be in the majority of the FFI functions, see the crate top-level docs for more info.

If your function doesn’t produce an error, you may use call_with_output instead, which doesn’t require you return a Result.

Example

A few points about the following example:

  • This function must be unsafe, as it reads from a raw pointer. If you made it safe, then safe Rust could cause memory safety violations, which would be very bad! (However, FFI functions that don’t read from raw pointers don’t need to be marked unsafe! Sadly, most of ours need to take strings, and so we’re out of luck…)

  • We need to mark it as #[no_mangle] pub extern "C".

  • We prefix it with a unique name for the library (e.g. mylib_). Foreign functions are not namespaced, and symbol collisions can cause a large number of problems and subtle bugs, including memory safety issues in some cases.



#[no_mangle]
pub unsafe extern "C" fn mylib_print_string(
    // Strings come in as a null terminated C string. This is certainly not ideal but it simplifies
    // the "FFI consumer" code, which is trickier code to get right, as it typically has poor
    // support for interacting with native libraries.
    thing_to_print: *const c_char,
    // Note that taking `&mut T` and `&T` is both allowed and encouraged, so long as `T: Sized`,
    // (e.g. it can't be a trait object, `&[T]`, a `&str`, etc). Also note that `Option<&T>` and
    // `Option<&mut T>` are also allowed, if you expect the caller to sometimes pass in null, but
    // that's the only case when it's currently to use `Option` in an argument list like this).
    error: &mut ExternError
) {
    // You should try to to do as little as possible outside the call_with_result,
    // to avoid a case where a panic occurs.
    ffi_support::call_with_result(error, || {
        let s = ffi_support::rust_str_from_c(thing_to_print);
        if s.len() == 0 {
            // This is a silly example!
            return Err(BadEmptyString);
        }
        println!("{}", s);
        Ok(())
    })
}

Unwind (panic) Safety

Internally, this function wraps it’s argument in a AssertUnwindSafe. That means it doesn’t attempt to force you to mark things as UnwindSafe. Effectively, we’re saying that every caller to this function is automatically panic safe, which is a lie. This is not ideal, but it’s unclear what the right call here would be.

To be clear, making the wrong choice here has no bearing on memory safety, unless there are exisiting memory safety holes in the code. That means by using AssertUnwindSafe, we end up in a position closer to the position we’d be in if we were working in a language with exceptions, which typically provides little-to-no assistance in terms of program correctness in the case of something throwing.

Anyway, if we were to require F: UnwindSafe, the implementer of the FFI component would need to use AssertUnwindSafe on every FFI binding that wraps a method that needs to call something on a &mut T (note that this is not true for *mut T, which we want to discourage). The use of this seems likely to be frequent enough in this FFI that I have an extremely hard time believing it would be used with consideration, so while the strategy of “assume everything is panic-safe” is clearly not great, it seems likely to be what happens anyway.

There are, of course, other options:

  1. Abort on panic (e.g. only expose the implementations in abort_on_panic), which is bad for obvious reasons, and seems even worse given our position as libraries.
  2. Poison on panic (as std::sync::Mutex does, for example). This is a valid option, but seems wrong for all cases.
  3. Re-initialize on panic (e.g. reopen the DB connection).

2 and 3 are promising, and allowing users of ffi-support to make these choices with a low amount of boilerplate is something we’d like to investigate in the future, but currently this is where we’ve landed.