serial

tape
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:

AttributeOnMeans
@serial.skipa fieldNever 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 fieldSaved under name instead of the field’s own name
@serial.explicita structOnly the fields marked @serial.field are saved
@serial.fielda field of an explicit structThis field is saved
@serial.optionala fieldWhen loading, the field may be missing: it is then zero (an empty Vec, 0, null). A field with a default may be missing anyway
tape
@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 is null), a [N]S or a Vec_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:

text
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:

FormatGeneratorEmits
JSONjson.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:

tape
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: