Hot Serializer is a fast serializer/deserializer for odin, that works well for use cases such as hot reloading, where:
- we need it to be really fast, for large amounts of data
- we dont need a human readable or widely compatible format
- we do minimal type changes per iteration (data is mostly identical)
package main
import hs "HotSerializer"
main :: proc(){
A :: struct {
position: [2]f32,
velocity: [2]f32
}
a := A {
position = { 1, 2 },
velocity = { 3, 4 }
}
data: []byte = hs.serialize(&a)
B :: struct {
position: [2]f32,
health: f32,
velocity: [2]f32
}
b: B
hs.deserialize(&b, data)
assert(a.position == b.position)
assert(a.velocity == b.velocity)
assert(b.health == 0)
}you can use struct field tags to change how hs works.
package main
import hs "HotSerializer"
main :: proc(){
A :: struct {
position: [2]f32,
grid_index: int,
temp_value: int,
}
B :: struct {
position: [2]f32,
grid_location: int `hs:"grid_index,grid_id"`,
temp_value: int `hs:"-"`,
}
}grid_locationhas two aliases:grid_indexandgrid_id. hs will look for these field names when deserializing.temp_valuehas a value of-. This tells hs to ignore the field.
Arrays, and now [dynamic; S]T will work as expected, allowing for modifications of length and structure of elements inside.
Since [dynamic; S]T is an inline value, it will work by default without requiring .Dynamics to be enabled.
Enums and their derivatives: bit_sets and enumerated_arrays, are handled properly. Meaning you can reorder, add, or remove enum fields, and as long as names stay consistent, hs will match them up.
This is something currently neither cbor nor json support properly.
Structs and bit_fields similarly can have fields added / removed / modified, and allow for tag features (above)
Unions will match on named values first, i.e Name :: struct, and then will default to matching based on typeid. For more robust union matching across revisions, it is recommended that you use named values. For primitive types you can do this with distinct.
Currently, the supported dynamic types are as follows:
[dynamic]T,
[]T,
string,
cstring.
By default, these types are not serialized. However you can enable this feature with the .Dynamics option.
package main
import hs "HotSerializer"
main :: proc(){
Car :: struct {
top_speed: f32,
acceleration: f32
}
cars: [dynamic]Car
append(&cars, Car { 1, 2 })
append(&cars, Car { 3, 4 })
options := bit_set[hs.Option]{ .Dynamics }
data := hs.serialize(&cars, options = options)
saved_cars: [dynamic]Car
hs.deserialize(&saved_cars, data, options = options)
}The following dynamic types are not currently supported:
map,
^T,
This feature may decrease serialization performance arbitrarily, based on the size and nested-ness of the dynamic data.
currently there is some limited support for primitive type casting. Below is a list of all primitives that can cast between each other:
f16,
f32,
f64,
u8,
u16,
u32,
u64,
uint,
i8,
ì16,
i32,
i64,
int,
b8,
b16,
b32,
b64,
bool,
- hs serialization is always very fast, regardless of the structure of the data.
- hs deserialization can be arbitrarily slower depending on exactly how different the data is.
check out the tests/ directory for more examples on usage.
hs works best when used alongside a style of coding that favours 'inline' values as opposed to 'dynamic' values.
This is because 'inline' values are stored within the containing structure. For example:
State :: struct { items: [dynamic; 5]Item, }
Would in memory look like:
Item | Item | Item | Item | Item | len
Which can be trivially copied and stored.
However a dynamic value:
State :: struct { items: [dynamic]Item }
Would in memory look like:
data | len | capacity | allocator
Since the actual data lives elsewhere in memory, hs now has to manually inspect these dynamic elements, and copy the data they point to.
Therefore hs loses two important optimsations:
- it can no longer do a bulk memcopy of the input when serializing
- it can no longer memcopy identical elements that contain dynamics when deserializing
This is why by default, dynamics are turned off.