#binary #binary-encoding #serialization

macro lbs_derive

Lazy Binary Serialization (derive)

5 releases (3 breaking)

0.4.1 Dec 24, 2023
0.4.0 Dec 24, 2023
0.3.1 Dec 19, 2023
0.2.0 May 10, 2021
0.1.0 May 7, 2021

#47 in #binary-serialization

46 downloads per month
Used in lbs

Custom license

26KB
390 lines

LBS

crates.io | docs.rs

Library name stands for Lazy Binary Serialization. We call it lazy because it does not serizalize/deserialize struct fields of type Option<T> when value is None. When it comes to large structures with significant amount of optional fields this simple technique makes LBS faster than other libraries, where None values must be somehow represented on wire anyway.

Safety

No unsafe code.

Status

API or format changes may be introduced until v1.0.0.

Usage

  1. Add lbs = { version = "0.4.1", features = ["chrono", "smallvec", "ipnet", "uuid", "time"] } to Cargo.toml. Remove features you don't need.
  2. There are LBSWrite and LBSRead traits which implementations can be derived for structs and enums with #[derive(LBSWrite, LBSRead)].
  3. Each field or variant must have an attribute #[lbs(id(<u16>))]. This allows to change order of fields anytime and makes serialization cheaper.
  4. If field is of type Option<T> and it's value is None, it is not serialized/deserialized, at all. Otherwise such a field is required unless it has explicit #[lbs(optional)] attribute.
  5. Each struct field's type must implement Default or such a field must have an attribute #[lbs(default(<expr>))]. Even if field is required. This is because we don't want to use unsafe Rust to initialize structures. For now.
  6. Struct field may be ignored using #[lbs(skip)] attribute.
  7. Attributes may be concatenated like this: #[lbs(id(<u16>), default(<expr>), skip, optional)].
#![allow(unused_imports, dead_code)]

use bytes::Buf;
use bytes::BufMut;
use bytes::Bytes;
use bytes::BytesMut;
use chrono::NaiveDate;
use ipnet::IpNet;
use lbs::error::LBSError;
use lbs::LBSRead;
use lbs::LBSWrite;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::collections::HashSet;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::Ipv6Addr;
use std::ops::Range;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use std::time::SystemTime;
use time::OffsetDateTime;
use uuid::Uuid;

#[derive(LBSWrite, LBSRead)]
struct StructOne<'a> {
    #[lbs(id(0))]
    f0: u8,
    #[lbs(id(1))]
    f1: u16,
    #[lbs(id(2))]
    f2: u32,
    #[lbs(id(3))]
    f3: u64,
    #[lbs(id(4))]
    f4: usize,
    #[lbs(id(5))]
    f5: u128,
    #[lbs(id(6))]
    f6: i8,
    #[lbs(id(7))]
    f7: i16,
    #[lbs(id(8))]
    f8: i32,
    #[lbs(id(9))]
    f9: i64,
    #[lbs(id(10))]
    f10: isize,
    #[lbs(id(11))]
    f11: i128,
    #[lbs(id(12))]
    f12: f32,
    #[lbs(id(13))]
    f13: f64,
    #[lbs(id(14))]
    f14: (),
    #[lbs(id(15))]
    f15: (u64, String),
    #[lbs(id(16))]
    f16: (u64, u64, u64),
    #[lbs(id(17))]
    f17: bool,
    #[lbs(id(18))]
    f18: char,
    #[lbs(id(19))]
    f19: String,
    #[lbs(id(20))]
    f20: Duration,
    #[lbs(id(21), default(SystemTime::UNIX_EPOCH))]
    f21: SystemTime,
    #[lbs(id(22), default(Ipv4Addr::UNSPECIFIED))]
    f22: Ipv4Addr,
    #[lbs(id(23), default(Ipv6Addr::UNSPECIFIED))]
    f23: Ipv6Addr,
    #[lbs(id(24), default(IpAddr::V4(Ipv4Addr::UNSPECIFIED)))]
    f24: IpAddr,
    #[lbs(id(25))]
    f25: Range<u64>,
    #[lbs(id(26))]
    f26: Vec<u64>,
    #[lbs(id(27))]
    f27: Rc<String>,
    #[lbs(id(28))]
    f28: Arc<String>,
    #[lbs(id(29), default(Arc::from("")))]
    f29: Arc<str>,
    #[lbs(id(30))]
    f30: Cow<'a, str>,
    #[lbs(id(31))]
    f31: Option<String>,
    #[lbs(id(32))]
    f32: Vec<String>,
    #[lbs(id(33))]
    f33: HashMap<String, u64>,
    #[lbs(id(34))]
    f34: BTreeMap<u64, String>,
    #[lbs(id(35))]
    f35: HashSet<String>,
    #[lbs(id(36))]
    f36: BTreeSet<u64>,
    #[lbs(id(37))]
    f37: chrono::DateTime<chrono::Utc>,
    #[lbs(id(38))]
    f38: smallvec::SmallVec<[i64; 4]>,
    #[lbs(id(39))]
    f39: StructTwo,
    #[lbs(id(40))]
    f40: EnumOne,
    #[lbs(id(41))]
    f41: IpNet,
    #[lbs(id(42))]
    f42: Uuid,
    #[lbs(id(43), default(OffsetDateTime::UNIX_EPOCH))]
    f43: OffsetDateTime,
    #[lbs(id(44), skip)]
    f44: bool,
}

// Field IDs are assigned implicitly, using their index
#[derive(LBSWrite, LBSRead, Default, PartialEq, Debug)]
struct StructTwo {
    #[lbs(id(0))]
    id: Uuid,
    #[lbs(id(1))]
    name: String,
    #[lbs(id(2))]
    en: Option<EnumOne>,
}

// Variant IDs are assigned implicitly, using their index
#[derive(LBSWrite, LBSRead, PartialEq, Debug, Default)]
enum EnumOne {
    #[default]
    #[lbs(id(0))]
    One,
    #[lbs(id(1))]
    Two,
    #[lbs(id(2))]
    Three(String),
    #[lbs(id(3))]
    Four(EnumTwo),
}

#[derive(LBSWrite, LBSRead, PartialEq, Debug)]
enum EnumTwo {
    #[lbs(id(0))]
    One,
    #[lbs(id(1))]
    Two,
}

#[test]
fn usage() {
    let mut original = StructOne {
        f0: 1,
        f1: 1,
        f2: 2,
        f3: 3,
        f4: 4,
        f5: 5,
        f6: 1,
        f7: -1,
        f8: -2,
        f9: -3,
        f10: 23,
        f11: 1,
        f12: 1.1,
        f13: -3.14,
        f14: (),
        f15: (1, String::from("1")),
        f16: (1, 2, 3),
        f17: true,
        f18: 'a',
        f19: String::from("test"),
        f20: Duration::from_millis(1000),
        f21: SystemTime::now(),
        f22: Ipv4Addr::new(192, 168, 1, 2),
        f23: Ipv6Addr::from_str("2001:0db8:85a3:0000:0000:8a2e:0370:7334").unwrap(),
        f24: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
        f25: Range { start: 0, end: 1 },
        f26: vec![1, 2, 3],
        f27: Rc::new(String::from("test_rc")),
        f28: Arc::new(String::from("test_arc")),
        f29: Arc::from("test_str_arc"),
        f30: Cow::Owned(String::from("test_cow")),
        f31: Some("str".to_string()),
        f32: Vec::new(),
        f33: HashMap::new(),
        f34: BTreeMap::new(),
        f35: HashSet::new(),
        f36: BTreeSet::new(),
        f37: chrono::Utc::now(),
        f38: smallvec::smallvec![0, 1],
        f39: StructTwo::default(),
        f40: EnumOne::Three(String::from("test_enum")),
        f41: IpNet::from_str("192.168.1.0/24").unwrap(),
        f42: Uuid::new_v4(),
        f43: OffsetDateTime::now_utc(),
        f44: true,
    };

    original.f33.insert(String::from("key1"), 1);
    original.f33.insert(String::from("key2"), 2);

    original.f34.insert(1, String::from("key1"));
    original.f34.insert(2, String::from("key2"));

    original.f35.insert(String::from("key1"));
    original.f35.insert(String::from("key2"));

    original.f36.insert(1);
    original.f36.insert(1);

    let mut buf = Vec::with_capacity(128);
    original.lbs_write(&mut buf).unwrap();

    let decoded = StructOne::lbs_read(&mut buf.as_slice()).unwrap();
    assert_eq!(decoded.f0, original.f0);
    assert_eq!(decoded.f1, original.f1);
    assert_eq!(decoded.f2, original.f2);
    assert_eq!(decoded.f3, original.f3);
    assert_eq!(decoded.f4, original.f4);
    assert_eq!(decoded.f5, original.f5);
    assert_eq!(decoded.f6, original.f6);
    assert_eq!(decoded.f7, original.f7);
    assert_eq!(decoded.f8, original.f8);
    assert_eq!(decoded.f9, original.f9);
    assert_eq!(decoded.f10, original.f10);
    assert_eq!(decoded.f11, original.f11);
    assert_eq!(decoded.f12, original.f12);
    assert_eq!(decoded.f13, original.f13);
    assert_eq!(decoded.f14, original.f14);
    assert_eq!(decoded.f15, original.f15);
    assert_eq!(decoded.f16, original.f16);
    assert_eq!(decoded.f17, original.f17);
    assert_eq!(decoded.f18, original.f18);
    assert_eq!(decoded.f19, original.f19);
    assert_eq!(decoded.f20, original.f20);
    assert_eq!(decoded.f21, original.f21);
    assert_eq!(decoded.f22, original.f22);
    assert_eq!(decoded.f23, original.f23);
    assert_eq!(decoded.f24, original.f24);
    assert_eq!(decoded.f25, original.f25);
    assert_eq!(decoded.f26, original.f26);
    assert_eq!(decoded.f27, original.f27);
    assert_eq!(decoded.f28, original.f28);
    assert_eq!(decoded.f29, original.f29);
    assert_eq!(decoded.f30, original.f30);
    assert_eq!(decoded.f31, original.f31);
    assert_eq!(decoded.f32, original.f32);
    assert_eq!(decoded.f33, original.f33);
    assert_eq!(decoded.f34, original.f34);
    assert_eq!(decoded.f35, original.f35);
    assert_eq!(decoded.f36, original.f36);
    assert_eq!(decoded.f37, original.f37);
    assert_eq!(decoded.f38, original.f38);
    assert_eq!(decoded.f39, original.f39);
    assert_eq!(decoded.f40, original.f40);
    assert_eq!(decoded.f41, original.f41);
    assert_eq!(decoded.f42, original.f42);
    assert_eq!(decoded.f43, original.f43);
    assert_eq!(decoded.f44, false);
}

#[derive(LBSWrite, LBSRead, PartialEq, Debug)]
struct MessageV1 {
    #[lbs(id(0))]
    f0: u64,
    #[lbs(id(1))]
    f1: Option<u64>,
}

#[derive(LBSWrite, LBSRead, PartialEq, Debug)]
struct MessageV2 {
    #[lbs(id(0))]
    f0: u64,
    #[lbs(id(1))]
    f1: Option<u64>,
    #[lbs(id(2))]
    f2: u64,
}

#[test]
fn required() {
    let msgv1 = MessageV1 { f0: 1, f1: None };

    let mut buf = Vec::with_capacity(128);
    msgv1.lbs_write(&mut buf).unwrap();

    if let Err(e) = MessageV2::lbs_read(&mut buf.as_slice()) {
        if let LBSError::WithField(id, inner) = e {
            if let LBSError::RequiredButMissing = inner.as_ref() {
                if id == 2 {
                    return;
                }
            }
        }
    }

    panic!("not an error");
}

#[derive(LBSWrite, LBSRead, PartialEq, Debug)]
struct OtherMessageV1 {
    #[lbs(id(0))]
    f0: u64,
    #[lbs(id(1))]
    f1: Option<u64>,
}

#[derive(LBSWrite, LBSRead, PartialEq, Debug)]
struct OtherMessageV2 {
    #[lbs(id(0))]
    f0: u64,
    #[lbs(id(1))]
    f1: Option<u64>,
    #[lbs(id(2), optional)]
    f2: u64,
}

#[test]
fn optional() {
    let msgv1 = OtherMessageV1 { f0: 1, f1: None };
    let mut buf = Vec::with_capacity(128);
    msgv1.lbs_write(&mut buf).unwrap();
    OtherMessageV2::lbs_read(&mut buf.as_slice()).unwrap();
}

Dependencies

~280–730KB
~18K SLoC