#serde #serde-derive #silkroad #serialization #deserialize #macro #skrillax-serde

macro skrillax-serde-derive

Derive macro to automatically implement seralization/deserialization for stuctures

2 releases

new 0.1.1 Apr 28, 2024
0.1.0 Apr 21, 2024

#8 in #silkroad

Download history 106/week @ 2024-04-15 160/week @ 2024-04-22

266 downloads per month
Used in skrillax-serde

MIT license

41KB
862 lines

skrillax-serde-derive

This is a #[derive] macro that implements Serialize, Deserialize, and ByteSize from skrillax-serde for a given type. For examples and documentation, please check the module documentation. You may also check out the tests in the derive test directory.


lib.rs:

Generally it should be enough to simply #[derive(Deserialize)] or whichever trait you need. Just like the more general serde crate, this will handle most common things, like fields of different types, including references to other structures. However, there are a few things to keep in mind. Silkroad Online packets are not self-specifying, and thus we often need to provide just a little bit of help to serialize/deserialize some kinds of data. In general, you can provide additional options through the #[silkroad] tag. Which options are available for which elements will be explained in the following section.

Enums

Enums are generally serialized as one byte discriminant, followed by the content of that variant without further details. Currently, we don't automatically map the index of the enum variant to the discriminant. As such, you need to define a value manually. This can be done using #[silkroad(value = 1)] to set the variants byte value to 1:

#[derive(Serialize, Deserialize)]
enum Hello {
    #[silkroad(value = 1)]
    ClientHello(String),
    #[silkroad(value = 2)]
    ServerHello(String)
}

In some cases it may be necessary for the discriminant to be two bytes wide, which you can specify using #[silkroad(size = 2)] on the enum itself:

#[derive(Serialize, Deserialize)]
#[silkroad(size = 2)]
enum Hello {
    #[silkroad(value = 0x400D)]
    ClientHello(String)
}

Structs

Structs are always serialized/deserialized by serializing/deserializing their fields. A unit struct therefor has length zero. There are also no options currently to alter the behavior for structs themselves, only their fields.

#[derive(Serialize, Deserialize)]
struct Hello(String);

Fields

The serialization/deserialization of fields is identical between structs and enums. Each field is serialized one after another without any separators. Therefor, it is necessary to match the size exactly to the consumed bytes. Fields are serialized and deserialized in the order they are defined.

#[derive(Serialize, Deserialize)]
struct Hello {
    one_byte: u8,
    two_bytes: u16
}

Collections

Collections (i.e. vectors) are encoded using one byte length followed by the elements of the collection without a separator. If the size is larger, this needs to be denoted using the #[silkroad(size = 2)] attribute.

#[derive(Serialize, Deserialize)]
struct Hello {
    #[silkroad(size = 2)]
    greetings: Vec<String>
}

The default size is 1 with a size of up to 4 being supported. Additionally, you may change the type of encoding for a collection using the list_type attribute. This accepts one of three options: length (default), break, and has-more. break and has-more specify before each element if another element will follow using different values. break uses 1 for 'has more values' and 2 for finished, while has-more uses 1 for more elements and 0 for being finished.

#[derive(Serialize, Deserialize)]
struct Hello {
    #[silkroad(list_type = "break")]
    greetings: Vec<String>
}

Strings

Generally a string is encoded using two bytes length and then the UTF-8 representation of that string. In some cases, Silkroad however uses two byte wide characters (UTF-16) in strings. This can be configured by using a size of 2.

#[derive(Serialize, Deserialize)]
struct Hello {
    #[silkroad(size = 2)]
    greeting: String
}

Optional

Optional values will be encoded using a byte denoting the presence (1) or absence (0), following the underlying value if it is present. In some cases, due to previous knowledge, optional values may just appear (or be missing) without the presence indicator. This makes them impossible to deserialize (currently), but this is unfortunately current necessary. To achieve this, you can set the size of the field to 0.

#[derive(Serialize)]
struct Hello {
    #[silkroad(size = 0)]
    greeting: Option<String>
}

Alternatively, if there is an indication in the data whether the value will be present or not, you can use the when attribute to specify a condition. In that case the presence byte will be omitted as well, but makes it possible to be deserialized. This does not make any checks for serialization and will always append a present value, ignoring the condition. The condition in when should denote an expression which returns a boolean, showing if the values is present in the packet or not. It is possible to access any previous values, but is currently limited to expressions without imports.

#[derive(Deserialize, Serialize)]
struct Hello {
    condition: u8
    #[silkroad(when = "condition == 1")]
    greeting: Option<String>
}

Dependencies

~0.7–1.2MB
~26K SLoC