Rust protocol implementation

General

Create simple empty rust project

.
├── Cargo.toml
├── prot
│      └── protocol.prot      <== protocol scheme, see below
└── src
       └── main.rs

Let's use for a demonstration next simple protocol scheme. Here is more about protocol syntax.

struct StructExampleA {
    str field_str;
    u32 field_u32;
    u64 field_u64;
    i8 field_i8;
    i16 field_i16;
    f64 field_f64;
    bool field_bool;
}

struct StructExampleB {
    str[] field_str;
    u8[] field_u8;
    i8[] field_i8;
    bool[] field_bool;
    StructExampleA field_struct;
    StructExampleA[] field_structs;
}

struct StructExampleC {
    str field_str?;
    u8 field_u8?;
    i16 field_i16?;
    i32 field_i32?;
    i64 field_i64?;
    bool field_bool?;
}

Dependencies

Take into account, protocol implementation requeired some crates. So add next dependencies into curgo.toml:

Generate

As soon as we have protocol scheme, let's generate code-base.

clibri --src ./prot/protocol.prot -rs ./src/protocol.rs -o --em

# --src path_to_protocol_scheme
# -o overwrite protocol.rs file if it already exists
# --em include all code to avoid using CLIBRI library
# -rs path_to_rust_implementation_file

Now our solution looks like it:

.
├── Cargo.toml
├── prot
│      └── protocol.prot      
└── src
       ├── main.rs
       └── protocol.rs     <== protocol implementation

All our protocol's messages now are available in protocol.rs module.

pub mod protocol; fn main() { let struct_example_a = protocol::StructExampleA { field_str: String::from("test"), field_u32: 3, field_u64: 4, field_i8: -1, field_i16: -2, field_f64: 0.2, field_bool: true, }; }

So, every structure or enum would be a struct and enum; group - mod (module).

Take into account, sometimes we need to do tests with some kind of dummy data, but if we have complex structures it might be quite annoying. You always can use static method defaults to get an instance of your structure with dummy data.

pub mod protocol; fn main() { let mut struct_example_a = protocol::StructExampleA::defaults(); let mut struct_example_b = protocol::StructExampleB::defaults(); let mut struct_example_c = protocol::StructExampleC::defaults(); }

There are a couple of important methods related to decode/encode and packing:

Signature Trait Description
abduct(&mut self) -> Result<Vec<u8>, String> StructEncode Encode instance of struct/enum and returns bytes
extract(buf: Vec<u8>) -> Result<Self, String> StructDecode Makes attempt to create an instance of structure from the given buffer
defaults() -> Self StructDecode Returs self instance with default values
pack(
&mut self,
sequence: u32,
uuid: Option<String>) -> Result<Vec<u8>, String>
PackingStruct Packs struct/enum and retures bytes

As you can see methods abduct and pack look pretty the same. Both return bytes. The important difference is: the method pack prepares structure to send and if you are going to use CLIBRI reader, you should use a method pack. Because this method adds a header to the structure to correctly decode it after.

Let's see how CLIBRI reader works

pub mod protocol; use protocol::{PackingStruct, StructDecode, StructEncode}; fn reading() -> Result<(), String> { // Create a couple of examples let mut struct_example_a = protocol::StructExampleA::defaults(); let mut struct_example_b = protocol::StructExampleB::defaults(); let mut struct_example_c = protocol::StructExampleC::defaults(); // Create reader let mut reader = protocol::Buffer::new(); // Create a buffer and fill it with example data let buffer: Vec<u8> = [ struct_example_a.pack(1, None).map_err(|e| e.to_string())?, struct_example_b.pack(2, None).map_err(|e| e.to_string())?, struct_example_c.pack(3, None).map_err(|e| e.to_string())?, ] .concat(); // Put data into reader reader .chunk(&buffer, None) .map_err(|e| format!("Fail to add data: {:?}", e))?; // Reading messages while let Some(msg) = reader.next() { match msg.msg { protocol::AvailableMessages::StructExampleA(struct_a) => { println!("{:?}", struct_a); } protocol::AvailableMessages::StructExampleB(struct_b) => { println!("{:?}", struct_b); } protocol::AvailableMessages::StructExampleC(struct_c) => { println!("{:?}", struct_c); } _ => {} } } Ok(()) } fn main() { reading().expect("Oops!"); }

Buffer has two major methods

Signature Description
chunk(
&mut self,
buf: &Vec<u8>,
uuid: Option<String>) -> Result<(), ReadError>
Add data into reader buffer
next(&mut self) -> Option<IncomeMessage<T>> Returns decoded message or None if buffer doesn't have any messages

In some cases, it might be you want to do some additional actions on a buffer before it would be sent. For example, compress and decompress data. For such kinds of purposes, you can use struct PackingMiddleware with your own implementation.

pub mod protocol; use protocol::PackingMiddleware; impl PackingMiddleware { fn decode( buffer: Vec<u8>, _id: u32, _sequence: u32, _uuid: Option<String>, ) -> Result<Vec<u8>, String> { // Do something... Decompress for example Ok(buffer) } fn encode( buffer: Vec<u8>, _id: u32, _sequence: u32, _uuid: Option<String>, ) -> Result<Vec<u8>, String> { // Do something... Compress for example Ok(buffer) } }

More examples you can find on github

Packing vs encoding

As you already noticed there is a difference between packing and encoding, even both give bytes.

Packing adds a header to each message, which includes

At the same time encoding just gives you a byte representation of your message without any data, which would need to decode the message.