Skip to content

Conversation

UnknownSuperficialNight
Copy link

@UnknownSuperficialNight UnknownSuperficialNight commented Jun 14, 2025

Implement a storage_creator field to allow users to create a custom implementation of FileStorage for custom storage of persistence data

Here is a portable self-contained example of using JSON instead of Ron via storage_creator:
eframe_template_persistence.zip

- Implement a `storage_creator` field for the user to create their own
custom implementation of persistent storage
@UnknownSuperficialNight
Copy link
Author

Few discussion points on the implementation:

  • Should persistence_path and storage_creator be merged or kept separate?
  • Can we remove the dependency on ron since users implement their own storage logic?
  • Should we require developers to choose between storage_creator and persistence_path to allow for optional exclusion of file_storage.rs from the binary?

@UnknownSuperficialNight UnknownSuperficialNight marked this pull request as ready for review June 26, 2025 20:08
@UnknownSuperficialNight
Copy link
Author

Few discussion points on the implementation:

* Should `persistence_path` and `storage_creator` be merged or kept separate?

* Can we remove the dependency on `ron` since users implement their own storage logic?

* Should we require developers to choose between `storage_creator` and `persistence_path` to allow for optional exclusion of [file_storage.rs](https://github.com/emilk/egui/blob/main/crates/eframe/src/native/file_storage.rs) from the binary?

Marked for review, looking for feedback on the above

Copy link
Collaborator

@lucasmerlin lucasmerlin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Makes sense to me.

Should persistence_path and storage_creator be merged or kept separate?

I think it makes sense to keep it separate for convenience.

Can we remove the dependency on ron since users implement their own storage logic?
Should we require developers to choose between storage_creator and persistence_path to allow for optional exclusion of file_storage.rs from the binary?

I think we could add a persistence-ron and/or persistence-file feature flag that is enabled by default.

@lucasmerlin lucasmerlin added the eframe Relates to epi and eframe label Jun 30, 2025
Copy link

Preview available at https://egui-pr-preview.github.io/pr/7143-custom-persistence
Note that it might take a couple seconds for the update to show up after the preview_build workflow has completed.

@UnknownSuperficialNight UnknownSuperficialNight marked this pull request as draft June 30, 2025 17:46
@UnknownSuperficialNight
Copy link
Author

@lucasmerlin Small road block I need feedback on:

eframe uses get_value and set_value to Deserialize and Serialize values
Source

/// Get and deserialize the [RON](https://github.com/ron-rs/ron) stored at the given key.
#[cfg(feature = "ron")]
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
    profiling::function_scope!(key);
    storage
        .get_string(key)
        .and_then(|value| match ron::from_str(&value) {
            Ok(value) => Some(value),
            Err(err) => {
                // This happens on when we break the format, e.g. when updating egui.
                log::debug!("Failed to decode RON: {err}");
                None
            }
        })
}

/// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key.
#[cfg(feature = "ron")]
pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, value: &T) {
    profiling::function_scope!(key);
    match ron::ser::to_string(value) {
        Ok(string) => storage.set_string(key, string),
        Err(err) => log::error!("eframe failed to encode data using ron: {}", err),
    }
}

Now there are only 2 ways i can think of to solve this for persistence-custom

  1. Import erased_serde to allow trait objects like &dyn Serialize or boxed trait objects like Box<dyn Serialize> then save this in EpiIntegration allowing users to pass in their own serialize and deserialize to eframe (This does make things more complex both for eframe and users)
  2. Add equivalent get_value & set_value functions for every common serialization format (json, yaml etc...)

The 2nd one is more limiting but more efficient and makes the process of persistence-custom persistence easier also means that eframe has control over these functions so future changes to the persistence are less lightly break people's custom serialization implementations.

If we do the 2nd option we can just add a persistence-custom-json to enable json for example.

@lucasmerlin
Copy link
Collaborator

Could we make set_value and get_value part of the storage trait? Then the user can implement this hovever they like

@UnknownSuperficialNight
Copy link
Author

Could we make set_value and get_value part of the storage trait? Then the user can implement this however they like

Good idea is something like this what you mean?

pub trait Storage {
    /// Get the value for the given key.
    fn get_string(&self, key: &str) -> Option<String>;

    /// Set the value for the given key.
    fn set_string(&mut self, key: &str, value: String);

    /// Get and deserialize the value stored at the given key.
    fn get_value<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T>;
    
    /// Serialize the given value and store it with the given key.
    fn set_value<T: serde::Serialize>(&mut self, key: &str, value: &T);

    /// write-to-disk or similar
    fn flush(&mut self);
}

If so then unfortunately this is not possible due the use of generics unless we remove generics

error[E0038]: the trait `epi::Storage` is not dyn compatible
   --> /mysources/Rust/egui-custom-persistence/crates/eframe/src/native/glow_integration.rs:150:69
    |
150 |         let window_settings = epi_integration::load_window_settings(storage);
    |                                                                     ^^^^^^^ `epi::Storage` is not dyn compatible
    |
note: for a trait to be dyn compatible it needs to allow building a vtable
      for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
   --> /mysources/Rust/egui-custom-persistence/crates/eframe/src/epi.rs:895:8
    |
887 | pub trait Storage {
    |           ------- this trait is not dyn compatible...
...
895 |     fn get_value<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T>;
    |        ^^^^^^^^^ ...because method `get_value` has generic type parameters
...
898 |     fn set_value<T: serde::Serialize>(&mut self, key: &str, value: &T);
    |        ^^^^^^^^^ ...because method `set_value` has generic type parameters
    = help: consider moving `get_value` to another trait
    = help: consider moving `set_value` to another trait

I've also tried using a 2nd trait but then storage itself does not have them thus in epi_integration functions like this cannot use the functions

impl EpiIntegration {
    ...
    pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) {
        #[cfg(any(feature = "persistence", feature = "persistence-custom"))]
        if let Some(storage) = self.frame.storage_mut() {
    ..... rest of the code
}

@Evrey
Copy link

Evrey commented Jul 4, 2025

Must it be str/String, btw.? I’d vastly prefer Cow<'_, [u8]> or &[u8]/Box<[u8]>, which is half the motivation for not using the default RON storage.

E: Of course, the design ship of that trait has sailed, so this is indeed a hypothetical question for a future breaking version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
eframe Relates to epi and eframe
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Support for Custom Storage Backends in eframe via a New NativeOptions Field
3 participants