loading...
Cover image for Dart Meets Rust: a match made in heaven ✨
Sunshine

Dart Meets Rust: a match made in heaven ✨

shekohex profile image Shady Khalifa ・7 min read

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

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.

Create our simple adder package

$ cargo new --lib native/adder

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);
    }
}

boring, isn't it? 🥱

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

$ cargo new --lib native/adder-ffi

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" }
// lib.rs

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

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

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
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

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);

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);
}

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();
    }
  }
}

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);
}

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

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);
    });
  }
...

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

Posted on Jun 6 by:

shekohex profile

Shady Khalifa

@shekohex

My name is Shady khalifa I am an Electrical engineer and Web Developer Love Rust, NodeJS and Typescript

Discussion

markdown guide
 
 

What are some use cases where using rust and Dart makes more sense than platform integrations and vice versa?

 

I think our use case is not so especial, as I said our code base almost entirely written in Rust, and building FFI for rust and using tools to even generate the binding for us is really amazing, and easier than write two binding one for java using NDK and the other one for Swift which I guess also would require writing binding too, we going to just to ignoring that there is overhead to serialization/deserialization because channels using json message passing, which is not so bad, but it wouldn't be easy as the FFI approach.

Just Write once deploy everywhere :)