Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

erlbits — Erlang bit syntax for C++, via C++26 reflection

Erlang lets you describe and pattern-match binary layouts inline:

pack(Version, Type, Len, Body) ->
    <<Version:4, Type:4, Len:16, Body/binary>>.

unpack(Bin) ->
    <<Version:4, Type:4, Len:16, Body/binary>> = Bin,
    {Version, Type, Len, Body}.

There's no C++ syntax that can literally look like <<...>>, but C++26's static reflection (P2996) plus structured annotations give us the next best thing: annotate an ordinary struct's fields with their bit widths, and get pack/unpack for free, with zero per-type boilerplate and zero macros.

struct Packet {
    std::uint8_t  version [[=bits{4}]];
    std::uint8_t  type    [[=bits{4}]];
    std::uint16_t length  [[=bits{16}]];
    std::vector<std::byte> body [[=rest]];
};

auto bytes  = pack(p);              // <<Version:4, Type:4, Len:16, Body/binary>>
Packet p2   = unpack<Packet>(bytes); // the matching/destructuring side

The struct is the bit-syntax pattern. Reflection walks its members at compile time, reads off each one's annotations, and generates the packing code — the same role nonstatic_data_members_of + template for play in most other "derive a trait from a struct's shape" reflection demos.

Status of what's in this folder — please read

C++26 reflection is real (it's a merged part of the C++26 working draft), but as of today no shipping, mainstream compiler implements it end-to-end, particularly the annotation feature ([[=expr]]) that this library leans on to attach bits{N} etc. to a member. So this project is split into two honest halves:

  1. bitsyntax::detail::bit_writer / bit_reader (inside erlbits.hpp) — the actual bit-packing engine: MSB-first packing (Erlang's default), arbitrary bit widths that cross byte boundaries, little-endian byte-aligned words, byte-aligned "consume the rest" reads, and match_error thrown on a truncated buffer. This part has no reflection dependency, and it is compiled and unit-tested — see bitcore_test.cpp, which builds cleanly with g++ -std=c++20 and passes all assertions:

    $ g++ -std=c++20 -O2 -Wall -Wextra -o bitcore_test bitcore_test.cpp && ./bitcore_test
    a3 12 34 68 69
    test 1 OK: version=10 type=3 len=0x1234 rest_len=2
    test 2 OK (unaligned bit widths)
    test 3 OK (little-endian)
    test 4 OK (badmatch on short buffer)
    ALL TESTS PASSED
    
  2. pack<T>() / unpack<T>() in erlbits.hpp, and all of demo.cpp — the reflection-driven layer that turns annotations into calls into the engine above. This is written against P2996's publicly documented API (^^T, std::meta::info, nonstatic_data_members_of, annotations_of/extract, [: :] splicing, template for), but that API is still moving between proposal revisions, so treat this half as a design sketch, not portable code. The algorithm is straightforward (iterate members, dispatch on which annotation is present, delegate to the tested engine) and shouldn't need to change even if exact spelling of the reflection calls does.

erlbits.hpp guards the reflective half behind #if ERLBITS_REFLECTION_AVAILABLE, so the header parses and the tested half still works on ordinary compilers even though the whole file targets C++26.

Building the reflective half, once you have a suitable compiler

The closest thing to a working P2996 implementation right now is Bloomberg's experimental fork of Clang: https://github.com/bloomberg/clang-p2996. Once built:

clang++ -std=c++26 -freflection -stdlib=libc++ \
    -DERLBITS_HAVE_REFLECTION -o demo demo.cpp

(Exact flags depend on how that fork exposes reflection/annotations at the time you're reading this — check its README, since the flag names have changed between revisions of the fork.)

Field annotation reference

Erlang segment syntax erlbits annotation Notes
Field:Size [[=bits{Size}]] plain unsigned, big-endian, Size bits
Field:Size/unit:U [[=bits{Size}, =unit{U}]] effective width is Size * U
Field:Size/little [[=bits{Size}, =little]] default is /big if omitted, same as Erlang
Field:Size/signed [[=bits{Size}, =is_signed]] default is unsigned, same as Erlang
Rest/binary (tail segment) [[=rest]] must be the last member; consumes remaining bytes
<<16#89504E47:32, ...>> [[=bits{32}, =expect<T>{0x89504E47}]] throws match_error (a badmatch) if it doesn't match

What isn't modeled (yet)

  • Erlang's bare _:N (match-and-discard) segment — in C++ every struct member needs a name, so the closest analogue is just adding a normal named field and ignoring it; there's no dedicated "unnamed skip" annotation here.
  • Float segments (Field:32/float) — would be a small addition (reinterpret the read bits via std::bit_cast), left out to keep the sketch focused.
  • Nested/variable-length-prefixed segments where one field's Size is read from an earlier field (e.g. <<Len:16, Body:Len/binary>>) — the current bits{N} annotation takes a compile-time constant; supporting a runtime-computed size from a previous field is a natural extension but needs a bit more plumbing (the annotation would need to name a sibling member rather than hold a literal), so it's noted here rather than built.

Files

  • erlbits.hpp — the library (tested bit engine + reflective sketch)
  • bitcore_test.cpp — standalone, currently-compilable unit tests for the bit engine
  • demo.cpp — Erlang-equivalent examples using the reflective pack/unpack

About

proposal for an erlang bit syntax for c++ as generated by claude code

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages