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
asynccalls from Dart
- No Gradle or CMake on Android
- No need to make an Android archive (
- No need to make an iOS
The same environment will also work for any language that can build C-lang static and shared libraries, supporting ARM at least.
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
libgreeter.a we need a bit of 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
cbindgen to generate a C header with the bindings for us:
cargo install cbindgen
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:
cbindgen.toml on the
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
What have we compiled?
Three Android shared libraries:
An iOS static library containing
The bindings header:
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.
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
ios folder, create a symbolic link to
ln -s ../rust/target/universal/release/libgreeter.a .
Append the contents of
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.
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.
ios/greeter.podspec, so that XCode imports our Rust artifacts when the Flutter plugin is installed:
Calling the library from Dart
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.
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.
make all again on the
rust folder and add the new signature of
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.
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:
This project is a Flutter Plugin template. It provides out-of-the box support for cross-compiling native Rust code for…
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.