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.)
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.
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.
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.