Getting Started
Your Protocol in 5 Steps
Create a Crate
It’s best to create a separate crate for your protocol. This helps keep your code clean and organized. Simply create a new crate and include it in your project’s workspace.
Define Blocks
Decide on the blocks for your protocol. Just create Rust structs and annotate them with the #[block]
macro.
#[block]
pub struct MyBlock {
pub field_u8: u8,
pub field_u32: u32,
pub field_u64: u64,
pub blob: [u8; 1000],
}
Define Payload
Once the blocks are ready, define the payload. Annotate a struct or enum with the #[payload(bincode)]
macro.
(We recommend starting with the bincode feature.)
#[derive(Default, serde::Deserialize, serde::Serialize)]
pub enum MyNestedEntity {
One(String),
Two(Vec<u8>),
#[default]
Three,
}
#[payload(bincode)]
#[derive(Default, serde::Deserialize, serde::Serialize)]
pub struct MyPayload {
pub field_u8: u8,
pub field_u16: u16,
pub field_u32: u32,
pub field_u64: u64,
pub field_u128: u128,
pub field_nested: MyNestedEntity,
}
Add Code Generation
You must call brec::generate!() in a single location in your code to let brec generate the required code for your blocks and payloads.
The best place to do this is in the lib.rs file of the crate you created.
Make sure all blocks and payloads are in scope at the call site of brec::generate!()
.
use blocks::*; // Make your blocks visible
use payloads::*; // Make your payloads visible
// Let `brec` generate a code
brec::generate!();
Final Step
Add a simple build.rs
script.
brec::build_setup();
Done
Your protocol is ready to be used and you can create your first packet
let my_packet = Packet::new(
// You are limited to 255 blocks per packet.
vec![
Block::MyBlock(MyBlock::default()),
],
// Note: payload is optional
Some(Payload::MyPayload(MyPayload::default()))
);
Blocks and Payloads in details
brec
includes powerful macros that allow defining the components of a protocol with minimal effort. For example, to define a structure as a block (Block
), you simply need to use the block
macro:
#[brec::block]
pub struct MyBlock {
pub field_u8: u8,
pub field_u16: u16,
pub field_u32: u32,
pub field_u64: u64,
pub field_u128: u128,
pub field_i8: i8,
pub field_i16: i16,
pub field_i32: i32,
pub field_i64: i64,
pub field_i128: i128,
pub field_f32: f32,
pub field_f64: f64,
pub field_bool: bool,
pub blob_a: [u8; 1],
pub blob_b: [u8; 100],
pub blob_c: [u8; 1000],
pub blob_d: [u8; 10000],
}
The block
macro automatically generates all the necessary code for MyBlock
to function as a block. Specifically, it adds:
- A unique block signature based on its name and path.
- A CRC
field to ensure data integrity.
All user-defined blocks are ultimately included in a generated enumeration Block
, as shown in the example:
#[brec::block]
pub struct MyBlockA {
pub field: u8,
pub blob: [u8; 100],
}
#[brec::block]
pub struct MyBlockB {
pub field_u16: u16,
pub field_u32: u32,
}
#[brec::block]
pub struct MyBlockC {
pub field: u64,
pub blob: [u8; 10000],
}
// Instruct `brec` to generate and include all protocol types
brec::generate!();
// Generated by `brec`
pub enum Block {
MyBlockA(MyBlockA),
MyBlockB(MyBlockB),
MyBlockC(MyBlockC),
}
The generated Block
enumeration is always returned to the user as a result of message (packet) parsing, allowing for easy identification of blocks by their names.
Similarly to blocks, defining a payload can be done with a simple call to the payload
macro.
#[derive(serde::Deserialize, serde::Serialize)]
pub enum MyNestedEntity {
One(String),
Two(Vec<u8>),
Three,
}
#[payload(bincode)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct MyPayload {
pub field_u8: u8,
pub field_u16: u16,
pub field_u32: u32,
pub field_u64: u64,
pub field_u128: u128,
pub field_nested: MyNestedEntity,
}
A payload is an unrestricted set of data (unlike Block
, which is limited to primitive data). It can be either a struct
or an enum
with unlimited nesting levels. Additionally, there are no restrictions on the use of generic types.
brec
imposes only one requirement for payloads: they must implement the traits PayloadEncode
and PayloadDecode<T>
, which enable encoding and decoding of data into target types.
Out of the box (with the bincode
feature), brec
provides automatic support for the required traits by leveraging the bincode
crate. This means that to define a fully functional payload, it is enough to use #[payload(bincode)]
, eliminating the need for manually implementing the PayloadEncode
and PayloadDecode<T>
traits.
Note that bincode
requires serialization and deserialization support, which is why the previous examples include #[derive(serde::Deserialize, serde::Serialize)]
.
With brec
, defining protocol types (blocks and payloads) is reduced to simply defining structures and annotating them with the block
and payload
macros.
Simple Packet Construction
Once the protocol data types have been defined, the next step is to include the "unifying" code generated by brec
using brec::generate!();
#[brec::block]
pub struct MyBlockA { ... }
#[brec::block]
pub struct MyBlockB { ... }
#[brec::block]
pub struct MyBlockC { ... }
#[payload(bincode)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct MyPayloadA { ... }
#[payload(bincode)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct MyPayloadB { ... }
#[payload(bincode)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct MyPayloadC { ... }
// Instruct `brec` to generate and include all protocol types
brec::generate!();
// Now available:
// Generalized block representation
pub enum Block {
MyBlockA(MyBlockA),
MyBlockB(MyBlockB),
MyBlockC(MyBlockC),
}
// Generalized payload representation
pub enum Payload {
MyPayloadA(MyPayloadA),
MyPayloadB(MyPayloadB),
MyPayloadC(MyPayloadC),
}
// Packet type
pub type Packet = brec::PacketDef<Block, Payload, Payload>;
Once all protocol types are defined and the unifying code is generated, you can start creating packets:
let my_packet = Packet::new(
// You are limited to 255 blocks per packet.
vec![
Block::MyBlockA(MyBlockA::default()),
Block::MyBlockC(MyBlockC::default()),
],
// Note: payload is optional
Some(Payload::MyPayloadA(MyPayloadA::default()))
);
At this point, your protocol is ready for use. Packet
implements all the necessary methods for reading from and writing to a data source.
Code Generation Note
Pay special attention: using brec
involves code generation. Therefore, after defining the structure of your protocol (blocks
and payloads
), you must call brec::generate!()
. This macro can be invoked only once and only in a single location in your code.
Additionally, you will need to add a very simple build.rs
script. For more details, see the Code Generation section.
Performance, Security, and Efficiency
brec
is a binary protocol, meaning data is always transmitted and stored in a binary format.
The protocol ensures security through the following mechanisms:
- Each block includes a unique signature generated based on the block's name. Name conflicts within a single crate are eliminated, as the module path is taken into account.
- Similar to blocks, each payload also has a unique signature derived from its name.
- Additionally, Packet
itself has a fixed 64-bit signature.
These features enable reliable entity recognition within a data stream. Furthermore, blocks, payloads, and the packet itself have their own CRCs. While blocks always use a 32-bit CRC, payloads allow for optional support of 64-bit or 128-bit CRC to enhance protocol security.
brec
ensures maximum performance through the following optimizations:
- Minimization of data copying and cloning operations.
- Incremental packet parsing: first, blocks are parsed, allowing the user to inspect them and decide whether the packet should be fully parsed (including the payload) or skipped. This enables efficient packet filtering based on block values, avoiding the overhead of parsing a heavy payload.
- If data integrity verification is not required, brec
allows CRC to be disabled for all types or selectively. This improves performance by eliminating the need for hash calculations.
The conceptual separation of a packet into blocks and a payload allows users to efficiently manage traffic load. Blocks can carry "fast" information that requires quick access, while the payload can implement more complex encoding/decoding mechanisms (such as data compression). Filtering based on blocks helps avoid unnecessary operations on the payload when they are not required.