Rust + Flutter for Mobile Applications

As next week will have importat launch for Rust 2018 and Flutter 1.0, I thought to build an app using Rust for the business logic and Flutter for the user interface, that can run at both Android and iOS, I built one and tested it at Android and it is working fine.

I just wonder how to measure the performance and compare it with native Android/iOS app.

The app flow is:

  1. Main is in Flutter, that is calling native function through platform_channel
  2. The native function is calling rust library through JNI (JNI wrapper is required to be call the rust library)

The structure is as below:

The code used is:

`main.dart:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const platform = const MethodChannel('samples.flutter.io/battery');

  String _batteryLevel = 'Unknown battery level.';

  Future<void> _getBatteryLevel() async {
    String batteryLevel;
    try {
      final String hello = await platform.invokeMethod('getText');
      final int result = await platform.invokeMethod('getBatteryLevel');

      batteryLevel = '$hello Battery level at $result %.';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    setState(() {
      _batteryLevel = batteryLevel;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            RaisedButton(
              child: Text('Get Battery Level'),
              onPressed: _getBatteryLevel,
            ),
            Text(_batteryLevel),
          ],
        ),
      ),
    );
  }
}

JNI wrapper - RustGreetings.kt

package com.mozilla.greetings

class RustGreetings {
    companion object {
        init {
            System.loadLibrary("greetings")
        }
    }

    private external fun greeting(pattern: String): String

    fun sayHello(to: String): String = greeting(to)
}

And the Main Android activity is:

package com.example.batterylevel

import android.os.Bundle
import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.plugin.common.MethodChannel

import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES

import lib.Library
import com.mozilla.greetings.RustGreetings

class MainActivity: FlutterActivity() {
  private val CHANNEL = "samples.flutter.io/battery"

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)

    MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
      if (call.method == "getText") {
        result.success(getText())
      } else if (call.method == "getBatteryLevel") {
       // result.success(getText())
        val batteryLevel = getBatteryLevel()

        if (batteryLevel != -1) {
          result.success(batteryLevel)
        } else {
          result.error("UNAVAILABLE", "Battery level not available.", null)
        }
      }
      else {

        result.notImplemented()
      }
    }
  }

  private fun getBatteryLevel(): Int {
    val batteryLevel: Int
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
      batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    } else {
      val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
      batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
    }
    return batteryLevel
  }

  private fun getText(): String {
    val x = Library().someLibraryMethod()
    val g = RustGreetings()
    val r = g.sayHello("My $x Rust")
    return r
  }
}

In the Android gradle.build I just added the below, as I'm interested to check also the impact of adding kotlin JVM library and getting it interacted with the Rust library within the mobile application:

dependencies {
    implementation(files("src/main/libs/lib.jar"))
}

My question is:
How can check the performance and impact of each process when it is executed or called by another process

3 Likes

A good question.

I would probably write a function that does effectively nothing, in Rust and in Dart, run it 1000 times and compare the result,

A function can be “add one to a number”, and a test would be just calling this function 10000 times from Dart and measuring total time. This should give a lower bound on Dart->JNI->Native overhead.

One need to make sure though that the Dart’s version really calles a function, and not just inlines and constant folds it. One way to do that would be to force a dynamic dispatch when calling a function.

2 Likes

You might be interesting in this dart issue: https://github.com/dart-lang/sdk/issues/34452.

I am personally extremely excited about Rust/Flutter combo, I think it could be both the most advanced and versitale GUI toolkit and a quick hack to get production ready GUI for free.

However, Dart does not have direct C ffi now, only via Java/Objective-C layer, which means a huge drop in performance and a huge increase in accidental complexity.

That issue is about native ffi capabilities to Dart.

8 Likes

Now Dart has ffi in beta:

2 Likes

I started to make a dart-sys crate for making native extensions (not regular ffi) but was demotivated when I found it that dart language extensions don't work with flutter, so I did not continue development.

I think extensions are supoted now at Flutter 1.12,check this link

1 Like

Below a simple example for Rust/Dart integration:

First src/lib.rs

#[no_mangle]
pub extern fn rust_fn(x: i32) -> i32 {   
    println!("Hello from rust\nI'll return: {}", x.pow(2));
    x.pow(2)
 }

and Cargo.toml

[package]
name = "Double_in_Rost"
version = "0.1.0"
authors = ["Hasan Yousef"]
edition = "2018"

[lib]
name = "rust_lib"
crate-type = ["dylib"] # could be `staticlib` as well

[dependencies]

Running cargo build --release will generate target\release\rust_lib.dll copy/paste it into Dart application root directory

Write Dart code as below:

import 'dart:ffi';
import 'dart:io' show Platform;

// FFI signature of the hello_world C function
typedef ffi_func = Int32 Function(Int32 x);  //pub extern fn rust_fn(x: i32) -> i32
// Dart type definition for calling the C foreign function
typedef dart_func = int Function(int x);

void main() {
  // Open the dynamic library
  var path = './rust_lib.so';
  if (Platform.isMacOS) path = './rust_lib.dylib';
  if (Platform.isWindows) path = 'rust_lib.dll';
  final dylib = DynamicLibrary.open(path);
  // Look up the Rust/C function
  final my_func =
      dylib.lookup<NativeFunction<ffi_func>>('rust_fn').asFunction<dart_func>();
  print('Double of 3 is ${my_func(3)}');
}

So, in dart, it is simple, but how to use in flutter, have any suggest? I tried times in flutter, but not work. :sob:

Did you try the same example in my answer!

We also tried it, we tried to put the so file (linux) everywhere, but we alwyas get the error:
I/flutter ( 3954): Invalid argument(s): Failed to load dynamic library (dlopen failed: library
I/flutter ( 3954): "librust_integration_test.so" not found)

Any idea?

It should be saved at:

android/app/src/main/jniLibs/X folder, where X is architecture name

And in the Gradle Build, you need to add this directory main.jniLibs.srcDirs = ['src/main/jniLibs'] to the source set, so that the source set become:

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
        main.jniLibs.srcDirs = ['src/main/jniLibs']
    }

And for iOS, add it at:

General -> Linked Frameworks and Libraries

lib.rs:

#[no_mangle]
pub extern fn native_double(x: i32) -> i32 {
    x.pow(2)
}

use std::os::raw::{c_char};
use std::ffi::{CString, CStr};

#[no_mangle]
pub extern fn native_greeting(to: *const c_char) -> *mut c_char {
    let c_str = unsafe { CStr::from_ptr(to) };
    let recipient = match c_str.to_str() {
        Err(_) => "there",
        Ok(string) => string,
    };

    CString::new("Hello ".to_owned() + recipient).unwrap().into_raw()
}

Cargo.toml:

[package]
name = "app"
version = "0.1.0"
authors = ["Hasan Yousef"]
edition = "2018"

[lib]
name = "native" # generated library name will be: "libnative"
crate-type = ["dylib", "staticlib"] # "dylib" for Android, "staticlib" for iOS

[dependencies]

.cargo/config:

[target.i686-linux-android]
ar = "/Users/hasan/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/bin/i686-linux-android-ar"
linker = "/Users/hasan/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/bin/i686-linux-android29-clang"

[target.x86_64-linux-android]
ar = "/Users/hasan/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/bin/x86_64-linux-android-ar"
linker = "/Users/hasan/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/bin/x86_64-linux-android29-clang"

[target.aarch64-linux-android]
ar = "/Users/hasan/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-ar"
linker = "/Users/hasan/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android29-clang"

[target.armv7-linux-androideabi]
ar = "/Users/hasan/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-ar"
linker = "/Users/hasan/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi29-clang"

Add targets:

$ rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android arm-linux-androideabi

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

Build Android static libraries, Note aarch64 can be used for both arm64 and arm64-v8a:

$ cargo build --target x86_64-linux-android --release
$ cargo build --target aarch64-linux-android --release
$ cargo build --target armv7-linux-androideabi --release
$ cargo build --target i686-linux-android --release

Install cargo-lipo to generate iOS libraries, and the universal iOS library can be found in cargo/target/universal/release/libgnative.a

$ cargo install cargo-lipo

Build iOS static libraries:

$ cargo lipo --release

You need to be sure you installed Android NDK:

1 Like

@hyousef,

I found that the info you shared is very helpful to build rust native library for android/ios apps so that I summarized the workflow in Gist:

Hope this helps somebody (and please let me know if anyone has advise/comment).

4 Likes

Well done.

1 Like

I recently used Rust + Flutter as an example, but I encountered a problem. Can't find the symbol in the IOS meeting?
can you help me?

https://github.com/brickpop/flutter-rust-ffi/issues/22