# Rust-C-Dart FFI The goal of this guide is to provide a process for using Rust code in Dart projects for Android and iOS build targets via C ffi. ## Part 1: FFI Intro & Build targets for Android This part covers - how to create C bindings for a custom rust lib - compiling the lib for use on Android targets This example is only tested on linux but the logic is cross platform ### Tooling - cargo - Android ndk ### Install & Setup #### cargo ```bash # Install rust toolchain curl https://sh.rustup.rs -sSf | sh cargo --version ``` #### Android ndk The NDK contains all the tools required to help us build our rust based C library for android targetted hardware. The quickest way to get the ndk is via [Android Studio](https://developer.android.com/studio). Once Android studio is installed, setup a new project to get into the IDE. Find the SDK Manager as an icon on the top right of the screen, OR navigate to `Android Studio > Preferences > Appearance & Behaviour > Android SDK > SDK Tools`. Here you can chose to install the Android NDK. Take note of the installation path and inspect it on the terminal: ``` ls $HOME/Android/Sdk/ndk # The output should show you the of ndk # The tools we would need are specifically at the path below ls $HOME/Android/Sdk/ndk//toolchains/llvm/prebuilt/linux-x86_64/bin ``` Once you confirm the ndk is at the given path and that the bin folder contains a bunch of binaries, add this path to your `PATH` variable for cargo. ``` export PATH=$PATH:$HOME/Android/Sdk/ndk//toolchains/llvm/prebuilt/linux-x86_64/bin ``` It is safe to explicitly specify which specific linker to use per build target. Add the following to your global cargo config @ `$HOME/.cargo/config` to point to the correct linker for each build target. Make sure to substitute with the appropriate ```toml [target.aarch64-linux-android] ar = "~/Android/Sdk/ndk//toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-ar" linker = "~/Android/Sdk/ndk//toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang" [target.armv7-linux-androideabi] ar = "~/Android/Sdk/ndk//toolchains/llvm/prebuilt/linux-x86_64/bin/arm-linux-androideabi-ar" linker = "~/Android/Sdk/ndk//toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi29-clang" [target.i686-linux-android] ar = "~/Android/Sdk/ndk//toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android-ar" linker = "~/Android/Sdk/ndk//toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android29-clang" [target.x86_64-linux-android] ar = "~/Android/Sdk/ndk//toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android-ar" linker = "~/Android/Sdk/ndk//toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android29-clang" [target.x86_64-apple-darwin] linker = "x86_64-apple-darwin14-clang" ar = "x86_64-apple-darwin14-ar" ``` Finally, add toolchains for our build targets ```bash rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android ``` > NOTE: The linker binary for all targets are named slightly differently from their target name, eg: target: `aarch64-linux-android` has linker:`aarch64-linux-android29-clang`, this can change based on the version you have. Inspect your bin to see which versions are available and pick accordingly per linker. ### Write Rust Function With C Binding ```bash cargo new --lib stackmate-core && cd stackmate-core nano Cargo.toml ``` ```toml [package] name = "stackmate-core" version = "0.1.0" edition = "2018" [lib] name = "stackmate-core" crate-type = ["staticlib", "cdylib"] [dependencies] rand = "0.6.0" bip39 = "1.0.1" ``` ```bash nano src/lib.rs ``` Binding a rust function to a C function requires translating input and output types to and from native types. For simlicity, it is best to start with only working with strings, specifically the `CString` type. Considering the conversions to and from CStrings as boiler plate; you can use any snippet of rust code and translate it to a C compatible function. It makes sense to create your own custom use cases of specific libraries by just wrapping its usage into a purely `CString` interface. We are using rand and bip39 to create a menmonic bitcoin seed phrase. Notice that just as the function `mnemonic` converts the `length` input CString input into a native rust type `len`, in `test_mnemonic`,the output `mnemonic_ptr` of the function `mnemonic` is converted into a native rust type `mnemonic_native`. When working with `CString` on the input side, we use an `unsafe` block to extract the value from a pointer* which could potentially be a null and break rust rules. Once you get around the verbosity of it, its not all that intense. ```rust use std::os::raw::{c_char}; use std::ffi::{CString,CStr}; use rand::rngs::OsRng; use bip39::{Language, Mnemonic}; #[no_mangle] pub extern fn mnemonic(length: *const c_char) -> *mut c_char { // convert from CString inputs let input_cstr = unsafe {CStr::from_ptr(length)}; let len:usize = match input_cstr.to_str(){ Err(_) => 12, Ok(string)=> string.parse::().unwrap() }; // regular rust code let mut rng = OsRng::new().expect("!!OsRng Error!!"); let mnemonic = Mnemonic::generate_in_with(&mut rng, Language::English, len) .unwrap() .to_string(); // convert to CString outputs CString::new(mnemonic).unwrap().into_raw() } #[cfg(test)] mod tests { use super::*; #[test] fn test_mnemonic() { let length = 12; let length_cstr = CString::new(length.to_string()).unwrap().into_raw(); let mnemonic_ptr = mnemonic(length_cstr); println!("Output C *char: {:?}",mnemonic_ptr); let mnemonic_cstr = unsafe {CStr::from_ptr(mnemonic_ptr)}; let mnemonic_native = mnemonic_cstr.to_str().unwrap(); println!("Output Rust &str: {}",mnemonic_native); } } ``` ### Test ```bash cargo test -- --nocapture # You will see the mnemonic generated in the output as both a C pointer and Rust native &str ``` ```text ... running 1 test Output C *char: 0x7f86bc000cf0 Output Rust &str: blade rose lift later expand math story broccoli damp cruel lava video test tests::test_mnemonic ... ok ``` ### Build for Android Target cargo can compile binaries for specific target hardware. Check the list of [Supported Platforms](https://doc.rust-lang.org/nightly/rustc/platform-support.html). Our targets are all under Tier 2. ```bash cd path/to/project cargo clean cargo build --target aarch64-linux-android --release cargo build --target armv7-linux-androideabi --release cargo build --target i686-linux-android --release cargo build --target x86_64-linux-android --release ``` ## Debugging tips Incase you bump into errors along the way, before rebuilding clean up any artifacts from previous builds. ```bash rm -rf stackmate-core/target cargo clean ``` Check if you have the correct linker binaries by running them manually: ```bash $HOME/Android/Sdk/ndk//toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang --version ``` ## Part 2: Using Android builds with Dart-C FFI In the next part, we will call this C library via Dart on an Android device. ## Part 3: Build Targets for iOS Then we will build the same library for iOS target ## Part 4: Using iOS builds with Dart-C FFI Then we will call this C library via Dart on an iOS device. ## Part 5: Automating for easier development Finally, we will create a script to facilitate continous development.