bytes = <<104, 105>>
IO.inspect(bytes)
IO.inspect(bytes == "hi")
"hi"
true
A bitstring is a sequence of bits, written between
<< and >>. Three nested kinds share this syntax:
Each comma-separated value is a segment. By default a segment is one byte.
bytes = <<104, 105>>
IO.inspect(bytes)
IO.inspect(bytes == "hi")
"hi"
true
Four bits are a bitstring but not a binary. Two bytes that aren't valid UTF-8 are a binary but not a string.
nibble = <<5::4>>
IO.inspect(nibble)
IO.inspect(is_bitstring(nibble))
IO.inspect(is_binary(nibble))
junk = <<0xFF, 0xFE>>
IO.inspect(is_binary(junk))
IO.inspect(String.valid?(junk))
IO.inspect(String.valid?("héllo"))
<<5::size(4)>>
true
false
true
false
true
?a is the integer codepoint of a character, handy
when you build or match bytes by hand.
IO.inspect(?a)
IO.inspect(?é)
IO.inspect(<<?o, ?k>>)
97
233
"ok"
IO.inspect prints a binary as a string when it is
printable, and as <<...>> bytes otherwise. For
readable hex, pass base: :hex or use Base.
data = <<0, 1, 127, 128, 255>>
IO.inspect(data)
IO.inspect(data, base: :hex)
IO.puts(Base.encode16(data))
IO.inspect(Base.decode16!("CAFE"))
<<0, 1, 127, 128, 255>>
<<0x0, 0x1, 0x7F, 0x80, 0xFF>>
00017F80FF
<<202, 254>>
:: after a value says how to encode it: a size in
bits (::16, or ::size(n) when it's a variable),
a type, an endianness. Multi-byte integers are
big-endian unless you say otherwise. base: :binary
shows the bits; 0x and 0b write integers in hex
and binary.
IO.inspect(<<1::16>>)
IO.inspect(<<1::16-little>>)
IO.inspect(<<0b101::3, 0b00001::5>>, base: :binary)
width = 12
IO.inspect(<<1::size(width)>>)
IO.inspect(<<1.5::float-32>>, base: :hex)
IO.inspect(<<"é"::binary, ?é::utf8>>)
<<0, 1>>
<<1, 0>>
<<0b10100001>>
<<0, 1::size(4)>>
<<0x3F, 0xC0, 0x0, 0x0>>
"éé"
bit_size/1 counts bits, byte_size/1 counts whole
bytes, rounding up.
IO.inspect(bit_size(<<1::12>>))
IO.inspect(byte_size(<<1::12>>))
12
2
A value too big for its segment keeps only its low
bits. <<300>> is a single byte, 300 - 256:
<<b>> = <<300>>
IO.inspect(b)
44
Put <<>> on the left of = and it becomes a
pattern: the match binds its variables, or raises
MatchError if the bits don't fit. The modifiers
decide how bits are read back, so the same four
bytes mean different things.
four = <<0, 0, 1, 0>>
<<n::32>> = four
IO.inspect(n)
<<n_le::32-little>> = four
IO.inspect(n_le)
<<hi::16, lo::16>> = four
IO.puts("hi=#{hi} lo=#{lo}")
<<f::float-32>> = <<1.5::float-32>>
IO.inspect(f)
256
65536
hi=0 lo=256
1.5
Bytes are unsigned unless you ask for signed,
which reads the top bit as a minus sign.
<<u::8>> = <<255>>
<<i::8-signed>> = <<255>>
IO.puts("unsigned #{u}, signed #{i}")
unsigned 255, signed -1
::binary takes "the rest". With -size(n) it takes
exactly n bytes, and ::bits takes any number of
bits.
<<tag::binary-size(3), body::binary>> = "GET /index.html"
IO.inspect(tag)
IO.inspect(body)
<<flag::1, rest::bits>> = <<0b1010_0000>>
IO.inspect(flag)
IO.inspect(rest)
"GET"
" /index.html"
1
<<32::size(7)>>
Here is why this matters. A common network framing is a 2-byte length followed by that many bytes of payload. TCP hands you a stream, so a read can hold several messages and the start of the next one.
A size can use a variable bound earlier in the same
pattern, so len read from the first two bytes
tells binary-size(len) how much to take.
stream = <<5::16, "hello", 12::16, "binary world", 9::16, "par">>
IO.inspect(stream, base: :hex)
<<len::16, msg1::binary-size(len), stream::binary>> = stream
IO.inspect(msg1)
<<len::16, msg2::binary-size(len), stream::binary>> = stream
IO.inspect(msg2)
<<0x0, 0x5, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x0, 0xC, 0x62, 0x69, 0x6E, 0x61,
0x72, 0x79, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x0, 0x9, 0x70, 0x61, 0x72>>
"hello"
"binary world"
The third message announces 9 bytes but only 3 have
arrived. case tries each pattern in turn and runs
the first that matches. _ matches anything and
throws it away; a bare variable like partial
matches anything and keeps it, here the leftover
bytes to prepend to the next read.
case stream do
<<len::16, msg::binary-size(len), _::binary>> ->
IO.puts("complete: #{msg}")
partial ->
IO.puts("waiting, #{byte_size(partial)} bytes buffered")
end
waiting, 5 bytes buffered
Without bit syntax this is index arithmetic:
len = (buf[0] << 8) | buf[1], then
buf[2 .. 2 + len], then a bounds check you
hopefully remembered. The pattern is the format
spec, and a too-short buffer simply fails to match
instead of reading garbage.
Not every field is a whole byte. An IPv4 header packs the version and header length into one byte, and three flags plus a 13-bit offset into two. Let's build a real 20-byte header for a packet from 192.168.1.10 to 8.8.8.8.
header =
<<4::4, 5::4, 0::8, 60::16, 0x1C46::16, 0b010::3, 0::13, 64::8, 6::8, 0x4CB4::16, 192, 168, 1,
10, 8, 8, 8, 8>>
IO.inspect(byte_size(header))
IO.puts(Base.encode16(header))
20
4500003C1C46400040064CB4C0A8010A08080808
Parsing it is one match that reads like the diagram
in RFC 791. Names starting with _ mark fields we
skip.
<<version::4, ihl::4, _tos::8, total_length::16, _id::16, _reserved::1, dont_fragment::1,
more_fragments::1, _frag_offset::13, ttl::8, protocol::8, _checksum::16, s1, s2, s3, s4, d1, d2,
d3, d4>> = header
IO.puts("IPv#{version}, header #{ihl * 4} bytes, total #{total_length}")
IO.puts("DF=#{dont_fragment} MF=#{more_fragments} TTL=#{ttl} protocol=#{protocol}")
IO.puts("#{s1}.#{s2}.#{s3}.#{s4} -> #{d1}.#{d2}.#{d3}.#{d4}")
IPv4, header 20 bytes, total 60
DF=1 MF=0 TTL=64 protocol=6
192.168.1.10 -> 8.8.8.8
A literal in a pattern must match exactly, so you can also check while you parse: this only matches IPv4 TCP packets.
<<4::4, _::68, 6::8, _::binary>> = header
IO.puts("it's TCP over IPv4")
it's TCP over IPv4
String
module, which understands graphemes.byte_size
and bit syntax, not String functions.Pitfall: a value that doesn't fit its segment is silently truncated to its low bits, with no error:
<<300>>above came back as 44.
Pitfall: in a pattern, an unsized
::binaryor::bitsmust be the last segment.<<a::binary, "!">> = "hi!"can't tell whereaends, so it doesn't compile. Give it a size, or match from the other end.
Pitfall: integers are big-endian by default. Formats from x86-land (BMP, WAV, many file headers) are little-endian; read them without
-littleand you get numbers like65536instead of256, as in the match above.
Pitfall:
byte_size/1rounds up a bitstring that isn't a whole number of bytes, so a 12-bit value hasbyte_size2. Usebit_size/1when the difference matters.