Using FFI on Flutter Plugins to run native Rust code

Brick Pop
Flutter Community
Published in
6 min readMar 22, 2020

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.

--

--

Brick Pop
Flutter Community

Full stack tech, thoughts and life. Personal journey is underway. The future is now.