來源:Rust & cross-platform mobile development
Rust & cross-platform mobile development
Rust 與跨平臺移動開發(fā)
Recently I started to investigate options to share business logic between Android & iOS. This investigation leads me to Rust — very interesting and relatively new programing language so I decided to give it a try.
最近我開始研究 Android 和 iOS 之間共享業(yè)務(wù)邏輯的選項(xiàng)缰趋。這次調(diào)查讓我想到了 Rust ——非常有趣且相對較新的編程語言孽拷,所以我決定試一試。
What is Rust?
Rust 是什么知牌?
Two most important point from the documentation:
文檔中最重要的兩點(diǎn):
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.
Rust 的運(yùn)行速度非逞下簦快森书,內(nèi)存效率也非常高:沒有運(yùn)行時或垃圾回收器秀仲,它可以對重視性能的服務(wù)提供支持默色,可以運(yùn)行在嵌入式設(shè)備上球凰,并且很容易與其他語言集成。
It is a native-level language like C++.
它是一種類似 C++ 的原生語言腿宰。
Rust’s rich type system and ownership model guarantee memory-safety and thread-safety — enabling you to eliminate many classes of bugs at compile-time.
Rust 豐富的類型系統(tǒng)和所有權(quán)模型保證了內(nèi)存安全和線程安全——使您能夠在編譯時消除許多類型的 bug呕诉。
Its compiler will try to save you from common memory mistakes.
它的編譯器將試圖從常見的內(nèi)存錯誤中拯救你。
Is it popular?
它流行嗎吃度?
Based on the 2019 survey, Rust is one of the most loved & wanted among engineers (why?):
根據(jù) 2019 年的調(diào)查甩挫,Rust 是工程師中最受歡迎和最想要的 (為什么?):
General trends are not that great:
總體趨勢并沒有那么好:
The first appearance of the language was in 2010 almost at the same time as Go (2009). Version 1.0 was released in 2015, but they are still adding a lot of new features based on user demand.
該語言的首次出現(xiàn)是在 2010 年,幾乎與 Go 同時出現(xiàn)(2009年)椿每。版本 1.0 于 2015 年發(fā)布伊者,但他們?nèi)栽诟鶕?jù)用戶需求添加許多新功能。
Unfortunately, so far it is used only in a few big companies.
不幸的是间护,到目前為止亦渗,它只在少數(shù)幾家大公司中使用。
How good is it?
它有多好?
Probably the first thing you should worry about is performance. Rust is probably one of the best, here are some benchmarking (left to right):
也許你首先應(yīng)該擔(dān)心的是性能汁尺。Rust可能是最好的一個法精,以下是一些測試 (從左到右):
- Rust vs Go;
- Rust vs Swift;
- Rust vs C++.
On average it is comparable to C/C++ and can be slightly faster than Swift. Of course, it depends on the task and implementation.
平均而言,它與 C/C++ 相當(dāng)均函,可能比 Swift 稍快亿虽。當(dāng)然,這取決于任務(wù)和執(zhí)行情況苞也。
Go or Java is usually 10 positions lower than Rust.
Go 或 Java 通常比 Rust 低 10 個位置洛勉。
Readability
可讀性
Let’s check next code snippet - implementation of the bubble sort:
讓我們檢查下一個代碼片段——冒泡排序的實(shí)現(xiàn):
// C++
#include <algorithm>
#include <iostream>
#include <iterator>
template <typename RandomAccessIterator>
void bubble_sort(RandomAccessIterator begin, RandomAccessIterator end) {
bool swapped = true;
while (begin != end-- && swapped) {
swapped = false;
for (auto i = begin; i != end; ++i) {
if (*(i + 1) < *i) {
std::iter_swap(i, i + 1);
swapped = true;
}
}
}
}
int main() {
int a[] = {100, 2, 56, 200, -52, 3, 99, 33, 177, -199};
bubble_sort(std::begin(a), std::end(a));
copy(std::begin[a], std::end(a), std::ostream_iterator<int>(std::cout, " "));
std::cout << "\n";
}
// Rust
fn bubble_sort<T: Ord>(values: &mut[T]) {
let mut n = values.len();
let mut swapped = true;
while swapped {
swapped = false;
for i in 1..n {
if values[i - 1] > values[i] {
values.swap(i - 1, i);
swapped = true;
}
}
n = n - 1;
}
}
fn main() {
// sort numbers
let mut numbers = [8, 7, 1, 2, 9, 3, 4, 5, 0, 6];
println!("Before: {:?}", numbers);
bubble_sort(&mut numbers);
println!("After: {:?}", numbers);
// sort strings
let mut strings = ["empty", "beach", "art", "car", "deal"];
println!("Before: {:?}", strings);
bubble_sort(&mut strings);
println!("After: {:?}", strings);
}
// Swift
func bubbleSort<T:Compareable>(list: inout[T]) {
var done = false
while !done {
done = true
for i in 1..<list.count {
if list[i - 1] > list[i] {
(list[i], list[i - 1]) = (list[i - 1], list[ii])
done = false
}
}
}
}
var list1 = [3, 1, 7, 5, 2, 5, 3, 8, 4]
print(list1)
bubbleSort(list: &list1)
print(list1)
Syntax wise it is close to Swift;
It is more done in an idiomatic way: readable and understandable.
在語法上,它接近Swift;
它更多的是以一種習(xí)慣的方式來完成:可讀和可理解如迟。
Safety
安全
Another common problem with C++which is addressed in Rust is memory safety. Rust guaranteed memory safety at the compile-time and makes it hard (but still possible) to create a memory leak. At the same time, it provides a rich set of features to manage memory on your own — it can be safe or unsafe.
Rust 中解決的 C++ 的另一個常見問題是內(nèi)存安全收毫。Rust 保證了編譯時的內(nèi)存安全攻走,并使其難以(但仍有可能)產(chǎn)生內(nèi)存泄漏。同時此再,它提供了一組豐富的特性來管理您自己的內(nèi)存——它可以是安全的昔搂,也可以是不安全的。
Mobile
移動開發(fā)
I reviewed the official examples from Rust and many other projects on GitHub, but they definitely were not close to the real mobile application use case. So it was very hard to estimate the complexity of real-life projects or efforts to switch to Rust. That is why I decided to create an example that will cover the most important aspects for me:
- networking;
- multithreading;
- data serialization.
我回顧了 Rust 和 GitHub 上許多其他項(xiàng)目的官方例子输拇,但它們顯然與真正的移動應(yīng)用用例不太接近摘符。所以很難估計(jì)現(xiàn)實(shí)項(xiàng)目的復(fù)雜性或轉(zhuǎn)向 Rust 的努力。這就是為什么我決定創(chuàng)建一個例子策吠,將涵蓋對我來說最重要的方面:
- 網(wǎng)絡(luò)逛裤;
- 多線程;
- 數(shù)據(jù)序列化猴抹。
Backend
后端
For the backend, to simplify efforts I decided to pick StarWars API.
You can create a simple Rust server based on this official example.
對于后端带族,為了簡化工作,我決定選擇 StarWars API蟀给。
您可以根據(jù)這個正式示例創(chuàng)建一個簡單的 Rust 服務(wù)器蝙砌。
Environment
開發(fā)環(huán)境
To set up the environment and create IOS & Android application you can follow the official examples, they are very detailed and simple:
要搭建開發(fā)環(huán)境和創(chuàng)建 IOS 和 Android 應(yīng)用程序,你可以遵循官方的例子跋理,他們是非常詳細(xì)和簡單:
- Rust IOS
- Rust Android
Android example is slightly out-of-date. If you are using NDK 20+, you don’t need to create your own toolchain, you can skip this step:
Android 的例子有點(diǎn)過時了择克。如果你正在使用 NDK 20+,你不需要創(chuàng)建自己的工具鏈薪介,你可以跳過這個步驟:
mkdir NDK
${NDK_HOME}/build/tools/make_standalone_toolchain.py — api 26 — arch arm64 — install-dir NDK/arm64
${NDK_HOME}/build/tools/make_standalone_toolchain.py — api 26 — arch arm — install-dir NDK/arm
${NDK_HOME}/build/tools/make_standalone_toolchain.py — api 26 — arch x86 — install-dir NDK/x86
Instead, add your NDK bundle and precompiled toolchain to PATH:
相反祠饺,將你的 NDK 包和預(yù)編譯工具鏈添加到 PATH:
export NDK_HOME=/Users/$USER/Library/Android/sdk/ndk-bundle
export PATH=$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin:$PATH
And put this in cargo-config.toml
:
然后把這個放到 cargo-config.toml
中:
[target.aarch64-linux-android]
ar = "<NDK_HOME>/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-ar"
linker = "<NDK_HOME>/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang"
[target.armv7-linux-androideabi]
ar = "<NDK_HOME>/toolchains/llvm/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-ar"
linker = "<NDK_HOME>/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi21-clang"
[target.i686-linux-android]
ar = "<NDK_HOME>/toolchains/llvm/prebuilt/darwin-x86_64/bin/i686-linux-android-ar"
linker = "<NDK_HOME>/toolchains/llvm/prebuilt/darwin-x86_64/bin/i686-linux-android21-clang"
Multithreading, HTTP client and data deserialization
多線程,HTTP 客戶端和數(shù)據(jù)反序列化
Rust provides a pretty solid API for networking with next libraries:
- Tokio runtime & Async/.await framework
- Reqwest — simple HTTP client
- Serde — JSON serialization/deserialization library
Rust 提供了一個非持可靠的聯(lián)網(wǎng) API 用以下幾個庫:
- Tokio 運(yùn)行時 & 異步/等待框架
- Reqwest - 簡單的 HTTP 客戶端
- Serde - JSON 序列化/反序列化庫
Here is an example of how you can combine these to create SWAPI(StarWars API) client with a few lines of code:
下面是一個示例道偷,結(jié)合這些你可以用幾行代碼創(chuàng)建 SWAPI(StarWars API) 客戶端:
//Custom threaded runtime
lazy_static! {
static ref RUN_TIME: tokio::runtime::Runtime = tokio::runtime::Builder::new()
.threaded_scheduler()
.enable_all()
.build()
.unwrap();
}
//URL
const DATA_URL_LIST: &str = "https://swapi.dev/api/people/";
//Response DTO
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResponsePeople {
pub count: i64,
pub next: String,
pub results: Vec<People>,
}
//People DTO, i removed a few field to simplify example
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct People {
pub name: String,
pub height: String,
pub mass: String,
pub gender: String,
pub created: String,
pub edited: String,
pub url: String,
}
//async keyword mean that it return Future.
pub async fn load_all_people() -> Result<(ResponsePeople), Box<dyn std::error::Error>> {
println!("test_my_data: start");
let people: ResponsePeople = reqwest::get(DATA_URL_LIST)
.await?
.json()
.await?;
Ok(people)
}
//Test in main
#[tokio::main] //macro to create runtime
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let future = load_all_people();
block_on(future);//block program till future finishes
Ok(())
}
lazy_statlic — A macro for declaring lazily evaluated statics.
lazy_statlic — 一個用于聲明延遲計(jì)算靜態(tài)值的宏。
Communication
通信
We come to the complicated part: communication between IOS/Android and Rust.
For this, we will use FFI. It uses C-interop to do communication and supports only C compatible types. Communication with C-interop could be tricky. IOS & Android has own limitation and best ways of handling this, let’s check it one by one.
我們來到了復(fù)雜的部分:IOS/Android 與 Rust 之間的通信记劈。
為此勺鸦,我們將使用 FFI。它使用 C-interop 進(jìn)行通信目木,只支持 C 兼容的類型换途。與C-interop 的通信可能很棘手。IOS 和 Android 都有自己的局限性和最好的處理方法刽射,讓我們逐一檢查一下军拟。
To simplify data transfer you can also use byte transfer protocols: ProtoBuf, FlatBuffer. Both protocols support Rust, but I exclude them from this exercise because they have a performance overhead.
為了簡化數(shù)據(jù)傳輸,你也可以使用字節(jié)傳輸協(xié)議:ProtoBuf, FlatBuffer誓禁。這兩個協(xié)議都支持 Rust懈息,但我在本文中將它們排除在外,因?yàn)樗鼈冇行阅荛_銷摹恰。
Android
Communication with the Java environment is done through the JNIEnv instance. Here is a simple example which returns a string in the callback in the same thread:
與 Java 環(huán)境的通信是通過 JNIEnv 實(shí)例完成的辫继。下面是一個簡單的例子怒见,它在同一個線程中返回一個回調(diào)字符串:
#[no_mangle]
#[allow(non_snake_case)]
pub extern "C" fn Java_com_rust_app_MainActivity_callback(env: JNIEnv, _class: JClass, callback: JObject) {
let response = env.new_string("Callback from Rust").expect("Couldn't create java string!");
env.call_method(
callback, "rustCallbackResult",
"(Ljava/lang/String;)V",
&[JValue::from(JObject::from(response))]).unwrap();
}
It looks simple, but this method has a limitation. JNIEnv cannot be simply shared between threads because it doesn't implement Send
trait (trait == protocol/interface). If you wrap call_method in separate thread it will fail with a corresponding error. Yes, you can implement Send
on your own as well as Copy
and Clone
but to avoid boilerplate code we can use the rust_swig.
它看起來很簡單,但是這種方法有一個局限性姑宽。JNIEnv 不能簡單地在線程之間共享遣耍,因?yàn)樗鼪]有實(shí)現(xiàn) Send
trait (trait == protocol/interface)。如果將 call_method 包裝在單獨(dú)的線程中炮车,它將失敗并產(chǎn)生相應(yīng)的錯誤舵变。是的,你可以自己實(shí)現(xiàn) Send
瘦穆、Copy
和 Clone
棋傍,但為了避免樣板代碼,我們可以使用 rust_swig难审。
Rust swig is based on the same principles as SWIG — it is using DSL and code generation to provide an implementation for you. Here is an example of pseudocode for Rust SwapiClient we defined before:
Rust swig 基于與 SWIG 相同的原理——它使用 DSL 和代碼生成來為您提供實(shí)現(xiàn)。下面是我們之前定義的 Rust SwapiClient 的偽代碼示例:
foreign_class!(class People {
self_type People;
private constructor = empty;
fn getName(&self) -> &str {
&this.name
}
fn getGender(&self) -> &str {
&this.gender
}
});
foreign_interface!(interface SwapiPeopleLoadedListener {
self_type SwapiCallback + Send;
onLoaded = SwapiCallback::onLoad(&self, s: Vec<People>);
onError = SwapiCallback::onError(&self, s: &str);
});
foreign_class!(class SwapiClient {
self_type SwapiClient;
constructor SwapiClient::new() -> SwapiClient;
fn SwapiClient::loadAllPeople(&self, callback: Box<dyn SwapiCallback + Send>);
});
Besides Rust wrapper, it will generate Java code for you as well, here is an example of auto-generated SwapiClient class:
除了 Rust 包裝器亿絮,它也會為你生成 Java 代碼告喊,這里是一個自動生成 SwapiClient 類的例子:
public final class SwapiClient {
public SwapiClient() {
mNativeObj = init();
}
private static native long init();
public final void loadAllPeople(@NonNull SwapiPeopleLoadedListener callback) {
do_loadAllPeople(mNativeObj, callback);
}
private static native void do_loadAllPeople(long self, SwapiPeopleLoadedListener callback);
public synchronized void delete() {
if (mNativeObj != 0) {
do_delete(mNativeObj);
mNativeObj = 0;
}
}
@Override
protected void finalize() throws Throwable {
try {
delete();
}
finally {
super.finalize();
}
}
private static native void do_delete(long me);
/*package*/ SwapiClient(InternalPointerMarker marker, long ptr) {
assert marker == InternalPointerMarker.RAW_PTR;
this.mNativeObj = ptr;
}
/*package*/ long mNativeObj;
}
The only limitation here, that you’ll need to declare a separate getter method for each field of the DTO. Good point is that it could be declared inside of DSL. The library has a rich list of configurations that you can find in the documentation.
這里唯一的限制是,您需要為 DTO 的每個字段聲明一個單獨(dú)的 getter 方法派昧。好的一點(diǎn)是黔姜,它可以在 DSL 中聲明。這個庫有一個豐富的配置列表蒂萎,您可以在文檔中找到秆吵。
Also, in the rust-swig repo, android-example, you can find integration with Gradle.
另外,在 Android 的 repo 示例中五慈,你可以找到與 Gradle 的集成纳寂。
IOS
Since Swift doesn’t require any proxy (like JNIEnv) to communicate with Rust, we can use FFI directly, but still, there are many options on how to provide access to data:
-
Expose C compatible DTO.
For each DTO you need to create a C-compatible copy and map to it before sending it to Swift.
-
Expose pointer to struct without any fields.
Create a getter for each field in FFI which takes a pointer to the host object as param.
Here is also two possible variations:
- the method can
return
a result from getter; - or you can pass a pointer to populate with value as a parameter; (for C string you’ll need a pointer to start of char array and its length)
- the method can
Let’s check the implementation of both approaches.
由于 Swift 不需要任何代理(如 JNIEnv )來與 Rust 通信,我們可以直接使用 FFI泻拦,但關(guān)于如何提供數(shù)據(jù)訪問毙芜,仍然有很多選擇:
-
暴露 C 兼容的 DTO。
對于每個 DTO争拐,你需要創(chuàng)建一個 C 兼容的副本腋粥,并在將其發(fā)送給 Swift 之前映射到它。
-
將指針暴露給不帶任何字段的結(jié)構(gòu)體架曹。
為 FFI 中的每個字段創(chuàng)建一個 getter,它以一個指向主機(jī)對象的指針作為參數(shù)。
這里還有兩個可能的變體:
- 該方法可以從 getter 返回一個結(jié)果;
- 或者你可以傳遞一個指針來填充值作為參數(shù)这敬;(對于 C 字符串铅鲤,你需要一個指向 char 數(shù)組開始和長度的指針)
讓我們檢查一下這兩種方法的實(shí)現(xiàn)。
Approach #1
實(shí)現(xiàn) #1
//Create client
#[no_mangle]
pub extern "C" fn create_swapi_client() -> *mut SwapiClient {
Box::into_raw(Box::new(SwapiClient::new()))
}
//Release memory
#[no_mangle]
pub unsafe extern "C" fn free_swapi_client(client: *mut SwapiClient) {
assert!(!client.is_null());
Box::from_raw(client);
}
//you need reference to owner context to return data
#[allow(non_snake_case)]
#[repr(C)]
pub struct PeopleCallback {
owner: *mut c_void,
onResult: extern fn(owner: *mut c_void, arg: *const PeopleNativeWrapper),
onError: extern fn(owner: *mut c_void, arg: *const c_char),
}
impl Copy for PeopleCallback {}
impl Clone for PeopleCallback {
fn clone(&self) -> Self {
*self
}
}
unsafe impl Send for PeopleCallback {}
impl Deref for PeopleCallback {
type Target = PeopleCallback;
fn deref(&self) -> &PeopleCallback {
&self
}
}
#[no_mangle]
pub unsafe extern "C" fn load_all_people(client: *mut SwapiClient, outer_listener: PeopleCallback) {
assert!(!client.is_null());
let local_client = client.as_ref().unwrap();
let cb = Callback {
result: Box::new(move |result| {
let mut native_vec: Vec<PeopleNative> = Vec::new();
for p in result {
let native_people = PeopleNative {
name: CString::new(p.name).unwrap().into_raw(),
gender: CString::new(p.gender).unwrap().into_raw(),
mass: CString::new(p.mass).unwrap().into_raw(),
};
native_vec.push(native_people);
}
let ptr = PeopleNativeWrapper {
array: native_vec.as_mut_ptr(),
length: native_vec.len() as _,
};
(outer_listener.onResult)(outer_listener.owner, &ptr);
}),
error: Box::new(move |error| {
let error_message = CString::new(error.to_owned()).unwrap().into_raw();
(outer_listener.onError)(outer_listener.owner, error_message);
}),
};
let callback = Box::new(cb);
local_client.loadAllPeople(callback);
}
On the Swift side we will need to to use UnsafePointer and other variations of a raw pointer to unwrap data:
在 Swift 這邊绳慎,我們將需要使用 UnsafePointer 和其他原始指針的變體來展開數(shù)據(jù):
/Wrapper for Rust SwapiClient
class SwapiLoader {
private let client: OpaquePointer
init() {
client = create_swapi_client()
}
deinit {
free_swapi_client(client)
}
func loadPeople(resultsCallback: @escaping (([People]) -> Void), errorCallback: @escaping (String) -> Void) {
//We cannot call callback from C context, we need to send reference to callback to C
let callbackWrapper = PeopleResponse(onSuccess: resultsCallback, onError: errorCallback)
//pointer to callback class
let owner = UnsafeMutableRawPointer(Unmanaged.passRetained(callbackWrapper).toOpaque())
//C callback results
var onResult: @convention(c) (UnsafeMutableRawPointer?, UnsafePointer<PeopleNativeWrapper>?) -> Void = {
let owner: PeopleResponse = Unmanaged.fromOpaque($0!).takeRetainedValue()
if let data:PeopleNativeWrapper = $1?.pointee {
print("data \(data.length)")
let buffer = data.asBufferPointer
var people = [People]()
for b in buffer {
people.append(b.fromNative())
}
owner.onSuccess(people)
}
}
//C callback error
var onError: @convention(c) (UnsafeMutableRawPointer?, UnsafePointer<Int8>?) -> Void = {
guard let pointer = $1 else {return;}
let owner: PeopleResponse = Unmanaged.fromOpaque($0!).takeRetainedValue()
let error = String(cString: pointer)
owner.onError(error)
}
//Callback struct defined in Rust
var callback = PeopleCallback (
owner: owner,
onResult: onResult,
onError: onError
)
load_all_people(client, callback)
}
}
//Helper to change context from Rust to Swift
class PeopleResponse {
public let onSuccess: (([People]) -> Void)
public let onError: ((String) -> Void)
init(onSuccess: @escaping (([People]) -> Void), onError: @escaping ((String) -> Void)) {
self.onSuccess = onSuccess
self.onError = onError
}
}
//Transform C array [pointe; lenght] to Swift array
extension PeopleNativeWrapper {
var asBufferPointer: UnsafeMutableBufferPointer<PeopleNative> {
return UnsafeMutableBufferPointer(start: array, count: Int(length))
}
}
A reasonable question would be here: why do we need PeopleResponse
a class in swift and corresponding PeopleCallback
struck in swift? Basically to avoid this:
一個合理的問題在這里:為什么在 Swift 中我們需要一個 PeopleResponse
類和相應(yīng)的 PeopleCallback
回掉纵竖?基本上是為了避免這種情況:
You need to send callback object to native code and return it back with the result:
你需要發(fā)送回調(diào)對象到原生代碼漠烧,并返回結(jié)果:
Approach #2
實(shí)現(xiàn) #2
In this case, we don’t use PeopleNative
, we will use original People struct from Rust, but we will not expose any field to the client, instead, we will create methods that will accept a pointer to DTO and return required member. Note, we will still need to wrap arrays and callbacks as in the previous example.
Here are only getter methods, everything else is pretty the same:
在這種情況下,我們不使用 PeopleNative
靡砌,我們將使用 Rust 原生的 People
結(jié)構(gòu)體已脓,但我們不會公開任何字段給客戶端,相反通殃,我們將創(chuàng)建方法度液,接受 DTO 指針,并返回所需的成員画舌。注意堕担,我們?nèi)匀恍枰袂懊娴氖纠心菢影b數(shù)組和回調(diào)。
這里只有 getter 方法曲聂,其他的都是一樣的:
//return name
pub unsafe extern "C" fn people_get_name(person: *mut People) -> *mut c_char {
debug_assert!(!person.is_null());
let person = person.as_ref().unwrap();
return CString::new(person.name.to_owned()).unwrap().into_raw();
}
//Or you can accept pointer to name as param
#[no_mangle]
pub unsafe extern "C" fn people_get_name_(
person: *const People,
name: *mut *const c_char,
length: *mut c_int,
) {
debug_assert!(!person.is_null());
let person = &*person;
//to rebuild string you need content and lenght.
*name = person.name.as_ptr() as *const c_char;
*length = person.name.len() as c_int;
}
Generate headers
生成頭文件
After you finished defining FFI you can generate header like this:
在你完成 FFI 的定義后霹购,你可以像這樣生成頭文件:
cargo install cbindgen #install cbindgen if you don’t have it
#generate a header which you need to incluede in IOS project cbindgen -l C -o src/swapi.h
As well you can create a build configuration in build.rs
to automate this process:
同樣,您可以在 build.rs
中創(chuàng)建一個配置朋腋。將此過程自動化:
cbindgen::Builder::new()
.with_crate(crate_dir)
.with_language(C)
.generate()
.expect("Unable to generate bindings")
.write_to_file("src/greetings.h");
If Android {} else IOS {}
To decouple IOS and Android-specific logic, dependencies and etc. you can use macros (example):
為了解耦 IOS 和 Android 的特定邏輯齐疙、依賴等,你可以使用宏(例如):
#[cfg(target_os=”android”)]
#[cfg(target_os=”ios”)]
The easiest way to separate concerns is to have a separate macro on top of the file — one module per platform. I found this a little messy, especially because you cannot use it in build.rs
, so I separated a platform-specific logic in different projects from the core.
分離關(guān)注點(diǎn)的最簡單方法是在文件的頂部有一個單獨(dú)的宏——每個平臺一個模塊旭咽。我發(fā)現(xiàn)這有點(diǎn)亂贞奋,特別是因?yàn)槟悴荒茉跇?gòu)建中使用它。因此穷绵,我將不同項(xiàng)目中的特定于平臺的邏輯與核心分離開來轿塔。
Benchmarking
基準(zhǔn)測試
Size
Both projects measured only with Rust related code and UI.
兩個項(xiàng)目都只使用 Rust 相關(guān)代碼和 UI 進(jìn)行衡量。
Android debug API and shared libraries:
Android 調(diào)試 API 和共享庫:
IOS debug app and shared library:
IOS 調(diào)試 app 和共享庫:
Speed
速度
Load time of Rust standalone solution, its bridges called through Android & iOS and Swift & Kotlin native solutions implementation of the same network call:
Rust 獨(dú)立解決方案的加載時間仲墨、通過 Android&iOS 橋接和 Swift & Kotlin 原生解決方案實(shí)現(xiàn)相同的網(wǎng)絡(luò)調(diào)用:
iOS solution is using URL, URLSession, and Codable;
Android is using coroutines with kotlinx.serialization.
iOS 方案使用 URL, URLSession, 和 Codable勾缭;
Android 使用 kotlinx.serialization 的 coroutines。
As you see there almost none difference between calling Rust standalone solution or calling it through Andorid&Swift. This means that FFI doesn’t create any performance overhead.
如您所見宗收,調(diào)用 Rust 獨(dú)立解決方案或通過 Andorid&Swift 調(diào)用它幾乎沒有區(qū)別漫拭。 這意味著 FFI 不會增加任何性能開銷。
Note: speed of request highly depends on the server latency.
You can find both implementations in the GitHub project.
注意:請求的速度很大程度上取決于服務(wù)器的延遲混稽。
你可以在GitHub項(xiàng)目中找到這兩種實(shí)現(xiàn)采驻。
Project
項(xiàng)目
A full example of the project is available on GitHub:
GitHub 上有一個完整的項(xiàng)目示例:
xajik/rust-cross-platform-mobile
IOS & Android UI
Summary
Rust is a very promising language, that gives you extremely high speed while taking care of common to C++ memory issues. Solid and simple API makes it easy to use and learn, between C++ and Rust, I would definitely pick last one, but it is still more complicated than Swift or Kotlin.
The biggest challenge is to build a proper bridge between Rust and client frameworks, if you can live with it — it could be a great solution for mobile.
Rust 是一種非常有前途的語言,它在處理常見的 C++ 內(nèi)存問題的同時匈勋,提供了極高的速度礼旅。堅(jiān)實(shí)和簡單的 API 使它易于使用和學(xué)習(xí),在 C++ 和 Rust 之間洽洁,我肯定會選擇后一個痘系,但它仍然比 Swift 或 Kotlin 更復(fù)雜。
最大的挑戰(zhàn)是在 Rust 和客戶端框架之間建立一個適當(dāng)?shù)臉蛄憾鲎裕绻隳芙邮芩赡苁且粋€很好的移動解決方案汰翠。
Reference:
參考
My previous investigation: Go + Gomobile for Android and IOS.
我之前的研究: Go + Gomobile for Android and IOS.
Implementation and benchmarking.
實(shí)現(xiàn)和基準(zhǔn)測試龄坪。