6.8 KiB
When to use what method of passing data between Rust and Java/Swift
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.
Types with identity
Examples of this are things like database connections, the FirefoxAccounts
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 Closable
/AutoClosable
).
You have two choices here:
-
Use a
ConcurrentHandleMap
to 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 self
method, this is the safest choice.Additionally, this will ensure panic-safety, as you'll poison your Mutex.
The
ffi_support::handle_map
docs are good, and underConcurrentHandleMap
include 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 ofrc_log
, which has unique requirements). -
Using an opaque pointer. This is generally only recommended for rare cases like the
PlacesInterruptHandle
(or theLogAdapterState
fromrc_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
ConcurrentHandleMap
is undesirable. You should probably talk to us before adding another type that works this way, to make sure it's sound.The
ffi_support
docs discuss how to do this, or take a look at how it's done forPlacesInterruptHandle
).
Plain Old Data
This includes both primitive values, strings, arrays, or arbitrarially nested structures containing them.
Primitives
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 (likeu8
) and represent 0 for false and 1 for true for interchange over the FFI, converting back to a KotlinBoolean
or swiftBool
after (as to not expose this somewhat annoying limitation in our public API). -
usize
/isize
: These cause the structure size to be different based on the platform. JNA does handle this if you useNativeSize
, 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 usingi64
/i32
forLong
/Int
. (You can also useu64
/u32
forLong
/Int
, if you're certain the value is not negative) -
char
: I really don't see a reason you need to pass a single codepoint over the FFI, but if someone needs to do this, they instead should just pass it as au32
.If you do this, you should probably be aware of the fact that Java chars are 16 bit, and Swift
Character
s are actually strings (they represent Extended Grapheme Clusters, not codepoints).
Strings
These we pass as nul-terminated UTF-8 C-strings.
For return values, used *mut c_char
, and for input, use
ffi_support::FfiStr
-
If the string is returned from Rust to Kotlin/Swift, you need to expose a string destructor from your ffi crate. See
ffi_support::define_string_destructor!
).For converting to a
*mut c_char
, use eitherrust_string_to_c
if you have aString
, oropt_rust_string_to_c
forOption<String>
(None becomesstd::ptr::null_mut()
).Important: In Kotlin, the type returned by a function that produces this must be
Pointer
, and notString
, and the parameter that the destructor takes as input must also bePointer
.Using
String
will almost work. JNA will convert the return value toString
automatically, 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. TheFfiStr
has methods for extracting it's data as&str
,Option<&str>
,String
, andOption<String>
.
It's also completely fine to use Protobufs or JSON for this case!
Aggregates
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 protobuf-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 addingffi_support::implement_into_ffi_by_json
into 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 tradeoff to make.