In the last blog post: Dart Meets Rust: a match made in heaven we show how to connect both languages and build more secure, performant Flutter Applications.
In this blog post, we will talk about how to use Multithreaded async Rust code with async Dart and how we could build applications that have more than one runtime working together in a smooth way like how the sΕzu works.
The Problem π―
Running more than async loops is not always easy to get it right, you know that Dart VM has it is own async runtime and in Rust, we could plug-in any one of the popular runtimes, here is a few for reference Tokio, async-std, and recently smol which is now built-in the async-std as it is low-level runtime.
The issue is that Dart as a language is designed to be single-threaded, that's not a problem in itself, but we will see later how would that be an issue when using Rust.
well, you could run more than Isolate which is like a small Dart VM instance they can work together and communicate by message passing, here is a video where Andrew from Flutter team talk about Dart Isolates and the Event Loop, I highly recommend watching it before continuing if you have no idea about Dart isolates (PS: it is less than 6 min π
)
Let's Scrape the web π
For making this post more exciting let's build a simple web scraping library in Rust and use it in Flutter Application.
using our experience from the last blog post on how to connect both languages, we will use the same idea here too.
Create the Rust crate first:
$ cargo new --lib native/scrap # yes, it is a scrap lol
cool, now we need a way to send async web requests, so let's use 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"] }
Now let's write some rusty code π
// 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?)
}
so as you see it is some simple code with sh**ty error handling π
Next, Let's Write our FFI binding to the scrap
crate:
$ cargo new --lib native/scrap-ffi
in our Cargo.toml
[lib]
name = "scrap_ffi"
crate-type = ["cdylib", "staticlib"]
[dependencies]
scrap = { path = "../scrap" }
Now, it gets dirty because we need to consider a few things here
Setting up the Runtime π
We will choose Tokio here as our runtime, it does not matter which runtime we use, but in our case, since reqwest
works well with Tokio we going to use it.
also lazy_static it will be handy in a second.
tokio = { version = "0.2", features = ["rt-threaded"] }
lazy_static = "1.4"
Write some code to set up the runtime
// 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;
}
}
};
}
This setup a Tokio
Runtime with 4 threads and naming them flutterust
for each thread.
Error Handling π§°
Handling Errors between FFI is hard, so we will use a helper crate that will make our life easier.
add ffi_helpers to the dependencies
ffi_helpers = "0.2"
it provides useful functions and macros that could help us to handle errors, exposing last_error_length
and error_message_utf8
so in Dart's side we could get readable error messages.
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)
}
Exposing load_page
function π
Now comes the issue, how we would expose async
function? well C/Dart has no idea how Rust's async
works.
One way could be writing a normal non-async
function as a wrapper around the async
function and call block_on to get the result out from the async fn
, as you see the function name says it all, we will block that thread only to run that async
task, so what the benefit in the first place?
There is another way, what about using callbacks
?!
Good, that would be an option and use javascript Promise
style to handle the async tasks by passing two callbacks one called on the success
and the other one called on err
.
Theoretically, that would work, but
As I said before, Dart is designed to be single-threaded so you can't just call the callback from other threads it basically will break Dart semantics (you could read more here).
So what would be the solution then?
What if I told you that we could communicate between Dart and Rust using an Isolate
π¦.
Every new Isolate get a SendPort and ReceivePort these are used to communicate with the Isolate
and you only need SendPort
to send messages to the Isolate
, also every SendPort
have what is called a NativePort this the underlying number of that port.
Just a small note, the term of
Port
here is not related at all to aNetworking
Port or something like that, it is just numbers used as Keys for a Map implemented internally in Dart VM, think of them as aHandle
to thatIsolate
What if we used these numbers to send our async fn
result on them from any thread we want, is that possible? YES
Enter: Allo Isolate π
allo-isolate is a crate we built at Sunshine to help us Running Multithreaded Rust along with Dart VM (in isolate) π
It only needs a Port
of that isolate and you could send any message on it from your Rust code.
Under the hood, it uses the Dart API Dart_PostCObject
function and it can convert most (almost all?) of Rust types to Dart types, see the docs for more information.
Allo (pronounced Hello, without the H) usually used in communication over the phone in some languages like Arabic :).
Back to our example, we now know how to expose the load_page
function lets use allo-isolate
allo-isolate = "0.1"
use it
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
}
Great, we are ready to go, after generating our binding.h
and building everything for iOS
and Android
we need also to write the Dart side FFI... ugh not again π€¦ββοΈ
What if I told you that you could now generate your Dart FFI binding from a C Header File?
dart-bindgen: a new way to write Dart FFI
dart-bindgen is a tool for generating Dart FFI bindings to C Header file, it comes as a CLI and a Library.
and we going to use it to help us generate the FFI, just add it to your build-dependencies
[build-dependencies]
cbindgen = "0.14.2" # Rust -> C header file
dart-bindgen = "0.1" # C header file -> Dart FFI
and in our 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();
}
So far so good, let's write our higher-level abstraction over the ffi.dart
file.
// 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;
}
}
in our flutter application, we could use that package as so:
...
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/');
That's it, Fire up Android Emulator or iOS Simulator and Run:
$ cargo make # build all rust packages for iOS and Android
Then,
$ flutter run # π₯
All the Code is available in flutterust which is only clone and Hack π.
Stay tuned for other Hacks in Dart and Rust, you could follow me on twitter and github
Thank you π.
Top comments (4)
Awesome work, thanks a lot! :)
You have inspired me, thanks! ;)
always got error on cargo make .
i have copy the Makefile.toml but still got error.
just resolve. now i got "lost device connection" on IOS only.
on android alls good.