bitsong

Crates.iobitsong
lib.rsbitsong
version0.1.3
created_at2025-12-17 23:28:13.592909+00
updated_at2025-12-17 23:44:30.19918+00
descriptionFast `#[no_std]` macro-based serialization/deserialization.
homepage
repositoryhttps://codeberg.org/Serena/bitsong/
max_upload_size
id1991308
size21,254
Serena Lynas (SerenaLynas)

documentation

https://docs.rs/bitsong/latest/bitsong/

README

Bitsong

Fast #[no_std] macro-based serialization/deserialization into a minimal binary format. Makes no memory allocations, doesn't use tagging (except for enum discriminants), and supports enums and enums with tuple variants. Doesn't support strings (but you can use a buffer) or struct enum variants.

Getting started

  • Use #[derive(SongSize, FromSong, ToSong)] on your struct/enum (or implement the traits manually)
  • Check how many bytes will be written with my_struct.song_size()
  • Use my_struct.to_song(&mut buf) to write my_struct into a buffer buf
  • Use MyStruct::from_song(&buf) to read type MyStruct from a buffer buf

How it works

The derive macros generate the serializers and deserializers. The save format is dead simple; for structs, it is every field in the order they appear (without any tagging bytes), and for enums there is one discriminant field and then any associated enum data. It is very similar to what you would get with mem::transmute, except without any padding bytes and with the additional guarantee that it'll always work cross-platform.

This code has been used by Case Rocket Team and seems to work well enough, though some of the macro implementation is a bit messy. We use it for reading and writing low bandwidth LoRa radio packets.

Example code

use crate::*;

#[derive(Clone, Copy, Debug, PartialEq, Eq, FromSong, ToSong, SongSize)]
#[repr(u8)]
enum Color {
    Unknown = 0x00,
    Red = 0x01,
    Green = 0x02,
    Blue = 0x03
}

#[derive(Clone, Debug, PartialEq, Eq, FromSong, ToSong, SongSize)]
struct Cat {
    fluffy_paws: bool
}

#[derive(Clone, Debug, PartialEq, Eq, FromSong, ToSong, SongSize)]
#[song(discriminant(PetType = u8))]
enum Pet {
    Dog,
    Cat(Cat),
    Fish(Color)
}

#[derive(Clone, Debug, PartialEq, Eq, FromSong, ToSong, SongSize)]
struct Person {
    name: [u8; 16],
    age: u8,
    favorite_color: Color,
    pet: Option<Pet>
}

fn new_joe() -> Person {
    let mut name = [0; 16];
    name[0..3].copy_from_slice(b"Joe");
    Person {
        name,
        age: 21,
        favorite_color: Color::Green,
        pet: Some(Pet::Cat(Cat {
            fluffy_paws: true
        }))
    }
}

const JOE_SONG: [u8; 21] = [74, 111, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 2, 1, 1, 1];

#[test]
fn serialize() {
    let mut buf = [0; 21];
    new_joe().to_song(&mut buf).unwrap();
    assert_eq!(buf, JOE_SONG);
}

#[test]
fn deserialize() {
    let joe = Person::from_song(&JOE_SONG).unwrap();
    assert_eq!(joe, new_joe())
}

Running no_panic tests

cargo test --profile release --features no_panic
Commit count: 0

cargo fmt