Dart 和 Rust:异步编程的故事🔃
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
在上一篇博文《Dart 与 Rust 的天作之合》中,我们展示了如何将这两种语言连接起来,构建更安全、更高效的 Flutter 应用。在本文中,我们将探讨如何将多线程异步 Rust 代码与异步 Dart 代码结合使用,以及如何构建像sōzu
那样,多个运行时环境能够流畅协同工作的应用。
问题🎯
运行多个异步循环并不总是容易的,要正确运行它们并不容易,你知道 Dart VM 有自己的异步运行时,而在 Rust 中,我们可以插入任何流行的运行时,这里有一些供参考:Tokio、async-std,以及最近内置于 async-std 中的smol ,因为它是一个底层运行时。
问题在于 Dart 语言本身是单线程的,这本身不是问题,但我们稍后会看到,在使用 Rust 时,这会造成什么问题。
其实,你可以运行多个Isolate,它们就像一个个小型 Dart 虚拟机实例,可以协同工作并通过消息传递进行通信。这里有一个视频,Flutter 团队的 Andrew 在视频中讲解了 Dart Isolate 和事件循环,如果你对 Dart Isolate 一无所知,我强烈建议你在继续阅读之前观看这个视频(PS:视频不到 6 分钟😅)。
让我们来抓取网络数据吧🌍
为了让这篇文章更有趣,我们将用 Rust 构建一个简单的网页爬虫库,并将其用于 Flutter 应用中。
借鉴上一篇博文中关于如何连接这两种语言的经验,我们将在这里沿用相同的思路。
首先创建 Rust crate:
$ cargo new --lib native/scrap # yes, it is a scrap lol
好的,现在我们需要一种方法来发送异步 Web 请求,所以我们来使用reqwest。
[dependencies]
# we will use `rustls-tls` here since openssl is an issue when cross-compiling for Android/iOS
reqwest = { version = "0.10", default-features = false, features = ["rustls-tls"] }
现在让我们来写点生疏的代码吧 😀
// lib.rs
use std::{error, fmt, io};
/// A useless Error just for the Demo
#[derive(Copy, Clone, Debug)]
pub struct ScrapError;
impl fmt::Display for ScrapError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Error While Scrapping this page.")
}
}
impl error::Error for ScrapError {}
impl From<reqwest::Error> for ScrapError {
fn from(_: reqwest::Error) -> Self {
Self
}
}
impl From<io::Error> for ScrapError {
fn from(_: io::Error) -> Self {
Self
}
}
/// Load a page and return its HTML body as a `String`
pub async fn load_page(url: &str) -> Result<String, ScrapError> {
Ok(reqwest::get(url).await?.text().await?)
}
所以你看,这只是一段简单的代码,错误处理做得一塌糊涂😂
接下来,让我们为scrapcrate 编写 FFI 绑定:
$ cargo new --lib native/scrap-ffi
在我们的Cargo.toml
[lib]
name = "scrap_ffi"
crate-type = ["cdylib", "staticlib"]
[dependencies]
scrap = { path = "../scrap" }
现在,事情变得复杂了,因为我们需要考虑一些事情。
设置运行时🔌
这里我们将选择Tokio作为运行时环境。其实使用哪个运行时环境都无所谓,但鉴于reqwestTokio 与我们的需求兼容良好,所以我们选择使用它。
另外,lazy_static稍后会派上用场。
tokio = { version = "0.2", features = ["rt-threaded"] }
lazy_static = "1.4"
编写一些代码来设置运行时环境。
// lib.rs
use tokio::runtime::{Builder, Runtime};
use lazy_static::lazy_static;
use std::io;
lazy_static! {
static ref RUNTIME: io::Result<Runtime> = Builder::new()
.threaded_scheduler()
.enable_all()
.core_threads(4)
.thread_name("flutterust")
.build();
}
/// Simple Macro to help getting the value of the runtime.
macro_rules! runtime {
() => {
match RUNTIME.as_ref() {
Ok(rt) => rt,
Err(_) => {
return 0;
}
}
};
}
此设置创建了一个包含 4 个线程的运行时,并为每个线程Tokio命名。flutterust
错误处理🧰
处理 FFI 之间的错误很困难,所以我们将使用一个辅助 crate 来简化操作。将ffi_helpers
添加到依赖项中。
ffi_helpers = "0.2"
它提供了有用的函数和宏,可以帮助我们处理错误,last_error_length从而error_message_utf8在 Dart 端暴露出可读的错误消息。
macro_rules! error {
($result:expr) => {
error!($result, 0);
};
($result:expr, $error:expr) => {
match $result {
Ok(value) => value,
Err(e) => {
ffi_helpers::update_last_error(e);
return $error;
}
}
};
}
macro_rules! cstr {
($ptr:expr) => {
cstr!($ptr, 0);
};
($ptr:expr, $error:expr) => {{
null_pointer_check!($ptr);
error!(unsafe { CStr::from_ptr($ptr).to_str() }, $error)
}};
}
#[no_mangle]
pub unsafe extern "C" fn last_error_length() -> i32 {
ffi_helpers::error_handling::last_error_length()
}
#[no_mangle]
pub unsafe extern "C" fn error_message_utf8(buf: *mut raw::c_char, length: i32) -> i32 {
ffi_helpers::error_handling::error_message_utf8(buf, length)
}
公开load_page功能🔑
现在问题来了,我们该如何暴露async函数呢?C/Dart 完全不了解 Rust 的async工作方式。 一种方法是编写一个普通的非函数作为函数的包装器,并调用`block_on`来获取结果。正如函数名所示,我们会阻塞该线程来执行该任务,那么这样做究竟有什么好处呢?
asyncasyncasync fnasync
还有另一种方法,使用callbacks?! 怎么样?!
很好,这会是一个选择,并且可以使用 JavaScriptPromise风格来处理异步任务,通过传递两个回调函数,一个在 上调用success,另一个在 上调用err。
理论上,这样做可行,但是
正如我之前所说,Dart 被设计成单线程的,所以你不能从其他线程调用回调函数,这基本上会破坏 Dart 的语义(你可以在这里阅读更多内容)。
那么解决方案是什么呢?
如果我告诉你我们可以使用Isolate😦在 Dart 和 Rust 之间进行通信呢?
每个新的 Isolate 都有一个SendPort和一个 ReceivePort,它们用于与 通信Isolate,你只需SendPort要向 发送消息Isolate,此外,每个SendPort都有一个称为NativePort 的东西,这是该端口的底层编号。
简单说明一下,
Port这里的“of”与端口或其他类似的东西完全无关Networking,它只是Dart虚拟机内部实现的映射的键值对,你可以把它理解为指向Handle该映射的键。Isolate
如果我们用这些数字async fn从任何我们想发送的线程发送我们的结果,这可行吗?是的。
输入:Allo Isolate 📞
allo-isolate是我们在Sunshine开发的一个 crate,旨在帮助我们在 Dart VM(隔离环境中)运行多线程 Rust 🌀
它只需要一个Port隔离环境,你就可以从你的 Rust 代码中向它发送任何消息。
其底层使用了 Dart APIDart_PostCObject函数,可以将大多数(几乎全部?)Rust 类型转换为 Dart 类型,详情请参阅文档。
Allo(发音为 Hello,不带 H)通常用于某些语言(如阿拉伯语)的电话交流中 :)。
回到我们的例子,我们现在知道如何公开这个load_page函数了,让我们使用它。allo-isolate
allo-isolate = "0.1"
用它
use allo_isolate::Isolate;
...
#[no_mangle]
pub extern "C" fn load_page(port: i64, url: *const raw::c_char) -> i32 {
// get a ref to the runtime
let rt = runtime!();
let url = cstr!(url);
rt.spawn(async move {
// load the page and get the result back
let result = scrap::load_page(url).await;
// make a ref to an isolate using it's port
let isolate = Isolate::new(port);
// and sent it the `Rust's` result
// no need to convert anything :)
isolate.post(result);
});
1
}
太好了,一切准备就绪,生成binding.h并构建好所有东西之后iOS,Android我们还需要编写 Dart 端的 FFI……唉,又来了🤦♂️
如果我告诉你,现在你可以从 C 头文件生成 Dart FFI 绑定,你会相信吗?
dart-bindgen:一种编写 Dart FFI 的新方法
dart-bindgen是一个用于生成 Dart FFI 绑定到 C 头文件的工具,它以CLI和库的形式提供。
我们将用它来帮助我们生成FFI,只需将其添加到您的……build-dependencies
[build-dependencies]
cbindgen = "0.14.2" # Rust -> C header file
dart-bindgen = "0.1" # C header file -> Dart FFI
并且在我们的build.rs
use dart_bindgen::{config::*, Codegen};
fn main() {
...
let config = DynamicLibraryConfig {
ios: DynamicLibraryCreationMode::Executable.into(),
android: DynamicLibraryCreationMode::open("libscrap_ffi.so").into(),
..Default::default()
};
// load the c header file, with config and lib name
let codegen = Codegen::builder()
.with_src_header("binding.h")
.with_lib_name("libscrap")
.with_config(config)
.build()
.unwrap();
// generate the dart code and get the bindings back
let bindings = codegen.generate().unwrap();
// write the bindings to your dart package
// and start using it to write your own high level abstraction.
bindings
.write_to_file("../../packages/scrap_ffi/lib/ffi.dart")
.unwrap();
}
目前一切顺利,让我们在这个ffi.dart文件上编写更高层次的抽象层。
// packages/scrap_ffi/lib/scrap.dart
import 'dart:async';
import 'dart:ffi';
import 'package:ffi/ffi.dart';
// isolate package help us creating isolate and getting the port back easily.
import 'package:isolate/ports.dart';
import 'ffi.dart' as native;
class Scrap {
// this only should be called once at the start up.
static setup() {
// give rust `allo-isolate` package a ref to the `NativeApi.postCObject` function.
native.store_dart_post_cobject(NativeApi.postCObject);
print("Scrap Setup Done");
}
Future<String> loadPage(String url) {
var urlPointer = Utf8.toUtf8(url);
final completer = Completer<String>();
// Create a SendPort that accepts only one message.
final sendPort = singleCompletePort(completer);
final res = native.load_page(
sendPort.nativePort,
urlPointer,
);
if (res != 1) {
_throwError();
}
return completer.future;
}
void _throwError() {
final length = native.last_error_length();
final Pointer<Utf8> message = allocate(count: length);
native.error_message_utf8(message, length);
final error = Utf8.fromUtf8(message);
print(error);
throw error;
}
}
在我们的 Flutter 应用中,我们可以这样使用该包:
...
class _MyHomePageState extends State<MyHomePage> {
...
Scrap scrap;
@override
void initState() {
super.initState();
scrap = Scrap();
Scrap.setup();
}
// somewhere in your app
final html = await scrap.loadPage('https://www.rust-lang.org/');
就这样,启动安卓模拟器或iOS模拟器并运行:
$ cargo make # build all rust packages for iOS and Android
然后,
$ flutter run # 🔥
所有代码都可以在flutterust上找到,它只是一个克隆和破解版 😀。
敬请期待更多 Dart 和 Rust 的 Hacks 内容,你可以在Twitter和GitHub上关注我。
谢谢你💚。
文章来源:https://dev.to/sunshine-chain/rust-and-dart-the-async-story-3adk

