This is currently copied from application-services documentation.
There are a bunch of options here. For the purposes of our discussion, there are two kinds of values you may want to pass over the FFI.
- Types with identity (includes stateful types, resource types, or anything that isn't really serializable).
- Plain ol' data.
Examples of this are things like database connections, the
struct, etc. These types are complex, implemented in Rust, and it's not
unreasonable for them to come to Java/Kotlin as a type representing a
resource (e.g. implementing
You have two choices here:
ConcurrentHandleMapto store all instances of your object, and pass the handle back and forth as a u64 from Rust / Long from Kotlin.
This is recommended for most cases, as it's the hardest to mess up. Additionally, for types T such that
&T: Sync + Send, or that you need to call
&mut selfmethod, this is the safest choice.
Additionally, this will ensure panic-safety, as you'll poison your Mutex.
ffi_support::handle_mapdocs are good, and under
ConcurrentHandleMapinclude an example of how to set this up. You can also look at most of the FFI crates, as they do this (with the exception of
rc_log, which has unique requirements).
Using an opaque pointer. This is generally only recommended for rare cases like the
rc_log, although it will probably eventually use a handle).
It's good if your synchronization or threading requirements are somewhat complex and handled separately, such that the additional overhead of the
ConcurrentHandleMapis undesirable. You should probably talk to us before adding another type that works this way, to make sure it's sound.
ffi_supportdocs discuss how to do this, or take a look at how it's done for
This includes both primitive values, strings, arrays, or arbitrarily nested structures containing them.
Specifically numeric primitives. These we'll tackle first since they're the easiest.
In general, you can just pass them as you wish. There are a couple of exceptions/caveats. All of them are caused by JNA/Android issues (Swift has very good support for calling over the FFI), but it's our lowest common denominator.
bool: Don't use it. JNA doesn't handle it well. Instead, use a numeric type (like
u8) and represent 0 for false and 1 for true for interchange over the FFI, converting back to a Kotlin
Boolafter (as to not expose this somewhat annoying limitation in our public API).
isize: These cause the structure size to be different based on the platform. JNA does handle this if you use
NativeSize, but it's awkward, incompatible with it's Direct Mapping optimization (which we don't use but want to in the future), and has more overhead than just using
Int. (You can also use
Int, if you're certain the value is not negative)
char: I really don't see a reason you need to pass a single code point over the FFI, but if someone needs to do this, they instead should just pass it as a
If you do this, you should probably be aware of the fact that Java chars are 16 bit, and Swift
Characters are actually strings (they represent Extended Grapheme Clusters, not codepoints).
These we pass as null-terminated UTF-8 C-strings.
For return values, used
*mut c_char, and for input, use
If the string is returned from Rust to Kotlin/Swift, you need to expose a string destructor from your ffi crate. See
Important: In Kotlin, the type returned by a function that produces this must be
Pointer, and not
String, and the parameter that the destructor takes as input must also be
Stringwill almost work. JNA will convert the return value to
Stringautomatically, leaking the value Rust provides. Then, when passing to the destructor, it will allocate a temporary buffer, pass it to Rust, which we'll free, corrupting both heaps 💥. Oops!
If the string is passed into Rust from Kotlin/Swift, the rust code should declare the parameter as a
FfiStr<'_>. and things should then work more or less automatically. The
FfiStrhas methods for extracting it's data as
It's also completely fine to use Protobufs or JSON for this case!
This is any type that's more complex than a primitive or a string (arrays, structures, and combinations there-in). There are two options we recommend for these cases:
Passing data using Protobufs. See the "Using Protobufs-encoded data over Rust FFI" document for details on how to do this. We recommend this for all new use cases, unless you have a specific reason that JSON is better (e.g. semi-opaque JSON encoded data is desired on the other side).
Passing data as JSON. This is very easy, and useful for prototyping, however much slower, requires a great deal of copying and redundant encode/decode steps (in general, the data will be copied at least 4 times to make this work, and almost certainly more in practice), and can be done relatively easily by
derive(Serialize, Deserialize), and adding
ffi_support::implement_into_ffi_by_jsoninto the crate that defines the type.
For new non-test code this is not a recommended approach. For test code, this can be a useful performance/simplicity trade off to make.