serial
import serial from "serial";
import json from "json";
struct Player {
name: string;
@serial.rename("hp") health: i64;
@serial.skip cache: *u8;
}
@tape { json.gen_codec(Player); }
let p: Player = Player { name: "Ada", health: 3, cache: 0 as *u8 };
io.println(json.stringify(&to_json_Player(&p))); // {"name":"Ada","hp":3}serial is how you say which parts of a struct are saved, independently
of the format they are saved in. You mark the struct once; each format
(JSON today, with json.gen_codec) reads the same
markings, so adding another format later doesn’t mean marking your types
again.
You make a struct serializable by calling a format’s generator on it. Every field takes part unless you mark it otherwise:
| Attribute | On | Means |
|---|---|---|
@serial.skip | a field | Never saved; when loading, the field keeps its default (zero if it has none). A skipped field may have any type, a pointer included |
@serial.rename("name") | a field | Saved under name instead of the field’s own name |
@serial.explicit | a struct | Only the fields marked @serial.field are saved |
@serial.field | a field of an explicit struct | This field is saved |
@serial.optional | a field | When loading, the field may be missing: it is then zero (an empty Vec, 0, null). A field with a default may be missing anyway |
@serial.explicit struct Save {
@serial.field level: i64;
@serial.field @serial.rename("who") player: string;
frame_time: f64; // runtime state: not saved
}Use @serial.explicit for types that hold mostly runtime state, where
listing what is saved is shorter than listing what isn’t.
What a field can hold
The same in every format:
bool, any integer,f32,f64,string;- an enum (text formats write its variant’s name);
- a struct, whose own codec you generated first;
- a
?S(none isnull), a[N]Sor aVec_S(any length;collections.gen_vec) of those; - a coroutine frame, as a plain field.
Any other field type (a pointer, a slice, a tagged union) is a compile error naming the format, the field and the struct, unless the field is skipped:
error: json.gen_codec: field cache of Player has no serialized form (a bool, integer, float, string, enum or struct, or a ?S, [N]S or Vec_S of those; or mark it @serial.skip)Formats
Each format is its own module with a gen_codec(T) generator, and you can
use several on the same struct:
| Format | Generator | Emits |
|---|---|---|
| JSON | json.gen_codec(T) | to_json_T, from_json_T (json §Structs) |
A format may also have attributes of its own for what only it needs, such
as a field number for a binary format. Those sit next to serial’s on
the same field.
The attributes work under any import name: with import s from "serial";
you write @s.skip, and every format still sees serial.skip.
Frames
A coroutine’s frame is saved by code the compiler makes, through two small interfaces every format implements once:
pub struct Writer {
ctx: *u8; // the format's own state
begin: fn(*u8, []const u8); // open a record named name
end: fn(*u8);
integer: fn(*u8, []const u8, i64);
unsigned: fn(*u8, []const u8, u64);
real: fn(*u8, []const u8, f64);
boolean: fn(*u8, []const u8, bool);
text: fn(*u8, []const u8, []const u8);
}
pub struct Reader {
ctx: *u8;
enter: fn(*u8, []const u8) -> bool; // into the record named name
leave: fn(*u8);
integer: fn(*u8, []const u8, *i64) -> bool; // false: missing or another kind
unsigned: fn(*u8, []const u8, *u64) -> bool;
real: fn(*u8, []const u8, *f64) -> bool;
boolean: fn(*u8, []const u8, *bool) -> bool;
text: fn(*u8, []const u8, *string) -> bool;
}@frame_save(&frame, &writer) and @frame_load(&frame, &reader) call
them; they fail with a serial.FrameError: ForeignPointer,
LayoutChanged or Malformed. JSON’s implementation is
json.frame_writer / json.frame_reader (json); a
format of your own fills a Writer and a Reader with its functions and
its state the same way.
Last modified: