Joining a Vec<Rc<RefCell<Pin<Box<dyn Future<Output=Value>>>>>>

I'm a rust beginner, and I'm trying to learn by building a simple stack-based VM in rust and adding support for async functions. My idea was to use rust's async functions and just push the future to the execution stack, and when awaiting, just pop the future from the stack, await it, and push the result back to the stack. I managed to do so, but when implementing join_all function, the code seems more complicated than it needs to be.

Basically, my problem is converting Vec<Rc<RefCell<Pin<Box<dyn Future<Output=Value>>>>>> to Vec<Pin<&mut dyn Future<Output=Value>>>

Here is the short version of the code:

use std::cell::{RefCell, RefMut};
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::time::Duration;
use async_std::task;
use futures::executor::block_on;
use futures::future::join_all;

/**
 * Example async function
 */
pub async fn test_async() -> Value {
    task::sleep(Duration::from_millis(1000)).await;
    println!("Hello from async");
    Value::Void
}

#[derive(Debug)]
enum Instruction {
    // Other instructions
    CallNative(String),
    Await,
    Join(usize),
}

#[derive(Clone)]
#[allow(dead_code)]
pub enum Value {
    I32(i32),
    I64(i64),
    F32(f32),
    F64(f64),
    Bool(bool),
    Void,
    // Other types
    Future(Rc<RefCell<Pin<Box<dyn Future<Output=Value>>>>>),
}


async fn async_main() {
    let program = vec![
        Instruction::CallNative(stringify!(test_async).to_string()),
        Instruction::Await,
        Instruction::CallNative(stringify!(test_async).to_string()),
        Instruction::Await,
        Instruction::CallNative(stringify!(test_async).to_string()),
        Instruction::CallNative(stringify!(test_async).to_string()),
        Instruction::CallNative(stringify!(test_async).to_string()),
        Instruction::CallNative(stringify!(test_async).to_string()),
        Instruction::Join(4),
    ];

    let mut stack: Vec<Value> = vec![];

    for instruction in program {
        match instruction {
            Instruction::CallNative(name) => {
                let future = match name.as_str() {
                    stringify!(test_async) => test_async(),
                    _ => panic!("Unknown native function"),
                };
                let future: Rc<RefCell<Pin<Box<dyn Future<Output=Value>>>>> = Rc::new(RefCell::new(Box::pin(future)));
                stack.push(Value::Future(future));
            }
            Instruction::Await => {
                let future = stack.pop().unwrap();
                let future = match future {
                    Value::Future(future) => future,
                    _ => panic!("Expected future"),
                };
                let mut future = future.borrow_mut();
                let future = future.as_mut();
                let value = future.await;
                stack.push(value);
            }
            Instruction::Join(count) => {
                // This part seems more complicated than it should be
                let mut futures: Vec<Rc<RefCell<Pin<Box<dyn Future<Output=Value>>>>>> = vec![];
                for _ in 0..count {
                    let future = stack.pop().unwrap();
                    match future {
                        Value::Future(future) => futures.push(future),
                        _ => panic!("Expected future"),
                    }
                }

                let futures2: Rc<RefCell<Vec<RefMut<Pin<Box<dyn Future<Output=Value>>>>>>> = Rc::new(RefCell::new(vec![]));
                for i in 0..count {
                    futures2.borrow_mut().push(futures[i].borrow_mut());
                }

                let mut futures3: Vec<Pin<&mut dyn Future<Output=Value>>> = vec![];
                let mut futures2 = futures2.borrow_mut();
                for i in futures2.iter_mut() {
                    futures3.push(i.as_mut());
                }

                join_all(futures3).await;
            }
        }
    }
}

fn main() {
    block_on(async_main());
}

It seems like you managed to find a way to do it. I would recommend dropping the Rc/RefCell.

As a more general recommendation, I would recommend dropping support for a general Future value entirely. Instead, I would consider having the call-native operation spawn the operation with tokio::spawn (or whatever the equivalent for async-std is). Then, you should be able to just store a JoinHandle<Value> instead. I don't think I would make the main function async — you can call block_on when you need the answer from the JoinHandle.

1 Like

Hi Alice, thanks for the answer. I'll try doing it the tokio spawn way.

As for the Rc<RefCell>, I needed to do that, since my stack value needs to be cloneable, and I couldn't think of a better way. (Everything other than the primitive types in my VM is Rc<RefCell>, e.g. Strings, Lists, Maps, etc.). Do you have any recommendations on how do to that better?

Even if you wrap it in Rc/RefCell, its not really cloneable. You can only await a future once — if you try to do it twice, the second attempt will result in a panic.

1 Like