Skip to content

Coder

Coder writes a graph of objects to JSON and reads it back. It is the xc form of Foundation’s NSKeyedArchiver and NSKeyedUnarchiver, in one class. The archive can be gzipped.

#import "Coder.xc"

To archive, pass the root object to archive or archiveJSON. To restore, pass the result to unarchive or unarchiveJSON:

Data* blob = Coder.archive(root, (u8)6); // gzip, level 6
try {
Point* back = (Point* ?)Coder.unarchive(blob);
...
} catch (CoderError e) {
Stdio.printf("%s\n", e.message().cString());
}

Your own classes take part through Codable: the coder calls encodeWithCoder on each object while it writes, and initWithCoder on each new instance while it reads. Inside those methods, the encode… and decode… methods on this page store and fetch values by string key. Only keyed coding exists.

An object that appears several times in the graph is written once, and every reference to it points at that one entry. Shared objects stay shared and a cycle terminates. Identity is the object’s address, so two equal strings that are separate objects stay separate.

The archive is a JSON object table in the shape NSKeyedArchiver uses:

{"$archiver":"Coder","$version":1,
"$top":{"root":{"$ref":1}},
"$objects":["$null",
{"$class":"Point","x":3,"y":4,"next":{"$ref":2}},
{"$class":"Point","x":5,"y":6,"next":{"$ref":1}}]}

Each object is one entry in $objects, and {"$ref":n} refers to entry n. Entry 0 is always "$null", so {"$ref":0} is a null reference. Scalars encoded with encodeI32 and the others are written inline in their object’s entry.

The library’s value types have a native form:

TypeWritten as
Stringa JSON string
Numbera JSON number: 42, 2.5
Data{"$class":"Data","$base64":"3q2+7w=="}
Array{"$class":"Array","$items":[2,3,0]} (indexes into $objects)
Set{"$class":"Set","$items":[4,5]}
Map{"$class":"Map","$keys":[6,7],"$values":[8,0]}

A subclass of one of these is archived as that class.

Numbers are exact. Integers keep all 64 bits. A double is written in the shortest form that reads back as the same value, and reading rounds correctly, so a double survives the round trip bit for bit. A Number holding a float is always written with a . or an exponent (3.0, 1.0e+21) and an integer never is, so the kind survives too. JSON has no spelling for NaN or the infinities: inline they are the strings "NaN", "Infinity" and "-Infinity", and a Number holding one is {"$class":"Number","$double":"NaN"}.

Strings are written as UTF-8, with ", \ and the control characters escaped. A String whose bytes are not valid UTF-8 is written as {"$class":"String","$base64":…} so it comes back unchanged.

Keys beginning with $ are reserved for the format. A key of your own that begins with $ is written with a second $ in front and read back as you wrote it.

With a compression level from 1 to 9, archive gzips the finished JSON once, at that level. unarchive recognises gzip data by its first two bytes and inflates it first.

The gzip code is part of Coder, so no target needs zlib: an RFC 1951 deflate (LZ77 over hash chains, fixed and dynamic Huffman blocks) and a full inflate, in the RFC 1952 wrapper with its CRC-32 and length trailer. gzip -d reads the output, and gunzip reads what gzip writes. Both directions are public as gzip and gunzip for data that is not an archive.

unarchive, unarchiveJSON and gunzip throw a CoderError when the input is not JSON, is not a Coder archive, names a class the program does not have, fails its CRC or ends early. The message says which.

Inside initWithCoder the decode… methods do not throw. A missing key reads as 0, false or null, as in Foundation, so a newer class can read an older archive; containsKey tells the two apart. A value of the wrong type or out of range for the method is recorded, and unarchive throws it once the graph is finished.

Archiving · archive · archiveJSON

Unarchiving · unarchive · unarchiveJSON

Encoding · encodeObject · encodeBool · encodeI32 · encodeU32 · encodeI64 · encodeU64 · encodeFloat · encodeDouble

Decoding · decodeObject · decodeBool · decodeI32 · decodeU32 · decodeI64 · decodeU64 · decodeFloat · decodeDouble · containsKey

gzip · gzip · gunzip · crc32

Errors · CoderError


static Data* archive(Object* root, u8 compression)

The archive of root and everything it refers to. With compression 0 the result is the UTF-8 JSON text; with 1 to 9 it is that text gzipped at that level (1 fastest, 9 smallest; above 9 counts as 9).

static String* archiveJSON(Object* root)

The archive as a JSON String.

static Object* unarchive(Data* data) throws

The root object of an archive made by archive, compressed or not. Downcast the result to the class you expect. Throws a CoderError for anything it cannot read.

static Object* unarchiveJSON(String* json) throws

The same, from JSON text.

Call these from encodeWithCoder. Each writes one value under key. Outside an archive they do nothing.

void encodeObject(Object* obj, string key)

A reference to obj, which is archived too the first time it is seen. Null is allowed.

void encodeBool(bool v, string key)
void encodeI32(i32 v, string key)
void encodeU32(u32 v, string key)
void encodeI64(i64 v, string key)
void encodeU64(u64 v, string key)
void encodeFloat(float v, string key)

Written in the shortest form that reads back as the same float.

void encodeDouble(double v, string key)

Written in the shortest form that reads back as the same double.

Call these from initWithCoder. A missing key gives 0, false or null. A value of the wrong type, or one that does not fit the method’s type, gives the same and makes unarchive throw.

Object* decodeObject(string key)

The object stored under key. Downcast it: (Point* ?)coder.decodeObject("next"). Inside a cycle this can be an object whose own initWithCoder has not finished.

bool decodeBool(string key)
i32 decodeI32(string key)
u32 decodeU32(string key)
i64 decodeI64(string key)
u64 decodeU64(string key)
float decodeFloat(string key)
double decodeDouble(string key)
bool containsKey(string key)

Whether the object being decoded has a value under key.

static Data* gzip(Data* data, u8 level)

data in gzip format, at level 1 to 9. Level 0 stores the bytes uncompressed inside the gzip wrapper.

static Data* gunzip(Data* data) throws

The contents of the first member of a gzip stream. Throws a CoderError for data that is not gzip, is damaged, fails its CRC or length check, or ends early.

static u32 crc32(u8* p, u32 n)

The CRC-32 of n bytes, as gzip and zip use it.

class CoderError <Error> {
String* message(void);
}

What unarchive, unarchiveJSON and gunzip throw. It conforms to Error; message describes the problem, for example Coder: unknown class 'Point' or gzip: CRC mismatch.

↑ Topics