Any trick to restart wasm program while reversing states

I was trying to find a way to simulate some behaviors from JavaScript, where I can attach a single giant state to globalThis, and reload the program that was modified, reusing previous states, and still runs well. It's very easy for scripting languages.

Is there any trick to do that in Rust? I tried to hold arbitrary data with dyn Any. it worked for data storing part. But how to preserve memory, even if I killed the process and restart another one? I tried serde_json first and restricted all my data to Json Value, it worked although was not the data I wanted. I still need dyn Any or dyn DynEq for richer data types and less costs in conversion.

Also, my use case is Rust compiled to WASM, preserving single piece of giant states during hot reloading.

Another way I was dreaming was, can I compile my program into 2 wasm components, one holding global state and being stable most of the time, another one being modified with components that accessing/sending states to the global state and meanwhile for rendering. Is it possible with current toolchains?(I'm not familiar with WASM component model yet, maybe it's still limited.)

Any suggestion is appreciated. thx.

You might want to look at how projects like this handle hot reloading.

I'm not familiar with this particular project (I stumbled upon it in the #hot-reloading tag on crates.io) but I've enjoyed reading this blog so far.

1 Like

Edit: my answer is just for fun, I forgot you want WASM :no_mouth:

Sound like you need something like phoenix:

use serde::{Deserialize, Serialize};
use std::io;

const OUT_DIR: &str = concat!(env!("OUT_DIR"), "/state.bin");

#[derive(Debug, Default, Deserialize, Serialize, PartialEq)]
struct State {
    flag1: bool,
    flag2: bool,
}

trait Phoenix {
    const PATH: &'static str = OUT_DIR;

    fn die(&self);
    fn reborn() -> Self;
}

impl Phoenix for State {
    fn die(&self) {
        let dump = bincode::serialize(self).unwrap();
        std::fs::write(Self::PATH, dump).unwrap();
        println!("die");
    }

    fn reborn() -> Self {
        println!("reborn");
        std::fs::read(Self::PATH)
            .and_then(|dump| {
                bincode::deserialize(&dump)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
            })
            .unwrap_or_default()
    }
}

impl Drop for State {
    fn drop(&mut self) {
        self.die();
    }
}

fn main() {
    let mut state = State::reborn();
    dbg!(&state);
    state.flag1 = true;
}

The output is:

$ cargo run
   Compiling serde v1.0.203
   Compiling some_test v0.1.0 (/Users/louis/rust/some_test)
   Compiling bincode v1.3.3
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.91s
     Running `/Users/louis/rust/.cargo/target/debug/some_test`
reborn
[src/main.rs:45:5] &state = State {
    flag1: false,
    flag2: false,
}
die

$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `/Users/louis/rust/.cargo/target/debug/some_test`
reborn
[src/main.rs:45:5] &state = State {
    flag1: true,
    flag2: false,
}
die
1 Like

yep that's original idea. pity that I used dyn Any and would run into trait object safety problem if I add Serialize/Deserialize traits to my new structs.

Is below what you want?

  1. You have a State, which has dyn Any as its field.
  2. You want to persist it while killing process.
  3. Then bring it back to memory in another new process.
  4. And the solution should work in WSAM context.

I'm not a native speaker, if this is what you want, I'll start finding the answer with you! :grin:

1 Like

Yes. It's okay I reload whole webpage and lose some browsers states. I only want my app states. For the State, the details are on GitHub:

I'm searching on Google quite frequent as well :smile:

If there was tricks for something like trait DynEq: Debug + Serialize + Deseriaize<S>, that would be enough for solving my problems.

Maybe this can help :sunglasses:

I've ran out of my brain, and cannot judge if it meets your demand now.

Although it seems not suit for WASM, ..., anyway it may help.

use std::any::Any;
use std::io;

const OUT_PATH: &str = concat!(env!("OUT_DIR"), "/state.bin");

#[derive(Debug)]
struct State {
    inner: Box<dyn Phoenix>,
}

impl Default for State {
    fn default() -> Self {
        Self::new::<i32>()
    }
}

impl State {
    fn new<T>() -> Self
    where
        for<'de> T: Any + Phoenix + serde::Deserialize<'de> + Default + 'de,
    {
        let inner = std::fs::read(OUT_PATH)
            .and_then(|dump| {
                let bincode_de = &mut bincode::Deserializer::from_slice(&dump, bincode::options());
                let mut deserializer =
                    Box::new(<dyn erased_serde::Deserializer>::erase(bincode_de));
                erased_serde::deserialize(&mut deserializer)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
            })
            .unwrap_or(Box::new(T::default()));
        Self { inner }
    }
}

trait Phoenix: Any + erased_serde::Serialize + std::fmt::Debug {
    fn die(&self);
    fn as_any(&self) -> &dyn Any;
    fn as_any_mut(&mut self) -> &mut dyn Any;
}

impl<T> Phoenix for T
where
    T: Any + erased_serde::Serialize + std::fmt::Debug,
{
    fn die(&self) {
        let file = std::fs::File::create(OUT_PATH).unwrap();
        let mut serializer = bincode::Serializer::new(file, bincode::options());
        erased_serde::serialize(self, &mut serializer).unwrap();
    }

    fn as_any(&self) -> &dyn Any {
        self
    }

    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }
}

impl Drop for State {
    fn drop(&mut self) {
        self.inner.die();
    }
}

fn main() {
    let mut state = State::new::<i32>();
    dbg!(&state);
    *state.inner.as_any_mut().downcast_mut().unwrap() = 42;
}

The output:

cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `/Users/louis/rust/.cargo/target/debug/some_test`
[src/main.rs:70:5] &state = State {
    inner: 0,
}
cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `/Users/louis/rust/.cargo/target/debug/some_test`
[src/main.rs:70:5] &state = State {
    inner: 42,
}

erase_serde provides an obj safe Serialize, so that we can make Foo: Any + Serialize. But for Deserialize, I think we must know its type, so need caller to provide it.

1 Like

Thanks anyway. I was suggested about typetag and also told that it's not working for WASM. So does erased_serde.

I tried another way today. It worked with some extra cost. The trick was saving 2 pieces of data, and use either if them in its own case. and I added another helper for converting data in a more verbose way.

Not a good solution, but an acceptable workaround for now.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.