Dart Meets Rust: a match made in heaven ✨

shekohex

shekohex

Posted on June 6, 2020

Dart Meets Rust: a match made in heaven ✨

A small piece of Dart

bird
Dart is a client-optimized language for fast apps on any platform, it make it easy to build the UI of your application and it is quite nice language to work with, it the language used by Flutter Framework, Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.

Enter Rust

rust
Rust is blazingly fast and memory-efficient, with no runtime or garbage collector, it can power performance-critical services, run on embedded devices, and easily integrate with other languages.

We are using both Rust and Dart (in Flutter) at Sunshine to enable open-source grant initiatives to easily operate in an on-chain ecosystem.

Almost all of our Code is written in Rust, that's why we needed to think about using the same code and the same logic in our client-side application, but How?

Well, let's see what options we have here

Using Flutter Platform Channels

Flutter Platform channels is a flexible system that allows you to call platform-specific APIs whether available in Kotlin or Java code on Android, or in Swift or Objective-C code on iOS.
this way, we will have to first bind our rust code to Java (for Android), Swift (for iOS), and WASM for the Web, but that would be an over complicated, and maybe that could result a performance issues in the future. Here is a simple graph to get an idea of how it looks like:

Flutter Platform Channels
but as you could see, there is a lot of overhead involved here and data serialization/deserialization is very costly at runtime, so what else we could do?

FFI, break boundaries

as Wikipedia says: A foreign function interface (FFI) is a mechanism by which a program written in one programming language can call routines or make use of services written in another.

hmmm, interesting let's see what we could do, dose Dart support FFI?
Yes!, actually FFI introduced in Dart 2.5 quite recently at the end of last year, so it is still under active development, but quite stable.

After Playing around with FFI Examples with Dart, I started to work on flutterust a simple template to show how to use Flutter/Dart with Rust through FFI.

The simple idea here is that we build our rust code for all supported targets then build a Flutter Package that uses these targets.

And Here is the benefits of using the FFI Method here

  • No Swift/Kotlin wrappers
  • No message passing
  • No async/await on Dart
  • Write once, use everywhere
  • No garbage collection
  • No need to export aar bundles or .framework's

So, it would be like this:

Dart FFI

that is so cool, here is a simple example

Learning How to count!

we are going to use the same flutter hello world example, but instead of doing the logic (incrementing the counter) in the Dart side, we going to do it in the Rust side.

Our Project Sturcutre:

.
├── android
├── ios
├── lib                     <- The Flutter App Code
├── native                  <- Containes all the Rust Code
│   ├── adder
│   └── adder-ffi
├── packages                <- Containes all the Dart Packages that bind to the Rust Code
│   └── adder_ffi
├── target                  <- The compiled rust code for every arch
│   ├── aarch64-apple-ios
│   ├── aarch64-linux-android
│   ├── armv7-linux-androideabi
│   ├── debug
│   ├── i686-linux-android
│   ├── universal
│   ├── x86_64-apple-ios
│   └── x86_64-linux-android
└── test

Enter fullscreen mode Exit fullscreen mode

The Rust Side

Start by creating a Cargo Workspace, so we add a simple Cargo.toml to the root of our Flutter app

[workspace]
members = ["native/*"]

[profile.release]
lto = true
codegen-units = 1
debug = true # turn it off if you want.
Enter fullscreen mode Exit fullscreen mode

Create our simple adder package

$ cargo new --lib native/adder
Enter fullscreen mode Exit fullscreen mode

and let's write some code

pub fn add(a: i64, b: i64) -> i64 {
    a.wrapping_add(b)
}

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(super::add(2, 2), 4);
    }
}
Enter fullscreen mode Exit fullscreen mode

boring, isn't it? 🥱

let's show the world our new add function :)

$ cargo new --lib native/adder-ffi
Enter fullscreen mode Exit fullscreen mode

and don't forget to change it's type in the native/adder-ffi/Cargo.toml

[lib]
name = "adder_ffi"
crate-type = ["cdylib", "staticlib"]

[dependencies]
adder = { path = "../adder" }
Enter fullscreen mode Exit fullscreen mode
// lib.rs

#[no_mangle]
pub extern "C" fn add(a: i64, b: i64) -> i64 {
    adder::add(a, b)
}
Enter fullscreen mode Exit fullscreen mode

Nice, but how to compile our code for the mobile?
Well, it is a bit complicated. We could use cargo directly and it would of course work, but we need to configure a lot of other things, so we will relay on other tools that would do it for us like cargo-lipo and cargo-ndk.

After Compiling our rust code to all of these platforms:

aarch64-apple-ios
aarch64-linux-android
armv7-linux-androideabi
i686-linux-android
x86_64-apple-ios
x86_64-linux-android
Enter fullscreen mode Exit fullscreen mode

we are ready to go to next step, that we will copy our compiled code to specific locations

start first by generating a flutter plugin named after our rust crate:

$ flutter create --template=plugin packages/adder
Enter fullscreen mode Exit fullscreen mode
target/universal/debug/libadder_ffi.a -> packages/adder/ios/libadder_ffi.a
target/aarch64-linux-android/debug/libadder_ffi.so -> packages/adder/android/src/main/jniLibs/arm64-v8a/libadder_ffi.so
...
...other android libs
Enter fullscreen mode Exit fullscreen mode

Are we ready yet? well, technicllay yes, but Xcode has another thing to do like writing a C Header file for our FFI for iOS, if you developing on a macOS you should do these steps here other than that you are ready to go to the next step, writing a Flutter Package to our rust lib.

The Dart Side

so back to Dart, in our generated flutter plugin, we will define how our rust function look like (the type definition) in dart code

import 'dart:ffi';

// For C/Rust
typedef add_func = Int64 Function(Int64 a, Int64 b);
// For Dart
typedef Add = int Function(int a, int b);

Enter fullscreen mode Exit fullscreen mode

and we need a function that loads our rust lib depending on the platform like iOS/Android or Linux/macOS or whatever it is.

import 'dart:io' show Platform;


DynamicLibrary load({String basePath = ''}) {
  if (Platform.isAndroid || Platform.isLinux) {
    return DynamicLibrary.open('${basePath}libadder_ffi.so');
  } else if (Platform.isIOS) {
    // iOS is statically linked, so it is the same as the current process
    return DynamicLibrary.process();
  } else if (Platform.isMacOS) {
    return DynamicLibrary.open('${basePath}libadder_ffi.dylib');
  } else if (Platform.isWindows) {
    return DynamicLibrary.open('${basePath}libadder_ffi.dll');
  } else {
    throw NotSupportedPlatform('${Platform.operatingSystem} is not supported!');
  }
}

class NotSupportedPlatform implements Exception {
  NotSupportedPlatform(String s);
}
Enter fullscreen mode Exit fullscreen mode

and finally create a simple Class that holds our ffi function

class Adder {
  static DynamicLibrary _lib;

  Adder() {
    if (_lib != null) return;
    // for debugging and tests
    if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
      _lib = load(basePath: '../../../target/debug/');
    } else {
      _lib = load();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

and here is the add method

int add(int a, int b) {
    // get a function pointer to the symbol called `add`
    final addPointer = _lib.lookup<NativeFunction<add_func>>('add');
    // and use it as a function
    final sum = addPointer.asFunction<Add>();
    return sum(a, b);
}
Enter fullscreen mode Exit fullscreen mode

so far so good, lets use it in our Flutter app

in the pubspec.yaml of the app, add our adder package under dependencies

adder:
    path: packages/adder_ffi
Enter fullscreen mode Exit fullscreen mode

and in lib/main.dart change the logic of the _incrementCounter method to use our rust logic

import 'package:adder/adder.dart';

// in the `MyHomePage` add 
final adder = Adder();

// and latter in `_MyHomePageState` replace
...
 void _incrementCounter() {
    setState(() {
      _counter = widget.adder.add(_counter, 1);
    });
  }
...
Enter fullscreen mode Exit fullscreen mode

and fire up the Flutter App on Android Emulator or iOS Simulator and Test it 🔥.

phew ..

phew

but we found it is so boring to do that, and especially when it comes to using other build systems like Xcode and Android NDK toolchain and linking everything together 🤦‍♂️. That's why we tried to automate everything, but we need something that is easy to use, cross platform, and CI friendly.

Cargo-make to rescue 🚀

cargo-make is a cross-platform task runner and build tool built in Rust, it is really an Amazing tool to use, it helps you write your workflow in a simple set of tasks, and it has a lot of other cool features like it is easy to add inline scripts in it and a lot more.
you could see how we using it at sunshine-flutter.

That's it, I hope it helped to understand how Dart FFI and Rust works together.

Next Up, How to handle async Rust and Dart FFI
I will leave this to a next blog post, pretty soon :)

For now, you could see that I start hacking on the scrap package that created to demonstrate how we could integrate async Rust with Dart.

Other Intersting Rust + Mobile FFI Development
💖 💪 🙅 🚩
shekohex
shekohex

Posted on June 6, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related