来源:Rust & cross-platform mobile development
Rust & cross-platform mobile development
Rust 与跨平台移动开发
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 之间共享业务逻辑的选项。这次调查让我想到了 Rust ——非常有趣且相对较新的编程语言,所以我决定试一试。
What is Rust?
Rust 是什么?
Two most important point from the documentation:
文档中最重要的两点:
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 的运行速度非常快,内存效率也非常高:没有运行时或垃圾回收器,它可以对重视性能的服务提供支持,可以运行在嵌入式设备上,并且很容易与其他语言集成。
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 丰富的类型系统和所有权模型保证了内存安全和线程安全——使您能够在编译时消除许多类型的 bug。
Its compiler will try to save you from common memory mistakes.
它的编译器将试图从常见的内存错误中拯救你。
Is it popular?
它流行吗?
Based on the 2019 survey, Rust is one of the most loved & wanted among engineers (why?):
根据 2019 年的调查,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.
该语言的首次出现是在 2010 年,几乎与 Go 同时出现(2009年)。版本 1.0 于 2015 年发布,但他们仍在根据用户需求添加许多新功能。
Unfortunately, so far it is used only in a few big companies.
不幸的是,到目前为止,它只在少数几家大公司中使用。
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):
也许你首先应该担心的是性能。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++ 相当,可能比 Swift 稍快。当然,这取决于任务和执行情况。
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:
让我们检查下一个代码片段——冒泡排序的实现:
// 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;
它更多的是以一种习惯的方式来完成:可读和可理解。
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++ 的另一个常见问题是内存安全。Rust 保证了编译时的内存安全,并使其难以(但仍有可能)产生内存泄漏。同时,它提供了一组丰富的特性来管理您自己的内存——它可以是安全的,也可以是不安全的。
Mobile
移动开发
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 上许多其他项目的官方例子,但它们显然与真正的移动应用用例不太接近。所以很难估计现实项目的复杂性或转向 Rust 的努力。这就是为什么我决定创建一个例子,将涵盖对我来说最重要的方面:
- 网络;
- 多线程;
- 数据序列化。
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。
您可以根据这个正式示例创建一个简单的 Rust 服务器。
Environment
开发环境
To set up the environment and create IOS & Android application you can follow the official examples, they are very detailed and simple:
要搭建开发环境和创建 IOS 和 Android 应用程序,你可以遵循官方的例子,他们是非常详细和简单:
- 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 的例子有点过时了。如果你正在使用 NDK 20+,你不需要创建自己的工具链,你可以跳过这个步骤:
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 包和预编译工具链添加到 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 客户端和数据反序列化
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 提供了一个非常可靠的联网 API 用以下几个库:
- Tokio 运行时 & 异步/等待框架
- 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:
下面是一个示例,结合这些你可以用几行代码创建 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 — 一个用于声明延迟计算静态值的宏。
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.
我们来到了复杂的部分:IOS/Android 与 Rust 之间的通信。
为此,我们将使用 FFI。它使用 C-interop 进行通信,只支持 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.
为了简化数据传输,你也可以使用字节传输协议:ProtoBuf, FlatBuffer。这两个协议都支持 Rust,但我在本文中将它们排除在外,因为它们有性能开销。
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 环境的通信是通过 JNIEnv 实例完成的。下面是一个简单的例子,它在同一个线程中返回一个回调字符串:
#[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 不能简单地在线程之间共享,因为它没有实现 Send
trait (trait == protocol/interface)。如果将 call_method 包装在单独的线程中,它将失败并产生相应的错误。是的,你可以自己实现 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 和代码生成来为您提供实现。下面是我们之前定义的 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 的每个字段声明一个单独的 getter 方法。好的一点是,它可以在 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,但关于如何提供数据访问,仍然有很多选择:
-
暴露 C 兼容的 DTO。
对于每个 DTO,你需要创建一个 C 兼容的副本,并在将其发送给 Swift 之前映射到它。
-
将指针暴露给不带任何字段的结构体。
为 FFI 中的每个字段创建一个 getter,它以一个指向主机对象的指针作为参数。
这里还有两个可能的变体:
- 该方法可以从 getter 返回一个结果;
- 或者你可以传递一个指针来填充值作为参数;(对于 C 字符串,你需要一个指向 char 数组开始和长度的指针)
让我们检查一下这两种方法的实现。
Approach #1
实现 #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 和其他原始指针的变体来展开数据:
/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
类和相应的 PeopleCallback
回掉?基本上是为了避免这种情况:
You need to send callback object to native code and return it back with the result:
你需要发送回调对象到原生代码,并返回结果:
Approach #2
实现 #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
结构体,但我们不会公开任何字段给客户端,相反,我们将创建方法,接受 DTO 指针,并返回所需的成员。注意,我们仍然需要像前面的示例中那样包装数组和回调。
这里只有 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
中创建一个配置。将此过程自动化:
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.
分离关注点的最简单方法是在文件的顶部有一个单独的宏——每个平台一个模块。我发现这有点乱,特别是因为你不能在构建中使用它。因此,我将不同项目中的特定于平台的逻辑与核心分离开来。
Benchmarking
基准测试
Size
Both projects measured only with Rust related code and UI.
两个项目都只使用 Rust 相关代码和 UI 进行衡量。
Android debug API and shared libraries:
Android 调试 API 和共享库:
IOS debug app and shared library:
IOS 调试 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 独立解决方案的加载时间、通过 Android&iOS 桥接和 Swift & Kotlin 原生解决方案实现相同的网络调用:
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.
如您所见,调用 Rust 独立解决方案或通过 Andorid&Swift 调用它几乎没有区别。 这意味着 FFI 不会增加任何性能开销。
Note: speed of request highly depends on the server latency.
You can find both implementations in the GitHub project.
注意:请求的速度很大程度上取决于服务器的延迟。
你可以在GitHub项目中找到这两种实现。
Project
项目
A full example of the project is available on GitHub:
GitHub 上有一个完整的项目示例:
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++ 内存问题的同时,提供了极高的速度。坚实和简单的 API 使它易于使用和学习,在 C++ 和 Rust 之间,我肯定会选择后一个,但它仍然比 Swift 或 Kotlin 更复杂。
最大的挑战是在 Rust 和客户端框架之间建立一个适当的桥梁,如果你能接受它——它可能是一个很好的移动解决方案。
Reference:
参考
My previous investigation: Go + Gomobile for Android and IOS.
我之前的研究: Go + Gomobile for Android and IOS.
Implementation and benchmarking.
实现和基准测试。