Finally running Rust natively on a Flutter plugin! Here is how

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

--

For the last weeks and months, I’ve been researching the ideal way to embed Rust code (or C lang compatible) on Flutter Plugins, using the new FFI features from Dart.

After days and nights hitting the wall, I’ve finally come up to a prototype that checks ✅ all the boxes 🚀

  • No Swift/Kotlin wrappers
  • No message passing
  • No async/await on Dart
  • Writing code once
  • No garbage collection
  • Mostly automated development

Since I heard of Dart supporting FFI, I knew this would be my next challenge. However, googling around didn’t bring the kind of example that I could apply on a Flutter plugin. So the only option left was to put my boots on and make it through the jungle.

Scaffold

Let’s start by creating the files for our Flutter Plugin, called Greeter.

flutter create --org com.brickpop -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

Photo by Jay Heike on Unsplash

Next, enter the greeter folder and create the Rust library in it.

cargo new --lib --name greeter rust

Then, edit the newly generated rust/Cargo.toml file and add the [lib] section as follows:

Our target will be a static library for iOS and a shared library for Android.

Let’s code our library

Time to write our super fast and efficient code in Rust. Add any dependencies you need in Cargo.toml and then, make sure to declare the functions you intend to call from Dart, like this:

This will make the exported symbols of our library be compatible with FFI, so we can load them from Dart.

For the sake of simplicity, we will use a trivial function working with strings in this example.

Compiling the library

To turn lib.rs into libgreeter.so and libgreeter.a we need a bit of setup.

Setup

For Android, we need to install the Android NDK and add the following Rust targets:

rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android

For iOS, we need XCode installed on a Mac, as well as the the following Rust targets:

rustup target add aarch64-apple-ios armv7-apple-ios armv7s-apple-ios x86_64-apple-ios i386-apple-ios

And then, install cargo lipo:

cargo install cargo-lipo

Finally, we will also install cbindgen to generate a C header automatically for us:

cargo install cbindgen

Compiling

Compiling the iOS static library is as simple as running:

cargo lipo --release

For Android, we need to run cargo build for every target, pointing to our Android NDK linkers:

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 on our source files and filter out the unwanted #include directives.

Automating builds

To make things smarter, this would typically be the job of a script.

My recommendation: use this beautiful makefile.

Now, the set up is as simple as running make init and compilation is as straighforward as typing make all.

What did we compile then?

The Android shared libraries:

  • target/aarch64-linux-android/release/libgreeter.so
  • target/armv7-linux-androideabi/release/libgreeter.so
  • target/i686-linux-android/release/libgreeter.so

The iOS static library:

  • target/universal/release/libgreeter.a

The bindings header:

  • target/bindings.h

We’re all set! Now it’s time for Flutter.

Importing the library on iOS

On the folder ios, create a symbolic link to libgreeter.a.

ln -s ../rust/target/universal/release/libgreeter.a .

And append the contents of bindings.h into ios/Classes/GreeterPlugin.h:

cat ../rust/target/bindings.h >> Classes/GreeterPlugin.h

Note: XCode will skip bundling our library if it doesn’t detect any piece of code using it.

Since our Dart code is out of XCode’s scope, we need to write a dummy Swift function that uses it. Edit ios/Classes/SwiftGreeterPlugin.swift and add a new method like this:

Also, since we will not be using the Flutter messaging channel, the rest of the class methods, can be left empty.

Finally, edit ios/greeter.podspec, so that XCode grabs our Rust artifacts:

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 artifacts on each rebuild, let’s also use symbolic links pointing to their respective architecture’s artifact:

That’s all for Android. Ready for the final step?

Calling the library from Dart

On lib/greeter.dart we need to declare our function binding, load it into a Function typed variable and… well, call it!

First of all, load the library:

Now let’s declare the Dart and FFI signatures of our function and find the symbol we want to use:

Note that the two typedef’s look the same, but for data types other than char*, they can differ. A void function would look like void Function(...) on the first line and Void Function(...) on the second. Certain types are the FFI version to help the loader do its job.

And finally… call it:

Here it is!

That’s one small step for a man, but a giant leap for native code on Flutter.

You can find the GitHub template with all of these steps already done for you:

Photo by Amy Shamblen on Unsplash

But not so fast. That’s not all, yet.

Cleaning after ourselves

As you may know, Rust does not do garbage collection. The language is designed in a way, such that you don’t need to allocate and free memory, 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 received by Dart, in our case. Since in Dart, this allocation will never be cleaned up, we need to do it ourselves.

From Dart, we could allocate some memory, pass a pointer and free it, but since our Rust code is already doing the allocation, it seems cleaner to free it there as well.

Let’s add a new Rust function to take care of that:

All it does is casting the incoming pointer back into a CString, and let it be disposed by the Rust library itself when the scope expires.

We’ll need to run make all in rust again and add the new signature of rust/target/bindings.h into ios/Classes/GreeterPlugin.h.

Then in Dart, add the new typedef’s for cstring_free:

Now, when we are done using the result of rustGreeting(...), we can ask Rust to free it:

If your Rust function returns simple types you don’t need to worry about freeing memory allocations.

Aside note on iOS

Keep in mind that using a few Rust creates may refuse to compile on iOS. Not because they lack support, but because several API’s available on MacOS can’t be used on iOS.

As an example, if you depend on a library that requires libproc to work, the compiler may not find it on the iOS SDK, since Apple does not want you to look at other processes, send signals, etc.

Enjoy the new way

Photo by Kevin Clark on Unsplash

That wraps it for the article. Feel free to use the example app to run and test your new native plugin.

I can’t say it loud enough how excited I am about this technology, and the community seems to be excited too 🎉🎊

As I said, all the hard work is already done for you:

Make sure that the app you make brings a smile to the lucky ones using it and make sure to submit improvements, comments and suggestions to make the template better.

If you found this useful, give some ❤️ back and clap 👏, star ✨ the repo, follow 👍, spread the word 🚀 and what not.

Thank you!

--

--

Brick Pop
Flutter Community

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