[−][src]Function ffi_support::call_with_result
pub fn call_with_result<R, E, F>(
out_error: &mut ExternError,
callback: F
) -> R::Value where
F: FnOnce() -> Result<R, E>,
E: Into<ExternError> + Debug,
R: IntoFfi,
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 usingIntoFfi
. - Converting
E
to a C-compatible error viaInto<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 throw
ing.
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:
- 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. - Poison on panic (as
std::sync::Mutex
does, for example). This is a valid option, but seems wrong for all cases. - 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.