stepbystep
This repository contains a stepbystep
crate, which exposes a single macro - export_async
, which can then be leveraged to use a state machine generated by async Rust in C++.
How it works
The export_async
macro can be used on an async Rust function and it will do the following things, assuming that the function is named do_thing
:
- Expose an unmangled symbol named
do_thing_init(usize)
, which can be used from C++ directly to initialize an instance of the state machine. - Expose an unmangled symbol named
do_thing_poll(usize)
, which can be used from C++ directly to move the state machine to the next state and returnstrue
when the final state is reached.
Intended use case
With the use of this simple macro, complicated state machines can be easily accessed with C++. That, in turn, allows the user to use a C++ runtime for an async function written in Rust. If the async function does not use async I/O and instead is 100% computation bound, an optimal runtime is to simply poll the machine in a tight loop.
It also means that integration with more complicated runtimes - like Seastar (http://seastar.io) - also becomes easy, especially with 100% computation bound async functions:
seastar::future<> run_something_coded_in_rust() {
do_thing_init(0);
put_some_custom_payload(0, "payload");
while (!do_thing_poll(0)) {
co_await seastar::coroutine::maybe_yield();
}
co_return extract_result_via_some_custom_exported_function(0);
}
Example of an exported function
Full example can be found in the stepbystep-example
crate, and the C++ demo can be compiled with compile-demo-debug.sh
script, assuming that cargo build
was successfully executed.
thread_local! {
static PAYLOAD_MAP: std::cell::RefCell<std::collections::HashMap<usize, String>> = std::cell::RefCell::new(std::collections::HashMap::new());
}
#[no_mangle]
pub fn put_payload(idx: usize, ptr: *const c_char) {
let cstr = unsafe { CStr::from_ptr(ptr) };
let s = cstr.to_str().unwrap().to_owned();
PAYLOAD_MAP.with(|payload_map| {
payload_map.borrow_mut().insert(idx, s);
})
}
struct Yield {
ready: bool,
}
impl Future for Yield {
type Output = ();
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.ready {
Poll::Ready(())
} else {
self.ready = true;
Poll::Pending
}
}
}
pub async fn compute_complicated_stuff(i: usize) {
println!("Computation performed: {}*{}={}", i, i, i*i);
Yield{ready: false}.await
}
#[export_async]
pub async fn test_me(instance_idx: usize) {
for i in 1..10 {
println!("Instance {}", instance_idx);
PAYLOAD_MAP.with(|payload_map| {
println!("i = {}; value = {}", i, match payload_map.borrow().get(&i) {
Some(s) => s,
None => "<none>",
});
});
compute_complicated_stuff(i).await;
}
}