5.1.41. Serialization (archive)
This tutorial covers binary serialization using the daslib/archive module.
The module provides automatic serialization for all built-in types, structs,
arrays, tables, tuples, and variants.
require daslib/archive
5.1.41.1. Primitive serialization
mem_archive_save serializes any value to an array<uint8>.
mem_archive_load deserializes it back:
var x = 42
var data <- mem_archive_save(x)
print("int data size: {length(data)} bytes\n") // 4 bytes
var loaded_x : int
mem_archive_load(data, loaded_x)
print("loaded int: {loaded_x}\n") // 42
This works for int, float, string, and all other primitive types.
5.1.41.2. Struct serialization
Structs are serialized field by field automatically:
struct Player {
name : string
hp : int
x : float
y : float
}
var player = Player(name = "Hero", hp = 100, x = 1.5, y = 2.5)
var data <- mem_archive_save(player)
var loaded : Player
mem_archive_load(data, loaded)
print("{loaded.name}, hp={loaded.hp}\n") // Hero, hp=100
5.1.41.3. Array serialization
Dynamic arrays serialize their length followed by all elements:
var scores <- [10, 20, 30, 40, 50]
var data <- mem_archive_save(scores)
var loaded : array<int>
mem_archive_load(data, loaded)
// loaded contains [10, 20, 30, 40, 50]
5.1.41.4. Table serialization
Tables serialize their length followed by key-value pairs:
var inventory : table<string; int>
inventory |> insert("sword", 1)
inventory |> insert("potion", 5)
inventory |> insert("arrow", 20)
var data <- mem_archive_save(inventory)
var loaded : table<string; int>
mem_archive_load(data, loaded)
Note
Table iteration order may vary, but all key-value pairs are preserved.
5.1.41.5. Tuple serialization
var pair : tuple<name:string; score:int>
pair.name = "Alice"
pair.score = 99
var data <- mem_archive_save(pair)
var loaded : tuple<name:string; score:int>
mem_archive_load(data, loaded)
print("name={loaded.name}, score={loaded.score}\n")
5.1.41.6. Variant serialization
Variants serialize the active variant index plus its value. Requires
unsafe for variant field access:
variant Value {
i : int
f : float
s : string
}
unsafe {
var v1 = Value(i = 42)
var data <- mem_archive_save(v1)
var loaded : Value
mem_archive_load(data, loaded)
if (loaded is i) {
print("loaded: int = {loaded.i}\n") // 42
}
}
5.1.41.7. Nested structures
Serialization is recursive — structs containing arrays, tables, and other structs are handled automatically:
struct Inventory {
items : array<string>
counts : table<string; int>
}
struct GameState {
player : Player
inventory : Inventory
level : int
}
var state : GameState
state.player = Player(name = "Knight", hp = 80, x = 10.0, y = 20.0)
state.level = 3
state.inventory.items |> push("sword")
state.inventory.counts |> insert("sword", 1)
var data <- mem_archive_save(state)
var loaded : GameState
mem_archive_load(data, loaded)
5.1.41.8. Manual Archive usage
For full control, create an Archive and MemSerializer manually.
This lets you write multiple values into a single byte stream:
// Writing phase
var writer = new MemSerializer()
var warch = Archive(reading = false, stream = writer)
var name = "save_001"
var score = 9999
var tags <- ["rpg", "fantasy"]
warch |> serialize(name)
warch |> serialize(score)
warch |> serialize(tags)
var data <- writer->extractData()
// Reading phase
var reader = new MemSerializer(data)
var rarch = Archive(reading = true, stream = reader)
var r_name : string
var r_score : int
var r_tags : array<string>
rarch |> serialize(r_name)
rarch |> serialize(r_score)
rarch |> serialize(r_tags)
The same serialize function works for both reading and writing — the
Archive.reading flag determines the direction.
5.1.41.9. Custom serialize
You can override the serialization of any type by defining a serialize
overload with the exact signature def serialize(var arch : Archive; var val : YourType&).
This overload is more specific than the generic struct serializer, so it wins —
as long as the serialize call happens within the module that defines the
overload.
Note
mem_archive_save / mem_archive_load resolve serialize inside
the archive module, so they won’t pick up overloads defined in user code.
Use the manual Archive + MemSerializer pattern instead.
Here a Color struct stores channels as floats but serializes as 3 compact
bytes:
struct Color {
r : float = 0.0
g : float = 0.0
b : float = 0.0
}
def serialize(var arch : Archive; var c : Color&) {
if (arch.reading) {
var rb, gb, bb : uint8
arch |> serialize_raw(rb)
arch |> serialize_raw(gb)
arch |> serialize_raw(bb)
c.r = float(rb) / 255.0
c.g = float(gb) / 255.0
c.b = float(bb) / 255.0
} else {
var rb = uint8(clamp(c.r * 255.0, 0.0, 255.0))
var gb = uint8(clamp(c.g * 255.0, 0.0, 255.0))
var bb = uint8(clamp(c.b * 255.0, 0.0, 255.0))
arch |> serialize_raw(rb)
arch |> serialize_raw(gb)
arch |> serialize_raw(bb)
}
}
Using it with the manual archive pattern:
var c = Color(r = 1.0, g = 0.5, b = 0.0)
var writer = new MemSerializer()
var warch = Archive(reading = false, stream = writer)
warch |> serialize(c)
var data <- writer->extractData()
print("size: {length(data)} bytes\n") // 3 (not 12)
var reader = new MemSerializer(data)
var rarch = Archive(reading = true, stream = reader)
var loaded = Color()
rarch |> serialize(loaded)
The custom overload is also picked up by the generic array serializer when the call-site is in the same module:
var colors <- [Color(r=1.0, g=0.0, b=0.0), Color(r=0.0, g=1.0, b=0.0)]
var writer = new MemSerializer()
var warch = Archive(reading = false, stream = writer)
warch |> serialize(colors) // 4 + 2×3 = 10 bytes
5.1.41.10. Summary
Function |
Description |
|---|---|
|
Serialize any value to |
|
Deserialize from |
|
Read or write depending on |
|
Raw byte read/write (no type dispatch) |
|
Struct combining stream + direction |
|
In-memory byte stream (reading or writing) |
See also
Full source: tutorials/language/41_serialization.das
Previous tutorial: Coroutines
Next tutorial: Testing Tools (faker + fuzzer)