Crate savefile

source ·
Expand description

This is the documentation for savefile

§Introduction

Savefile is a rust library to conveniently, quickly and correctly serialize and deserialize arbitrary rust structs and enums into an efficient and compact binary version controlled format.

The design use case is any application that needs to save large amounts of data to disk, and support loading files from previous versions of that application (but not from later versions!).

§Example

Here is a small example where data about a player in a hypothetical computer game is saved to disk using Savefile.

extern crate savefile;
use savefile::prelude::*;

#[macro_use]
extern crate savefile_derive;


#[derive(Savefile)]
struct Player {
    name : String,
    strength : u32,
    inventory : Vec<String>,
}

fn save_player(player:&Player) {
    save_file("save.bin", 0, player).unwrap();
}

fn load_player() -> Player {
    load_file("save.bin", 0).unwrap()
}

fn main() {
    let player = Player { name: "Steve".to_string(), strength: 42,
        inventory: vec!(
            "wallet".to_string(),
            "car keys".to_string(),
            "glasses".to_string())};

    save_player(&player);

    let reloaded_player = load_player();

    assert_eq!(reloaded_player.name,"Steve".to_string());
}

§Limitations of Savefile

Savefile does make a few tradeoffs:

1: It only supports the “savefile-format”. It does not support any sort of pluggable architecture with different formats. This format is generally pretty ‘raw’, data is mostly formatted the same way as it is in RAM. There is support for bzip2, but this is just a simple post-processing step.

2: It does not support serializing ‘graphs’. I.e, it does not have a concept of object identity, and cannot handle situations where the same object is reachable through many paths. If two objects both have a reference to a common object, it will be serialized twice and deserialized twice.

3: Since it doesn’t support ‘graphs’, it doesn’t do well with recursive data structures. When schema serialization is activated (which is the default), it also doesn’t support ‘potentially recursive’ data structures. I.e, serializing a tree-object where the same node type can occur on different levels is not possible, even if the actual links in the tree do not cause any cycles. This is because the serialized schema is very detailed, and tries to describe exactly what types may be contained in each node. In a tree, it will determine that children of the node may be another node, which may itself have children of the same type, which may have children of the same type, and so on.

§Handling old versions

Let’s expand the above example, by creating a 2nd version of the Player struct. Let’s say you decide that your game mechanics don’t really need to track the strength of the player, but you do wish to have a set of skills per player as well as the inventory.

Mark the struct like so:

extern crate savefile;
use savefile::prelude::*;
use std::path::Path;
#[macro_use]
extern crate savefile_derive;

const GLOBAL_VERSION:u32 = 1;
#[derive(Savefile)]
struct Player {
    name : String,
    #[savefile_versions="0..0"] //Only version 0 had this field
    strength : Removed<u32>,
    inventory : Vec<String>,
    #[savefile_versions="1.."] //Only versions 1 and later have this field
    skills : Vec<String>,
}

fn save_player(file:&'static str, player:&Player) {
    // Save current version of file.
    save_file(file, GLOBAL_VERSION, player).unwrap();
}

fn load_player(file:&'static str) -> Player {
    // The GLOBAL_VERSION means we have that version of our data structures,
    // but we can still load any older version.
    load_file(file, GLOBAL_VERSION).unwrap()
}

fn main() {
    if Path::new("save.bin").exists() == false { /* error handling */ return;}

    let mut player = load_player("save.bin"); //Load from previous save
    assert_eq!("Steve",&player.name); //The name from the previous version saved will remain
    assert_eq!(0,player.skills.len()); //Skills didn't exist when this was saved
    player.skills.push("Whistling".to_string());
    save_player("newsave.bin", &player); //The version saved here will have the vec of skills
}

§Behind the scenes

For Savefile to be able to load and save a type T, that type must implement traits crate::WithSchema, crate::Packed, crate::Serialize and crate::Deserialize . The custom derive macro Savefile derives all of these.

You can also implement these traits manually. Manual implementation can be good for:

1: Complex types for which the Savefile custom derive function does not work. For example, trait objects or objects containing pointers.

2: Objects for which not all fields should be serialized, or which need complex initialization (like running arbitrary code during deserialization).

Note that the four trait implementations for a particular type must be in sync. That is, the Serialize and Deserialize traits must follow the schema defined by the WithSchema trait for the type, and if the Packed trait promises a packed layout, then the format produced by Serialize and Deserialze must exactly match the in-memory format.

§WithSchema

The crate::WithSchema trait represents a type which knows which data layout it will have when saved. Savefile includes the schema in the serialized data by default, but this can be disabled by using the save_noschema function. When reading a file with unknown schema, it is up to the user to guarantee that the file is actually of the correct format.

§Serialize

The crate::Serialize trait represents a type which knows how to write instances of itself to a Serializer.

§Deserialize

The crate::Deserialize trait represents a type which knows how to read instances of itself from a Deserializer.

§Packed

The crate::Packed trait has an optional method that can be used to promise that the in-memory format is identical to the savefile disk representation. If this is true, instances of the type can be serialized by simply writing all the bytes in one go, rather than having to visit individual fields. This can speed up saves significantly.

§Rules for managing versions

The basic rule is that the Deserialize trait implementation must be able to deserialize data from any previous version.

The WithSchema trait implementation must be able to return the schema for any previous verison.

The Serialize trait implementation only needs to support the latest version, for savefile itself to work. However, for SavefileAbi to work, Serialize should support writing old versions. The savefile-derive macro does support serializing old versions, with some limitations.

§Versions and derive

The derive macro used by Savefile supports multiple versions of structs. To make this work, you have to add attributes whenever fields are removed, added or have their types changed.

When adding or removing fields, use the #[savefile_versions] attribute.

The syntax is one of the following:

#[savefile_versions = "N.."]  //A field added in version N
#[savefile_versions = "..N"]  //A field removed in version N+1. That is, it existed up to and including version N.
#[savefile_versions = "N..M"] //A field that was added in version N and removed in M+1. That is, a field which existed in versions N .. up to and including M.

Removed fields must keep their deserialization type. This is easiest accomplished by substituting their previous type using the Removed<T> type. Removed<T> uses zero space in RAM, but deserializes equivalently to T (with the result of the deserialization thrown away).

Savefile tries to validate that the Removed<T> type is used correctly. This validation is based on string matching, so it may trigger false positives for other types named Removed. Please avoid using a type with such a name. If this becomes a problem, please file an issue on github.

Using the #[savefile_versions] tag is critically important. If this is messed up, data corruption is likely.

When a field is added, its type must implement the Default trait (unless the default_val or default_fn attributes are used).

More about the savefile_default_val, default_fn and savefile_versions_as attributes below.

§The savefile_versions attribute

Rules for using the #[savefile_versions] attribute:

  • You must keep track of what the current version of your data is. Let’s call this version N.
  • You may only save data using version N (supply this number when calling save)
  • When data is loaded, you must supply version N as the memory-version number to load. Load will adapt the deserialization operation to the version of the serialized data.
  • The version number N is “global” (called GLOBAL_VERSION in the previous source example). All components of the saved data must have the same version.
  • Whenever changes to the data are to be made, the global version number N must be increased.
  • You may add a new field to your structs, iff you also give it a #[savefile_versions = “N..”] attribute. N must be the new version of your data.
  • You may remove a field from your structs.
    • If previously it had no #[savefile_versions] attribute, you must add a #[savefile_versions = “..N-1”] attribute.
    • If it already had an attribute #[savefile_versions = “M..”], you must close its version interval using the current version of your data: #[savefile_versions = “M..N-1”].
    • Whenever a field is removed, its type must simply be changed to Removed<T> where T is its previous type.
    • You may never completely remove items from your structs. Doing so removes backward-compatibility with that version. This will be detected at load.
    • For example, if you remove a field in version 3, you should add a #[savefile_versions=“..2”] attribute.
  • You may not change the type of a field in your structs, except when using the savefile_versions_as-macro.
  • You may add enum variants in future versions, but you may not change the size of the discriminant.

§The savefile_default_val attribute

The default_val attribute is used to provide a custom default value for primitive types, when fields are added.

Example:


#[derive(Savefile)]
struct SomeType {
    old_field: u32,
    #[savefile_default_val="42"]
    #[savefile_versions="1.."]
    new_field: u32
}

In the above example, the field new_field will have the value 42 when deserializing from version 0 of the protocol. If the default_val attribute is not used, new_field will have u32::default() instead, which is 0.

The default_val attribute only works for simple types.

§The savefile_default_fn attribute

The default_fn attribute allows constructing more complex values as defaults.


fn make_hello_pair() -> (String,String) {
    ("Hello".to_string(),"World".to_string())
}
#[derive(Savefile)]
struct SomeType {
    old_field: u32,
    #[savefile_default_fn="make_hello_pair"]
    #[savefile_versions="1.."]
    new_field: (String,String)
}

§The savefile_ignore attribute

The savefile_ignore attribute can be used to exclude certain fields from serialization. They still need to be constructed during deserialization (of course), so you need to use one of the default-attributes to make sure the field can be constructed. If none of the default-attributes (described above) are used, savefile will attempt to use the Default trait.

Here is an example, where a cached value is not to be deserialized. In this example, the value will be 0.0 after deserialization, regardless of the value when serializing.


#[derive(Savefile)]
struct IgnoreExample {
    a: f64,
    b: f64,
    #[savefile_ignore]
    cached_product: f64
}

savefile_ignore does not stop the generator from generating an implementation for Introspect for the given field. To stop this as well, also supply the attribute savefile_introspect_ignore .

§The savefile_versions_as attribute

The savefile_versions_as attribute can be used to support changing the type of a field.

Let’s say the first version of our protocol uses the following struct:


#[derive(Savefile)]
struct Employee {
    name : String,
    phone_number : u64
}

After a while, we realize that u64 is a bad choice for datatype for a phone number, since it can’t represent a number with leading 0, and also can’t represent special characters which sometimes appear in phone numbers, like ‘+’ or ‘-’ etc.

So, we change the type of phone_number to String:


fn convert(phone_number:u64) -> String {
    phone_number.to_string()
}
#[derive(Savefile)]
struct Employee {
    name : String,
    #[savefile_versions_as="0..0:convert:u64"]
    #[savefile_versions="1.."]
    phone_number : String
}

This will cause version 0 of the protocol to be deserialized expecting a u64 for the phone number, which will then be converted using the provided function convert into a String.

Note, that conversions which are supported by the From trait are done automatically, and the function need not be specified in these cases.

Let’s say we have the following struct:


#[derive(Savefile)]
struct Racecar {
    max_speed_kmh : u8,
}

We realize that we need to increase the range of the max_speed_kmh variable, and change it like this:


#[derive(Savefile)]
struct Racecar {
    #[savefile_versions_as="0..0:u8"]
    #[savefile_versions="1.."]
    max_speed_kmh : u16,
}

Note that in this case we don’t need to tell Savefile how the deserialized u8 is to be converted to an u16.

§Speeding things up

Note: This entire chapter can safely be ignored. Savefile will, in most circumstances, perform very well without any special work by the programmer.

Continuing the example from previous chapters, let’s say we want to add a list of all positions that our player have visited, so that we can provide an instant-replay function to our game. The list can become really long, so we want to make sure that the overhead when serializing this is as low as possible.

extern crate savefile;
use savefile::prelude::*;
use std::path::Path;

#[macro_use]
extern crate savefile_derive;

#[derive(Clone, Copy, Savefile)]
#[repr(C)] // Memory layout will become equal to savefile disk format - optimization possible!
struct Position {
    x : u32,
    y : u32,
}

const GLOBAL_VERSION:u32 = 2;
#[derive(Savefile)]
struct Player {
    name : String,
    #[savefile_versions="0..0"] //Only version 0 had this field
    strength : Removed<u32>,
    inventory : Vec<String>,
    #[savefile_versions="1.."] //Only versions 1 and later have this field
    skills : Vec<String>,
    #[savefile_versions="2.."] //Only versions 2 and later have this field
    history : Vec<Position>
}

fn save_player(file:&'static str, player:&Player) {
    save_file(file, GLOBAL_VERSION, player).unwrap();
}

fn load_player(file:&'static str) -> Player {
    load_file(file, GLOBAL_VERSION).unwrap()
}

fn main() {

    if Path::new("newsave.bin").exists() == false { /* error handling */ return;}

    let mut player = load_player("newsave.bin"); //Load from previous save
    player.history.push(Position{x:1,y:1});
    player.history.push(Position{x:2,y:1});
    player.history.push(Position{x:2,y:2});
    save_player("newersave.bin", &player);
}

Savefile can speed up serialization of arrays/vectors of certain types, when it can detect that the type consists entirely of packed plain binary data.

The above will be very fast, even if ‘history’ contains millions of position-instances.

Savefile has a trait crate::Packed that must be implemented for each T. The savefile-derive macro implements this automatically.

This trait has an unsafe function crate::Packed::repr_c_optimization_safe which answers the question:

  • Is this type such that it can safely be copied byte-per-byte? Answering yes for a specific type T, causes savefile to optimize serialization of Vec<T> into being a very fast, raw memory copy. The exact criteria is that the in-memory representation of the type must be identical to what the Serialize trait does for the type.

Most of the time, the user doesn’t need to implement Packed, as it can be derived automatically by the savefile derive macro.

However, implementing it manually can be done, with care. You, as implementor of the Packed trait take full responsibility that all the following rules are upheld:

  • The type T is Copy
  • The in-memory representation of T is identical to the savefile disk format.
  • The host platform is little endian. The savefile disk format uses little endian.
  • The type is represented in memory in an ordered, packed representation. Savefile is not clever enough to inspect the actual memory layout and adapt to this, so the memory representation has to be all the types of the struct fields in a consecutive sequence without any gaps. Note that the #[repr(C)] attribute is not enough to do this - it will include padding if needed for alignment reasons. You should not use #[repr(packed)], since that may lead to unaligned struct fields. Instead, you should use #[repr(C)] combined with manual padding, if necessary. If the type is an enum, it must be #[repr(u8)], #[repr(u16)] or #[repr(u32)]. Enums with fields are not presently optimized.

Regarding padding, don’t do:

#[repr(C)]
struct Bad {
    f1 : u8,
    f2 : u32,
}

Since the compiler is likely to insert 3 bytes of padding after f1, to ensure that f2 is aligned to 4 bytes.

Instead, do this:

#[repr(C)]
struct Good {
    f1 : u8,
    pad1 :u8,
    pad2 :u8,
    pad3 :u8,
    f2 : u32,
}

And simpy don’t use the pad1, pad2 and pad3 fields. Note, at time of writing, Savefile requires that the struct be free of all padding. Even padding at the end is not allowed. This means that the following does not work:

#[repr(C)]
struct Bad2 {
    f1 : u32,
    f2 : u8,
}

This restriction may be lifted at a later time.

When it comes to enums, there are requirements to enable the optimization:

This enum is not optimizable, since it doesn’t have a defined discrminant size:

enum BadEnum1 {
   Variant1,
   Variant2,
}

This will be optimized:

#[repr(u8)]
enum GoodEnum1 {
   Variant1,
   Variant2,
}

This also:

#[repr(u8)]
enum GoodEnum2 {
   Variant1(u8),
   Variant2(u8),
}

However, the following will not be optimized, since there will be padding after Variant1. To have the optimization enabled, all variants must be the same size, and without any padding.

#[repr(u8)]
enum BadEnum2 {
   Variant1,
   Variant2(u8),
}

This can be fixed with manual padding:

#[repr(u8)]
enum BadEnum2Fixed {
   Variant1{padding:u8},
   Variant2(u8),
}

This will be optimized:

#[repr(u8)]
enum GoodEnum3 {
   Variant1{x:u8,y:u16,z:u16,w:u16},
   Variant2{x:u8,y:u16,z:u32},
}

However, the following will not be:

#[repr(u8,C)]
enum BadEnum3 {
   Variant1{x:u8,y:u16,z:u16,w:u16},
   Variant2{x:u8,y:u16,z:u32},
}

The reason is that the format #[repr(u8,C)] will layout the struct as if the fields of each variant were a C-struct. This means alignment of Variant2 will be 4, and the offset of ‘x’ will be 4. This in turn means there will be padding between the discriminant and the fields, making the optimization impossible.

§The attribute savefile_require_fast

When deriving the savefile-traits automatically, specify the attribute #[savefile_require_fast] to require the optimized behaviour. If the type doesn’t fulfill the required characteristics, a diagnostic will be printed in many situations. Presently, badly aligned data structures are detected at compile time. Other problems are only detected at runtime, and result in lower performance but still correct behaviour. Using ‘savefile_require_fast’ is not unsafe, although it used to be in an old version. Since the speedups it produces are now produced regardless, it is mostly recommended to not use savefile_require_fast, unless compilation failure on bad alignment is desired.

§Custom serialization

For most user types, the savefile-derive crate can be used to automatically derive serializers and deserializers. This is not always possible, however.

By implementing the traits Serialize, Deserialize and WithSchema, it’s possible to create custom serializers for any type.

Let’s create a custom serializer for an object MyPathBuf, as an example (this is just an example, because of the rust ‘orphan rules’, only Savefile can actually implement the Savefile-traits for PathBuf. However, you can implement the Savefile traits for your own data types in your own crates!)

The first thing we need to do is implement WithSchema. This trait requires us to return an instance of Schema. The Schema is used to ‘sanity-check’ stored data, so that an attempt to deserialize a file which was serialized using a different schema will fail predictably.

Schema is an enum, with a few built-in variants. See documentation: crate::Schema .

In our case, we choose to handle a MyPathBuf as a string, so we choose Schema::Primitive, with the argument SchemaPrimitive::schema_string . If your data is a collection of some sort, Schema::Vector may be appropriate.

Note that the implementor of Serialize and Deserialize have total freedom to serialize data to/from the binary stream. It is important that the Schema accurately describes the format produced by Serialize and expected by Deserialize. Deserialization from a file is always sound, even if the Schema is wrong. However, the process may allocate too much memory, and data deserialized may be gibberish.

When the Schema is used by the savefile-abi crate, unsoundness can occur if the Schema is incorrect. However, the responsibility for ensuring correctness falls on the savefile-abi crate. The savefile-library itself produces correct Schema-instances for all types it supports.

extern crate savefile;
pub struct MyPathBuf {
    path: String,
}
use savefile::prelude::*;
impl WithSchema for MyPathBuf {
    fn schema(_version: u32, context: &mut WithSchemaContext) -> Schema {
        Schema::Primitive(SchemaPrimitive::schema_string((Default::default())))
    }
}
impl Packed for MyPathBuf {
}
impl Serialize for MyPathBuf {
    fn serialize<'a>(&self, serializer: &mut Serializer<impl std::io::Write>) -> Result<(), SavefileError> {
        self.path.serialize(serializer)
    }
}
impl Deserialize for MyPathBuf {
    fn deserialize(deserializer: &mut Deserializer<impl std::io::Read>) -> Result<Self, SavefileError> {
        Ok(MyPathBuf { path : String::deserialize(deserializer)? } )
    }
}

§Introspection

The Savefile crate also provides an introspection feature, meant for diagnostics. This is implemented through the trait Introspect. Any type implementing this can be introspected.

The savefile-derive crate supports automatically generating an implementation for most types.

The introspection is purely ‘read only’. There is no provision for using the framework to mutate data.

Here is an example of using the trait directly:

extern crate savefile;
#[macro_use]
extern crate savefile_derive;
use savefile::Introspect;
use savefile::IntrospectItem;
#[derive(Savefile)]
struct Weight {
    value: u32,
    unit: String
}
#[derive(Savefile)]
struct Person {
    name : String,
    age: u16,
    weight: Weight,
}
fn main() {
    let a_person = Person {
        name: "Leo".into(),
        age: 8,
        weight: Weight { value: 26, unit: "kg".into() }
    };
    assert_eq!(a_person.introspect_len(), 3); //There are three fields
    assert_eq!(a_person.introspect_value(), "Person"); //Value of structs is the struct type, per default
    assert_eq!(a_person.introspect_child(0).unwrap().key(), "name"); //Each child has a name and a value. The value is itself a &dyn Introspect, and can be introspected recursively
    assert_eq!(a_person.introspect_child(0).unwrap().val().introspect_value(), "Leo"); //In this case, the child (name) is a simple string with value "Leo".
    assert_eq!(a_person.introspect_child(1).unwrap().key(), "age");
    assert_eq!(a_person.introspect_child(1).unwrap().val().introspect_value(), "8");
    assert_eq!(a_person.introspect_child(2).unwrap().key(), "weight");
    let weight = a_person.introspect_child(2).unwrap();
    assert_eq!(weight.val().introspect_child(0).unwrap().key(), "value"); //Here the child 'weight' has an introspectable weight obj as value
    assert_eq!(weight.val().introspect_child(0).unwrap().val().introspect_value(), "26");
    assert_eq!(weight.val().introspect_child(1).unwrap().key(), "unit");
    assert_eq!(weight.val().introspect_child(1).unwrap().val().introspect_value(), "kg");
}

§Introspect Details

By using #[derive(SavefileIntrospectOnly)] it is possible to have only the Introspect-trait implemented, and not the serialization traits. This can be useful for types which aren’t possible to serialize, but you still wish to have introspection for.

By using the #[savefile_introspect_key] attribute on a field, it is possible to make the generated crate::Introspect::introspect_value return the string representation of the field. This can be useful, to have the primary key (name) of an object more prominently visible in the introspection output.

Example:


#[derive(Savefile)]
pub struct StructWithName {
    #[savefile_introspect_key]
    name: String,
    value: String
}

§Higher level introspection functions

There is a helper called crate::Introspector which allows to get a structured representation of parts of an introspectable object. The Introspector has a ‘path’ which looks in to the introspection tree and shows values for this tree. The advantage of using this compared to just using format!("{:#?}",mystuff) is that for very large data structures, unconditionally dumping all data may be unwieldy. The author has a struct which becomes hundreds of megabytes when formatted using the Debug-trait in this way.

An example:


extern crate savefile;
#[macro_use]
extern crate savefile_derive;
use savefile::Introspect;
use savefile::IntrospectItem;
use savefile::prelude::*;
#[derive(Savefile)]
struct Weight {
    value: u32,
    unit: String
}
#[derive(Savefile)]
struct Person {
    name : String,
    age: u16,
    weight: Weight,
}
fn main() {
    let a_person = Person {
        name: "Leo".into(),
        age: 8,
        weight: Weight { value: 26, unit: "kg".into() }
    };

    let mut introspector = Introspector::new();

    let result = introspector.do_introspect(&a_person,
        IntrospectorNavCommand::SelectNth{select_depth:0, select_index: 2}).unwrap();

    println!("{}",result);
    /*
    Output is:

   Introspectionresult:
       name = Leo
       age = 8
       eight = Weight
       value = 26
       unit = kg

     */
    // Note, that there is no point in using the Introspection framework just to get
    // a debug output like above, the point is that for larger data structures, the
    // introspection data can be programmatically used and shown in a live updating GUI,
    // or possibly command line interface or similar. The [crate::IntrospectionResult] does
    // implement Display, but this is just for convenience.

}

The crate::Introspector object can be used to navigate inside an object being introspected. A GUI-program could allow an operator to use arrow keys to navigate the introspected object.

Every time crate::Introspector::do_introspect is called, a crate::IntrospectorNavCommand is given which can traverse the tree downward or upward. In the example in the previous chapter, SelectNth is used to select the 2nd children at the 0th level in the tree.

§Troubleshooting

§The compiler complains that it cannot find item ‘deserialize’ on a type

Maybe you get an error like:

the function or associated item `deserialize` exists for struct `Vec<T>`, but its trait bounds were not satisfied

First, check that you’ve derived ‘Savefile’ for the type in question. If you’ve implemented the Savefile traits manually, check that you’ve implemented both [crate::prelude::Deserialize] and [crate::prelude::Packed]. Without Packed, vectors cannot be deserialized, since savefile can’t determine if they are safe to serialize through simple copying of bytes.

Modules§

  • The prelude contains all definitions thought to be needed by typical users of the library

Structs§

  • A method exposed through savefile-abi. Contains a name, and a signature.
  • The definition of an argument to a method
  • Return value and argument types for a method
  • Helper struct which represents a field which has been removed. This, in contrast to ‘AbiRemoved’,
  • Defines a dyn trait, basically
  • Useful zero-sized marker. It serializes to a magic value, and verifies this value on deserialization. Does not consume memory data structure. Useful to troubleshoot broken Serialize/Deserialize implementations.
  • Object from which bytes to be deserialized are read. This is basically just a wrapped std::io::Read object, the version number of the file being read, and the current version number of the data structures in memory.
  • A field is serialized according to its value. The name is just for diagnostics.
  • Type of single child of introspector for Mutex
  • Type of single child of introspector for RwLock
  • Standard child for Introspect trait. Simply owned key string and reference to dyn Introspect
  • Type of single child of introspector for std::sync::Mutex
  • A node in the introspection tree
  • Identifies an introspected element somewhere in the introspection tree of an object.
  • All fields at a specific depth in the introspection tree
  • An introspection tree. Note that each node in the tree can only have one expanded field, and thus at most one child (a bit of a boring ‘tree’ :-) ).
  • A helper which allows navigating an introspected object. It remembers a path down into the guts of the object.
  • Marker used to promise that some type fulfills all rules for the “Packed”-optimization.
  • Helper struct which represents a field which has been removed
  • An array is serialized by serializing its items one by one, without any padding. The dbg_name is just for diagnostics.
  • An enum is serialized as its u8 variant discriminant followed by all the field for that variant. The name of each variant, as well as its order in the enum (the discriminant), is significant. The memory format is given by ‘has_explicit_repr’, ‘discriminant_size’, ‘size’, ‘alignment’ and the vairous variants.
  • A struct is serialized by serializing its fields one by one, without any padding. The dbg_name is just for diagnostics. The memory format is given by size, alignment and the various field offsets. If any field lacks an offset, the memory format is unspecified.
  • Object to which serialized data is to be written. This is basically just a wrapped std::io::Write object and a file protocol version number. In versions prior to 0.15, ‘Serializer’ did not accept a type parameter. It now requires a type parameter with the type of writer to operate on.
  • An enum variant is serialized as its fields, one by one, without any padding.
  • Context object used to keep track of recursion. Datastructures which cannot contain recursion do not need to concern themselves with this. Recursive data structures in rust require the use of Box, Vec, Arc or similar. The most common of these datatypes from std are supported by savefile, and will guard against recursion in a well-defined way. As a user of Savefile, you only need to use this if you are implementing Savefile for container or smart-pointer type.

Enums§

  • Ways in which introspection may fail
  • A command to navigate within an introspected object
  • Defines little-endian serialization.
  • This object represents an error in deserializing or serializing an item.
  • The schema represents the save file format of your data structure. It is a tree, consisting of various types of nodes in the savefile format. Custom Serialize-implementations cannot add new types to this tree, but must reuse these existing ones. See the various enum variants for more information.
  • A primitive is serialized as the little endian representation of its type, except for string, which is serialized as an usize length followed by the string in utf8. These always have a specified memory format, except for String, which can in theory be unspecified.
  • The actual layout in memory of a Vec-like datastructure. If this is ‘Unknown’, the memory format is unspecified. Otherwise, it is as given by the variant.

Constants§

  • The current savefile version. This versions number is used for serialized schemas. There is an ambition that savefiles created by earlier versions will be possible to open using later versions. The other way around is not supported.
  • As a sort of guard against infinite loops, the default ‘len’-implementation only ever iterates this many times. This is so that broken ‘introspect_child’-implementations won’t cause introspect_len to iterate forever.

Traits§

  • This trait must be implemented for all data structures you wish to be able to deserialize.
  • Gives the ability to look into an object, inspecting any children (fields).
  • A child of an object implementing Introspect. Is a key-value pair. The only reason this is not simply (String, &dyn Introspect) is that Mutex wouldn’t be introspectable in that case. Mutex needs something like (String, MutexGuard<T>). By having this a trait, different types can have whatever reference holder needed (MutexGuard, RefMut etc).
  • This trait describes whether a type is such that it can just be blitted. See method repr_c_optimization_safe. Note! The name Packed is a little misleading. A better name would be ‘packed’
  • This trait must be implemented for all data structures you wish to be able to serialize. To actually serialize data: create a Serializer, then call serialize on your data to save, giving the Serializer as an argument.
  • Something that can construct a value of type T
  • This trait must be implemented by all data structures you wish to be able to save. It must encode the schema for the datastructure when saved using the given version number. When files are saved, the schema is encoded into the file. when loading, the schema is inspected to make sure that the load will safely succeed. This is only for increased safety, the file format does not in fact use the schema for any other purpose, the design is schema-less at the core, the schema is just an added layer of safety (which can be disabled).

Functions§

  • Calculate the memory layout of &[T]. I.e, of the reference to the data. This type is typically 16 bytes, consisting of two words, one being the length, the other being a pointer to the start of the data.
  • Calculate the memory layout of a Vec of the given type
  • Deserialize a slice into a Vec Unsized slices cannot be deserialized into unsized slices.
  • Return a (kind of) human-readable description of the difference between the two schemas. The schema ‘a’ is assumed to be the current schema (used in memory). Returns None if both schemas are equivalent This does not care about memory layout, only serializability.
  • Create a new WithSchemaContext, and then call ‘schema’ on type T. This is a useful convenience method.
  • Create a default IntrospectItem with the given key and Introspect.
  • Deserialize an instance of type T from the given reader . The current type of T in memory must be equal to version. The deserializer will use the actual protocol version in the file to do the deserialization.
  • Like crate::load , except it deserializes from the given file in the filesystem. This is a pure convenience function.
  • Like crate::load_noschema , except it deserializes from the given file in the filesystem. This is a pure convenience function.
  • Deserialize an instance of type T from the given u8 slice . The current type of T in memory must be equal to version. The deserializer will use the actual protocol version in the file to do the deserialization.
  • Like crate::load , but used to open files saved without schema, by one of the _noschema versions of the save functions.
  • Create a Deserializer. Don’t use this method directly, use the crate::load function instead.
  • Write the given data to the writer. The current version of data must be version.
  • Write the given data to the writer. Compresses data using ‘bzip2’ compression format. The current version of data must be version. The resultant data can be loaded using the regular load-function (it autodetects if compressions was active or not). Note, this function will fail if the bzip2-feature is not enabled.
  • Like crate::save , except it opens a file on the filesystem and writes the data to it. This is a pure convenience function.
  • Like crate::save_noschema , except it opens a file on the filesystem and writes the data to it. This is a pure convenience function.
  • Write the given data to the writer. The current version of data must be version. Do this write without writing any schema to disk. As long as all the serializers and deserializers are correctly written, the schema is not necessary. Omitting the schema saves some space in the saved file, but means that any mistake in implementation of the Serialize or Deserialize traits will cause hard-to-troubleshoot data corruption instead of a nice error message.
  • Serialize the given data and return as a Vec<u8> The current version of data must be version.