This short doc shows how to build rust to be used by flutter/android app.
Inspired by hyousef's work but added some details for the workflow.
Docs:
- https://dart.dev/guides/libraries/c-interop
- https://flutter.dev/docs/development/platform-integration/c-interop
Conversations:
- https://stackoverflow.com/questions/53532964/integrating-rust-flutter-kotlin-for-mobile-applications
- https://users.rust-lang.org/t/rust-flutter-for-mobile-applications/22725/10
- Ubuntu 18.04 or later
- Rust SDK
- Android SDK, NDK, LLVM, and CMake
- Flutter SDK as beta channel
We don't need to generate standalone NDK tools anymore but need to specify where the prebuilt tools are.
To find the archiver's location:
$ find -name "*aarch64-linux-android-ar*"
./Android/Sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-ar
To find the linker's location:
$ find -name "*aarch64-linux-android29-clang*"
./Android/Sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang
Create a config file under ~/.cargo:
$ vi ~/.cargo/config
Then fill the file with:
[target.aarch64-linux-android]
ar = "Android/Sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-ar"
linker = "Android/Sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang"
This needs to be done once per host machine.
$ rustup target add aarch64-linux-android
$ mkdir rustylib && cd rustylib
$ cargo init --name rustylib --lib
[package]
name = "rustylib"
version = "0.1.0"
authors = ["bus710 <[email protected]>"]
edition = "2018"
[lib]
name = "rustylib"
crate-type = ["dylib"] # could be `staticlib` as well
[dependencies]
This function accepts an int and returns the input's power value.
#[no_mangle]
pub extern fn rust_fn(x: i32) -> i32 {
println!("Hello from rust\nI'll return: {}", x.pow(2));
x.pow(2)
}
Build the code but target the architecture:
$ cargo build --target aarch64-linux-android --release
Then there are files as output:
$ tree
.
├── cargo-config.toml
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── target
└── aarch64-linux-android
└── release
├── build
├── deps
│ ├── librustylib.so
│ └── rustylib.d
├── examples
├── incremental
├── librustylib.d
└── librustylib.so
The librustylib.so is shared library as the ultimate output to be copied to and used by Flutter app.
To check the format:
$ file target/aarch64-linux-android/release/librustylib.so
target/aarch64-linux-android/release/librustylib.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, with debug_info, not stripped
A new flutter project should be created:
$ cd ..
$ flutter create caller
$ tree -L 2
.
├── caller
│ ├── android
│ ├── build
│ ├── caller.iml
│ ├── ios
│ ├── lib
│ ├── pubspec.lock
│ ├── pubspec.yaml
│ ├── README.md
│ ├── test
│ └── web
└── rustylib
├── Cargo.lock
├── Cargo.toml
├── src
└── target
Create a directory and copy the shared library file to the new directory:
$ mkdir -p caller/android/app/src/main/jniLibs/arm64-v8a
$ cp rustylib/target/aarch64-linux-android/release/librustylib.so \
caller/android/app/src/main/jinLibs/arm64-v8a
Open the build.gradle under android/app:
$ vi caller/android/app/build.gradle
Add a line in the android/sourceSets block:
android {
compileSdkVersion 28
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
main.jniLibs.srcDirs = ['src/main/jniLibs'] // this!
}
...
}
Below snippet shows how to use the rust library from flutter without kotlin wrapper:
import 'package:flutter/material.dart';
import 'dart:ffi';
// 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() => 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> {
int _counter = 0;
String path;
DynamicLibrary dylib;
Function myFunc;
@override
void initState() {
// Open the dynamic library
path = 'librustylib.so';
dylib = DynamicLibrary.open(path);
// Look up the Rust/C function
myFunc = dylib
.lookup<NativeFunction<ffi_func>>('rust_fn')
.asFunction<dart_func>();
super.initState();
}
void _incrementCounter() {
setState(() {
_counter++;
debugPrint('Double of 3 is ${myFunc(3)}');
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
So there will be message "Double of 3 is 9" in the debug console whenever the floating button is pressed.
To automate this little tedious steps, please refer this repo by brickpop: