Crate ffi_support
source ·Expand description
FFI Support
This crate implements a support library to simplify implementing the patterns that the
mozilla/application-services
repository uses for it’s “Rust Component” FFI libraries.
It is strongly encouraged that anybody writing FFI code in this repository read this documentation before doing so, as it is a subtle, difficult, and error prone process.
Terminology
For each library, there are currently three parts we’re concerned with. There’s no clear correct name for these, so this documentation will attempt to use the following terminology:
-
Rust Component: A Rust crate which does not expose an FFI directly, but may be may be wrapped by one that does. These have a
crate-type
in their Cargo.toml (see https://doc.rust-lang.org/reference/linkage.html) oflib
, and notstaticlib
orcdylib
(Note thatlib
is the default ifcrate-type
is not specified). Examples include thefxa-client
, andlogins
crates. -
FFI Component: A wrapper crate that takes a Rust component, and exposes an FFI from it. These typically have
ffi
in the name, and havecrate-type = ["lib", "staticlib", "cdylib"]
in their Cargo.toml. For example, thefxa-client/ffi
andlogins/ffi
crates (note: paths are subject to change). When built, these produce a native library that is consumed by the “FFI Consumer”. -
FFI Consumer: This is a low level library, typically implemented in Kotlin (for Android) or Swift (for iOS), that exposes a memory-safe wrapper around the memory-unsafe C API produced by the FFI component. It’s expected that the maintainers of the FFI Component and FFI Consumer be the same (or at least, the author of the consumer should be completely comfortable with the API exposed by, and code in the FFI component), since the code in these is extremely tightly coupled, and very easy to get wrong.
Note that while there are three parts, there may be more than three libraries relevant here, for example there may be more than one FFI consumer (one for Android, one for iOS).
Usage
This library will typically be used in both the Rust component, and the FFI component, however it frequently will be an optional dependency in the Rust component that’s only available when a feature flag (which the FFI component will always require) is used.
The reason it’s required inside the Rust component (and not solely in the FFI component, which
would be nice), is so that types provided by that crate may implement the traits provided by
this crate (this is because Rust does not allow crate C
to implement a trait defined in crate
A
for a type defined in crate B
).
In general, examples should be provided for the most important types and functions
(call_with_result
, IntoFfi
,
ExternError
, etc), but you should also look at the code of
consumers of this library.
Usage in the Rust Component
Inside the Rust component, you will implement:
-
IntoFfi
for all types defined in that crate that you want to return over the FFI. For most common cases, theimplement_into_ffi_by_pointer!
andimplement_into_ffi_by_json!
macros will do the job here, however you can see that trait’s documentation for discussion and examples of implementing it manually. -
Conversion to
ExternError
for the error type(s) exposed by that rust component, that is,impl From<MyError> for ExternError
.
Usage in the FFI Component
Inside the FFI component, you will use this library in a few ways:
-
Destructors will be exposed for each types that had
implement_into_ffi_by_pointer!
called on it (usingdefine_box_destructor!
), and a destructor for strings should be exposed as well, usingdefine_string_destructor
-
The body of every / nearly every FFI function will be wrapped in either a
call_with_result
orcall_with_output
.This is required because if we
panic!
(e.g. from anassert!
,unwrap()
,expect()
, from indexing past the end of an array, etc) across the FFI boundary, the behavior is undefined and in practice very weird things tend to happen (we aren’t caught by the caller, since they don’t have the same exception behavior as us).If you don’t think your program (or possibly just certain calls) can handle panics, you may also use the versions of these functions in the
abort_on_panic
module, which do as their name suggest.
Additionally, c strings that are passed in as arguments may be converted to rust strings using
helpers such as rust_str_from_c
, opt_rust_str_from_c
, rust_string_from_c
,
opt_rust_string_from_c
, etc.
Modules
call_with_result
and call_with_output
that aborts, instead of unwinding, on panic.Macros
Box::into_raw(Box::new(value))
(e.g. a pointer which is probably opaque).extern "C"
functions from this library. However, it’s pretty
common to want to free strings allocated by rust, so many libraries will need this, so we
provide it as a macro.IntoFfi
] for the provided types (more than one may be passed in) by converting to
the type to a JSON string. This macro also allows you to return Vec<T>
for the types, also by
serialization to JSON (by way of [IntoFfiJsonTag
]).IntoFfi
] for the provided types (more than one may be passed in) by allocating
$T
on the heap as an opaque pointer.Structs
Traits
IntoFfi::Value
).IntoFfi
for
Vec<T>
(and potentially things like HashMap<String, T>
in the future) by serializing it to
JSON. It’s automatically implemented as part of implement_into_ffi_by_json!
, and you
probably don’t need to implement it manually.Functions
T
while:Result<T, E>
while:rust_string_to_c
on the rust heap. If c_string
is
null, this is a no-op.rust_string_from_c
, but returns None if c_string
is null instead of asserting.rust_string_from_c
, but returns None if c_string
is null instead of asserting.rust_string_to_c
which takes an Option, and returns null for None.str
. This does not take ownership of the string,
and you should be careful about the lifetime of the resulting string. Note that strings
containing invalid UTF-8 are replaced with the empty string (for many cases, you will want to
use rust_string_from_c
instead, which will do a lossy conversion).