/stepbystep

Extremely experimental bindings intended for async Rust<->Seastar integration

Primary LanguageRust

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:

  1. Expose an unmangled symbol named do_thing_init(usize), which can be used from C++ directly to initialize an instance of the state machine.
  2. 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 returns true 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;
    }
}