bin 2.0.0 copy "bin: ^2.0.0" to clipboard
bin: ^2.0.0 copied to clipboard

Typed binary data structures for Dart (without code generation).

πŸ—‘οΈ Bin #

style: very good analysis Coverage Made in Minnesota License: MIT

Typed binary data structures in Dart (without code generation) built on the excellent binarize package.

import 'package:bin/bin.dart';

/// A phone number category.
enum PhoneKind { home, mobile, work }

final class AddressBook with Binnable {
  static final binType = obj(AddressBook.new);

  final owner = Bin.utf8();
  final emergencyContact = Bin.nullableObj(Contact.new);
  final contacts = Bin.custom(list(Contact.binType), <Contact>[]);
  final groups = Bin.custom(
    map(string.utf8(), list(list(uint32))),
    <String, List<List<int>>>{},
  );

  @override
  Iterable<Binned> get bins => [owner, emergencyContact, contacts, groups];
}

final class Contact with Binnable {
  static final binType = obj(Contact.new);

  // ...
}

void main() {
  final addressBook = AddressBook()
    ..owner.value = 'Bob'
    ..emergencyContact.value = (Contact()
      ..id.value = 7
      ..name.value = 'Alice'
      ..phoneNumbers.value = {PhoneKind.mobile: '1-234-5678'})
    ..contacts.value = [
      Contact()
        ..id.value = 7
        ..name.value = 'Alice'
        ..phoneNumbers.value = {
          PhoneKind.home: '1-234-5678',
          PhoneKind.mobile: '1-234-5678',
          PhoneKind.work: '1-234-5678',
        }
        ..addressLines.value = [
          '1000 Home Street',
          'Shore Town, TS 12345',
        ],
      Contact()
        ..id.value = 12
        ..name.value = 'Eve'
        ..phoneNumbers.value = {PhoneKind.mobile: '1-234-5678'},
    ]
    ..groups.value = {
      'family': [
        [7],
        [12],
      ],
      'work': [
        [12],
      ],
    };

  final bytes = encode(AddressBook.binType, addressBook);
  final decoded = decode(AddressBook.binType, bytes);
}

The above was taken from the complete example.

πŸ“¦ Installation #

dart pub add bin

πŸ“– Usage #

πŸ”Ÿ Binarize 101 #

Bin uses Binarize under the hood. A Binarize PayloadType<T> describes how a value is read and written.

final countType = uint32;
final nameType = string.utf8();
final kindType = enumeration(PhoneKind.values);

Payload types nest inside each other. Collection payloads accept other payloads:

final idsType = list(uint32);
final rowsType = list(list(uint32));
final lookupType = map(
  string.utf8(),
  list(uint32),
);

Common payloads include:

  • Numbers
    • int8, int16, int32, int64
    • uint8, uint16, uint32, uint64
    • float32, float64
  • Other primitives
    • boolean
    • string.utf8(), string.ascii(), string.latin1()
    • enumeration(MyEnum.values)
  • Composition
    • list(type)
    • map(keyType, valueType)
    • RawList(type, amount: n)
    • nil(type)
  • Bytes and typed data
    • Bytes(n)
    • uint8List(), byteData(), and other typed list payloads

Length-prefixed payloads use compact defaults, but most accept a lengthType when you need larger values:

final longNamesType = list(string.utf8(), lengthType: uint32);

πŸ”Œ Bin Additions #

Bin adds field, object, enum, union, and custom schema helpers on top of Binarize payloads.

Binnable Types

To make a type serializable, use the Binnable mixin on it.

final class Contact with Binnable {
  static final binType = obj(Contact.new);

  final id = Bin.uint32();
  final name = Bin.utf8();
  final phoneNumbers = Bin.custom(
    map(enumeration(PhoneKind.values), string.utf8()),
    <PhoneKind, String>{},
  );

  // Fields in serialization order.
  @override
  Iterable<Binned> get bins => [id, name, phoneNumbers];
}

Fields are implemented as Bin objects. Read and write field values through .value.

final contact = Contact()
  ..id.value = 7
  ..name.value = 'Alice'
  ..phoneNumbers.value = {PhoneKind.mobile: '1-234-5678'};

print(contact.name.value); // Alice

Warning

The order of bins is the binary schema. Changing the order changes the bytes.

Custom Payloads and Enums

Use Bin.custom to turn any Binarize payload into a field:

final count = Bin.custom(uint32, 0);
final tags = Bin.custom(list(string.utf8()), <String>[]);

For direct enum fields, use Bin.enumeration:

final kind = Bin.enumeration(PhoneKind.values, PhoneKind.home);

Nested Objects

Other Binnable objects work with obj:

static final binType = obj(Contact.new);

final emergencyContact = Bin.obj(Contact.new);
final contacts = Bin.custom(list(Contact.binType), <Contact>[]);
final pages = Bin.custom(list(list(Contact.binType)), <List<Contact>>[]);

Note

Bin needs a way to create a type when deserializing a byte stream, so by convention we pass in the parameter-less default constructor as MyClass.new. Public serializable models should expose this as static final binType = obj(MyClass.new).

Encoding and Decoding

Use encode and decode at the byte boundary with any Binarize PayloadType<T>. For concrete Binnable objects, use the model's binType.

final bytes = encode(Contact.binType, contact);
final decoded = decode(Contact.binType, bytes);

Because the boundary is payload-based, the same helpers also work for lists, maps, nullable values, primitives, and union roots:

final bytes = encode(ContactMethod.union, contactMethod);
final decoded = decode(ContactMethod.union, bytes);

For streaming or embedded formats, use read(ByteReader) and write(ByteWriter) directly.

Discriminated Unions

Discriminated unions declare a variants table on the sealed root:

sealed class ContactMethod with Binnable, Union {
  // by convention, base union types should declare a static field named `union`
  static final union = UnionType<ContactMethod, int>(
    uint8,
    {
      1: EmailContactMethod.new,
      2: PhoneContactMethod.new,
    },
  );
}

final class EmailContactMethod extends ContactMethod {
  final email = Bin.utf8();

  @override
  Iterable<Binned> get bins => [email];
}

Each derived type is entered into the map as an identifier that's associated with its class factory.

Fields inside Binnable objects simply reference the base union utility:

final preferredContactMethod = ContactMethod.union(EmailContactMethod());
final contactMethods = Bin.custom(list(ContactMethod.union), <ContactMethod>[]);

Custom Binned Fields

Implement Binned when a schema needs custom read/write behavior instead of one stored value.

final class MagicHeader implements Binned {
  const MagicHeader(this.expected);

  final int expected;

  @override
  void read(ByteReader reader) {
    final actual = uint32.get(reader);

    if (actual != expected) {
      throw FormatException('Invalid magic header: $actual');
    }
  }

  @override
  void write(ByteWriter writer) {
    uint32.set(writer, expected);
  }
}

final class Packet with Binnable {
  static const magic = MagicHeader(0x5041434b);

  final version = Bin.uint8();
  final payload = Bin.utf8();

  @override
  Iterable<Binned> get bins => [magic, version, payload];
}

Custom Binned fields are useful for magic headers, padding, checksums, alignment, version gates, and other schema steps that need direct stream access.

0
likes
155
points
33
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Typed binary data structures for Dart (without code generation).

Repository (GitHub)
View/report issues

Topics

#binary #serialization

License

MIT (license)

Dependencies

binarize

More

Packages that depend on bin