Using FFI on Flutter Plugins to run native Rust code
Back in September, Google announced Dart support for Foreign Function Interface at the Google Developer Days. The feature landed in Dart 2.5 and became also available in Flutter 1.9.
While taking advantage of FFI is straightforward in plain Dart, some people have found it much more complex to achieve similar results in Flutter.
Today we are going to create a Flutter mobile Plugin and call native Rust code using the cleanest and simplest approach possible.
What this means for us:
- No Swift or Kotlin wrappers
- No message channels
- No
async
calls from Dart - No Gradle or CMake on Android
- No need to make an Android archive (
.aar
) - No need to make an iOS
.framework
The same environment will also work for any language that can build C-lang static and shared libraries, supporting ARM at least.
Scaffold
Let’s start by creating the project files for our Flutter Plugin, called Greeter.
flutter create --org com.greeter -i swift -a kotlin greeter
This will create a new folder named greeter
and populate it with the project files to run a Flutter plugin.
The Rust library
Next, enter the greeter
folder and create a Rust library in it.
cargo new --lib --name greeter rust
Edit the newly generated rust/Cargo.toml
file and add the [lib]
section as follows:
Our target will be a shared library for Android and a static library for iOS.
Writing the library
Add the dependencies you need in Cargo.toml
, write your own code and make sure that the functions you intend to call from Dart are declared as follows:
This declaration will make exported symbols to be compatible with FFI, so that Dart will be able to load them later on.
For simplicity, we will use a trivial function, accepting a string as an argument and returning another string to the caller.
Compiling the library
Before we can turn lib.rs
into libgreeter.so
and libgreeter.a
we need a bit of setup.
Setup
For Android, install the Android NDK if you don’t have it and add the following Rust targets:
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android
For iOS, install XCode on your Mac and add the the following Rust targets:
rustup target add aarch64-apple-ios armv7-apple-ios armv7s-apple-ios x86_64-apple-ios i386-apple-ios
Next, install cargo lipo:
cargo install cargo-lipo
Finally, install cbindgen
to generate a C header with the bindings for us:
cargo install cbindgen
Compiling
On the rust
folder, compile the iOS static library by running:
cargo lipo --release
For Android, run cargo build
for every target. Point it to your Android NDK linkers like below:
Create cbindgen.toml
on the rust
folder:
Finally, our C header file can be generated as follows:
cbindgen ./src/lib.rs -c cbindgen.toml | grep -v \#include | uniq
This will call cbindgen
with our source files and filter out the unwanted #include
external directives. The output are our C bindings for iOS.
Automated setup and builds
To make things easier, you would typically write a build script.
You can also use this makefile to achieve the same.
With it, the setup is as simple as running make init
and compilation is done by issuing make all
.
What have we compiled?
Three Android shared libraries:
target/aarch64-linux-android/release/libgreeter.so
target/armv7-linux-androideabi/release/libgreeter.so
target/i686-linux-android/release/libgreeter.so
An iOS static library containing ARM64
and x86_64
:
target/universal/release/libgreeter.a
The bindings header:
target/bindings.h
Rust is all set. Now it’s time for Flutter.
Importing the library on Android
Gradle will bundle our shared libraries out of the box if we place them in the appropriate location.
On android/src/main
create this folder structure:
Instead of copying the artifacts on each rebuild, let’s create symbolic links pointing to their corresponding shared library:
Every platform will detect the relevant shared libraries within its own folder.
Importing the library on iOS
On the ios
folder, create a symbolic link to libgreeter.a
.
ln -s ../rust/target/universal/release/libgreeter.a .
Append the contents of bindings.h
into ios/Classes/GreeterPlugin.h
:
cat ../rust/target/bindings.h >> Classes/GreeterPlugin.h
Note: XCode will not bundle the library unless it detects explicit usage within the workspace. Since our Dart code calling it is out of the scope of XCode, we need to write a dummy Swift function that makes some fake usage.
Edit ios/Classes/SwiftGreeterPlugin.swift
and add a new method like the following:
Since we are not using the Flutter messaging channel, the rest of the class methods could be left empty.
Finally, edit ios/greeter.podspec
, so that XCode imports our Rust artifacts when the Flutter plugin is installed:
Calling the library from Dart
On lib/greeter.dart
we need to declare our function’s bindings, load them into local Function
variables and finally, call them.
First of all, load the library:
Define the Dart and the FFI signatures for our function and bind them to the symbol that we exported from Rust:
Note that there are two typedef
’s with the same signature, but each one of them serves a different purpose. The first signature is defining the types assigned to our local Dart Function
while the second one is the FFI version, which helps the loader do its job.
And finally, prepare the parameters to pass and call the native function.
If you followed all the steps, you should have called your first native function on Flutter!
There is still a bit more that we need do, however.
Cleaning after ourselves
As you may know, Rust does not have a garbage collector. The language is designed in a way, such that you don’t need to allocate and free memory yourself, neither.
In rust/src/lib.rs
we are returning a CString
and in a normal scenario, the result variable would be moved to the Rust function that called it (and eventually disposed).
However, the result of rust_greeting
is being received on the Dart domain. As it is, this allocation will never be cleaned up, so we need to prevent memory leaks and take care of it.
One way is to allocate some some memory, pass a pointer and free it from Dart. However, our Rust library is already doing the allocation, so it seems cleaner to free it there as well.
Let’s add a Rust function to take care of this:
What the function does is casting the incoming pointer back into a CString
, and let it be disposed by the Rust library itself as soon as the scope expires.
Run make all
again on the rust
folder and add the new signature of rust/target/bindings.h
into ios/Classes/GreeterPlugin.h
.
In lib/greeter.dart
, add the typedef
’s for the new function:
And finally, when you are done using the result of rustGreeting(...)
, ask Rust to release it:
If your Rust function returns basic types you don’t need to worry about freeing memory allocations. For more complex scenarios, look at the dart:ffi
example on GitHub.
Conclusion
Writing Flutter mobile plugins that can leverage native Rust code with FFI is now much simpler and straightforward, and the same may also apply for Flutter desktop pretty soon.
The examples featured on the article can be found on the GitHub template below:
Now it’s your turn to write amazing plugins and post them on pub.dev!
Aside note on iOS
You should keep in mind that using specific Rust crates may fail to compile on iOS. Not because the lack of support, but because several API’s that are available on MacOS may not be usable on iOS.
As an example, if you depend on a library that requires libproc
to work, the compiler will not be able to find it on the iOS SDK. This is because of Apple’s policy in regards of process sandboxing.