A small piece of Dart
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 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:
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:
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 ..
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
- https://dart.dev/guides/libraries/c-interop
- https://flutter.dev/docs/development/platform-integration/c-interop
- https://github.com/dart-lang/samples/blob/master/ffi/structs/structs.dart
- https://mozilla.github.io/firefox-browser-architecture/experiments/2017-09-06-rust-on-ios.html
- https://mozilla.github.io/firefox-browser-architecture/experiments/2017-09-21-rust-on-android.html
Top comments (8)
Neat!
Very nice walkthrough! I'm just starting to learn Flutter but would it be possible to use the same logic to target the web platform with webassembly ? Having the codebase in rust and targeting all 3 platform would be amazing.
Yes, wasm-bindgen with a samll Javascript glue and you can hook it into your dart/flutter, should be cool and easy.
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 :)
Thanks, Very cool! but Can we call an async function in rust with flutter?
See my other post dev.to/sunshine-chain/rust-and-dar...
alternative: rust in flutter: rinf: github.com/cunarist/rinf