【Rust blog】Rust + Flutter 高性能的跨端嘗試

  • 2020 年 2 月 21 日
  • 筆記

本文為 heymind 的翻譯投稿。

稍作配置,同一份代碼橫跨 Android & IOS,相比於 React Native 方案更加高性能。除此之外,得益於 Rust 跨平台加持,Rust 部分的代碼可在種種場合復用。

這篇文章旨在記錄作者嘗試結合 Rust 和 Flutter 的過程,且僅為初步嘗試。不會涉及諸如:

  • 如何搭建一個 Flutter 開發環境,以及 Dart 語言怎麼用
  • 如何搭建一個 Rust 開發環境,以及 Rust 語言怎麼學

Environment

  • Flutter: Android, IOS 工具配置妥當
  • Rust: Stable 就好

Rust Part

Prepare cross-platform toolchains & deps

IOS

# Download targets for IOS ( 64 bit targets (real device & simulator) )  rustup target add aarch64-apple-ios x86_64-apple-ios    # Install cargo-lipo to generate the iOS universal library  cargo install cargo-lipo  

Android

這裡有一些行之有效的輔助腳本用於更加快捷配置交叉編譯工具。

  1. 獲取 Android NDK sdkmanager --verbose ndk-bundle 如果已經準備好了 Android NDK ,則設置環境變量 $ANDROID_NDK_HOME # example: export ANDROID_NDK_HOME=/Users/yinsiwei/Downloads/android-ndk-r20b
  2. Create the standalone NDK # $(pwd) == ~/Downloads git clone https://github.com/kennytm/rust-ios-android.git cd rust-ios-android ./create-ndk-standalone.sh
  3. 在 Cargo default config VS 配置 Android 交叉編譯工具 cat cargo-config.toml >> ~/.cargo/config 執行上述命令後會在 Cargo 默認配置中,增加有關 Android 跨平台目標 (targets, aarch64-linux-android, armv7-linux-androideabi, i686-linux-android) 的工具信息,指向剛剛創建的 standalone NDK[target.aarch64-linux-android] ar = ... linker = .. [target.armv7-linux-androideabi] ... [target.i686-linux-android] ..
  4. 下載 Rust 支持 Android 交叉編譯的依賴
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android  

Start a simple rust library

  1. 創建一個 Rust 項目
 cargo init my-app-base --lib  
  1. 編輯 Cargo.toml 修改 crate-type
[lib]  name = "my_app_base"  crate-type = ["staticlib", "cdylib"]  
Rust 構建出來的二進制庫,在 IOS 中是靜態鏈接進最終的程序之中,需要對構建 `staticlib` 的支持;在 Android 是通過動態鏈接在運行時裝在進程序運行空間的,需要對構建 `cdylib` 的支持。  
  1. 寫一些符合 C ABI 的函數 src/lib.rs use std::os::raw::c_char; use std::ffi::CString; #[no_mangle] pub unsafe extern fn hello() -> *const c_char { let s = CString::new("world").unwrap(); s.into_raw() } 在上述代碼中,每次當外部調用 hello 函數時,會在晉城堆空間中創建一個字符串 ( CString ),並將所有權 ( 釋放該字符串所佔堆空間的權利 ) 移交給調用者

Build libraries

# IOS  cargo lipo --release    # Android  cargo build --target aarch64-linux-android --release  cargo build --target armv7-linux-androideabi --release  cargo build --target i686-linux-android --release  

然後在 target 目錄下會得到以下有用的物料。

target      ├── aarch64-linux-android      │   └── release      │       ├── libmy_app_base.a      │       └── libmy_app_base.so      ├── armv7-linux-androideabi      │   └── release      │       ├── libmy_app_base.a      │       └── libmy_app_base.so      ├── i686-linux-android      │   └── release      │       ├── libmy_app_base.a      │       └── libmy_app_base.so      ├── universal      │   └── release      │       └── libmy_app_base.a  

至此, Rust 部分就告於段落了。

Flutter Part

Copy build artifacts to flutter project

from: target/universal/release/libmy_app_base.a  to: ios/    from: target/aarch64-linux-android/release/libmy_app_base.so  to: android/app/src/main/jniLibs/arm64-v8a/    from: target/armv7-linux-androideabi/release/libmy_app_base.so  to: android/app/src/main/jniLibs/armeabi-v7a/    from: target/i686-linux-android/release/libmy_app_base.so  to: android/app/src/main/jniLibs/x86/  

Call FFI function in Dart

  1. 添加依賴 pubspec.yaml -> dev_dependencies: += ffi: ^0.1.3
  2. 添加代碼 (直接在生成的項目上修改,暫不考慮代碼設計問題,就簡簡單單的先把項目跑起來 ) import 'dart:ffi'; import 'package:ffi/ffi.dart'; // ... final dylib = Platform.isAndroid ? DynamicLibrary.open('libmy_app_base.so') :DynamicLibrary.process(); var hello = dylib.lookupFunction<Pointer<Utf8> Function(),Pointer<Utf8> Function()>('hello'); // ... hello(); // -> world

Build Android Project

flutter run # 如果連接着 Android 設備就直接運行了起來  

Build IOS Project

( 複雜了許多 )

  1. 跟隨 Flutter 官方文檔,配置 XCode 項目。
  2. Build PhasesLink Binary With Libraries 添加 libmy_app_base.a 文件 (按照圖上箭頭點…)
  1. Build SettingsOther Linker Flags 中添加 force_load 的參數。

這是由於在 Dart 中通過動態的方式調用了該庫的相關函數,但在編譯期間靜態分析的時候,這些都是未曾被調用過的無用函數,就被剪裁掉了。要通過 force_load 方式解決這個問題。

Result

Troubleshooting

XCode & IOS

Error getting attached iOS device: ideviceinfo could not find device

sudo xattr -d com.apple.quarantine ~/flutter/bin/cache/artifacts/libimobiledevice/ideviceinfo  

將後面的路徑替換成你的

dyld: Library not loaded

dyld: Library not loaded: /b/s/w/ir/k/homebrew/Cellar/libimobiledevice-flutter/HEAD-398c120_3/lib/libimobiledevice.6.dylib    Referenced from: /Users/hey/flutter/bin/cache/artifacts/libimobiledevice/idevice_id    Reason: image not found  

刪除&重新下載

rm -rf /Users/hey/flutter/bin/cache && flutter doctor -v  

真機無法啟動 Flutter 程序

參見 https://github.com/flutter/flutter/issues/49504#issuecomment-581554697 不要升級到 IOS 13.3.1 系統

What's next

  • 如何高效的實現 Rust & Dart 部分的通信 我們知道 Flutter 和廣大 GUI 庫類似,屬於單線程模型結合事件系統,因此在主線程中使用 FFI 調用 Rust 部分的代碼不能阻塞線程。Dart 語言提供 async/await 語法特性用於在 Flutter 中處理網絡請求等阻塞任務。而 Rust 也在最近版本中提供了 async/await 語法支持,如何優雅的把兩部分結合起來,這是一個問題。
  • 對 MacOS Windows Linux 桌面端的支持 Flutter 已經有了對桌面端的實驗性支持,可以研究下如何結合在一起,實現跨 6 個端共享代碼。

References

  • https://github.com/kennytm/rust-ios-android 介紹了如何構建出 Android, IOS 庫,並提供了例子
  • https://github.com/TimNN/cargo-lipo 用於構建 universal library
  • https://github.com/hanabi1224/flutter_native_extensions

原文鏈接:https://idx0.dev/2020/02/15/flutter-rust-1/