View Source Bitcraft.BitBlock behaviour (Bitcraft v0.1.1)
Defines a bit-block.
A bit-block is used to map a bitstring into an Elixir struct.
The definition of the bit-block is possible through defblock/3
.
defblock/3
is typically used to decode bitstring from a bit stream,
usually a binary protocol (e.g.: TCP/IP), into Elixir structs and
vice-versa (encoding Elixir structs into a bitstring).
example
Example
defmodule MyBlock do
import Bitcraft.BitBlock
defblock "my-static-block" do
segment :h, 5, type: :binary
segment :s1, 4, default: 1
segment :s2, 8, default: 1, sign: :signed
segment :t, 3, type: :binary
end
end
The segment
macro defines a segment in the bit-block with given
name and size. Bit-blocks are regular structs and can be created
and manipulated directly using Elixir's struct API:
iex> block = %MyBlock{h: "begin", s1: 3, s2: -3, t: "end"}
iex> %{block | h: "hello"}
By default, a bit-block will automatically generate the implementation
for the callbacks encode/1
and decode/3
. You can then encode
the struct into a bitstring and then decode a bitstring into a Elixir
struct, like so:
iex> bits = MyBlock.encode(block)
iex> data = MyBlock.decode(bits)
What we defined previously it is a static block, means a fixed size always.
This is the easiest scenario because we don't need to provide any additional
logic to the the encode/1
and decode/3
functions. For that reason,
in the example above, we call decode
function only with the input
bitstring, the other arguments are not needed since they are ment to
resolve the size for dynamic segments.
dynamic-segments
Dynamic Segments
There are other scenarios where the block of bits is dynamic, means
the size of the block is variable and depends on other segment values
to calculate the size, this makes it more complicated to decode it.
For those variable blocks, we can define dynamic segments using the
segment/3
API:
segment :var, :dynamic, type: :bits
As you can see, for the size argument we are passing :dynamic
atom.
In this way, the segment is marked as dynamic and its size is resolved
later during the decoding process.
The following is a more elaborate example of block. We define an IPv4 datagram which has a static and a dynamic part. The dynamic part is basically the options and the data. The block can be defined as:
defmodule IpDatagram do
@moduledoc false
import Bitcraft.BitBlock
defblock "IP-datagram" do
segment :vsn, 4
segment :hlen, 4
segment :srvc_type, 8
segment :tot_len, 16
segment :id, 16
segment :flags, 3
segment :frag_off, 13
segment :ttl, 8
segment :proto, 8
segment :hdr_chksum, 16, type: :bits
segment :src_ip, 32, type: :bits
segment :dst_ip, 32, type: :bits
segment :opts, :dynamic, type: :bits
segment :data, :dynamic, type: :bits
end
# Size resolver for dynamic segments invoked during the decoding
def calc_size(%__MODULE__{hlen: hlen}, :opts, dgram_s)
when hlen >= 5 and 4 * hlen <= dgram_s do
opts_s = 4 * (hlen - 5)
{opts_s * 8, dgram_s}
end
def calc_size(%__MODULE__{leftover: leftover}, :data, dgram_s) do
data_s = :erlang.bit_size(leftover)
{data_s, dgram_s}
end
end
Here, the segment corresponding to the :opts
segment has a type modifier,
specifying that :opts
is to bind to a bitstring (or binary). All other
segments have the default type equal to unsigned integer.
An IP datagram header is of variable length. This length is measured in the
number of 32-bit words and is given in the segment corresponding to :hlen
.
The minimum value of :hlen
is 5. It is the segment corresponding to
:opts
that is variable, so if :hlen
is equal to 5, :opts
becomes
an empty binary. Finally, the tail segment :data
bind to bitstring.
The decoding of the datagram fails if one of the following occurs:
- The first 4-bits segment of datagram is not equal to 4.
:hlen
is less than 5.- The size of the datagram is less than
4*hlen
.
Since this block has dynamic segments, we can now use the other decode arguments to resolve the size for them during the decoding process:
IpDatagram.decode(bits, :erlang.bit_size(bits), &IpDatagram.calc_size/3)
Where:
- The first argument is the input IPv4 datagram (bitstring).
- The second argument is is the accumulator to the callback function (third argument), in this case is the total number of bits in the datagram.
- And the third argument is the function callback or dynamic size resolver that will be invoked by the decoder for each dynamic segment. The callback functions receives the data struct with the current decoded segments, the segment name (to be pattern-matched and resolve its size), and the accumulator that can be used to pass metadata during the dynamic segments evaluation.
reflection
Reflection
Any bit-block module will generate the __bit_block__
function that can be
used for runtime introspection of the bit-block:
__bit_block__(:name)
- Returns the name or alias as given todefblock/3
.__bit_block__(:segments)
- Returns a list of all segments names.__schema__(:segment_info, segment)
- Returns a map with the segment info.
working-with-typespecs
Working with typespecs
By default, the typespec t/0
is generated but in the simplest form:
@type t :: %__MODULE__{}
If you want to provide a more accurate typespec for you block adding the
typespecs for each of the segments on it, you can set the option :typespec
to false
when defining the block, like so:
defblock "my-block", typespec: false do
...
end
Link to this section Summary
Types
Basic Segment types
Block's segment type definition
Resolver function for the size of dynamic segments.
Segment type
Bitblock definition
Callbacks
Decodes the given bitstring into the corresponding data type.
Encodes the given data type into a bitstring.
Functions
This is a helper function used internally for building a block segment.
Same as segment/3
, but automatically generates a dynamic
segment with the type Bitcraft.BitBlock.Array.t()
.
This is a helper function used internally for building the encoding expressions.
Internal helper for decoding the block segments.
Defines a bit-block struct with a name and segment definitions.
Internal helper for encoding the block segments.
Defines a segment on the block with a given name
and size
.
Link to this section Types
@type base_seg_type() ::
:integer
| :float
| :bitstring
| :bits
| :binary
| :bytes
| :utf8
| :utf16
| :utf32
Basic Segment types
@type block_segment() :: {:block_segment, name :: atom(), size :: integer() | :dynamic | nil, type :: segment_type(), sign :: atom(), endian :: atom(), default :: term()}
Block's segment type definition
@type dynamic_size_resolver() :: (t(), seg_name :: atom(), acc :: term() -> {size :: non_neg_integer(), acc :: term()})
Resolver function for the size of dynamic segments.
@type segment_type() :: base_seg_type() | Bitcraft.BitBlock.Array.t()
Segment type
Bitblock definition
Link to this section Callbacks
@callback decode(input :: bitstring(), acc :: term(), dynamic_size_resolver()) :: t()
Decodes the given bitstring into the corresponding data type.
example
Example
iex> block = %MyBlock{seg1: 1, seg: 2}
iex> bits = MyBlock.encode(block)
iex> MyBlock.decode(bits)
Encodes the given data type into a bitstring.
example
Example
iex> block = %MyBlock{seg1: 1, seg: 2}
iex> MyBlock.encode(block)
Link to this section Functions
@spec __segment__(module(), atom(), non_neg_integer(), Keyword.t()) :: :ok
This is a helper function used internally for building a block segment.
Same as segment/3
, but automatically generates a dynamic
segment with the type Bitcraft.BitBlock.Array.t()
.
The size of the array-type segment in bits has to be calculated
dynamically during the decoding, and the length of the array will
be segment_size/element_size
. This process is performs automatically
during the decoding. hence, it is important to set the right
element_size
and also implement properly the callback to calculate
the segment size. See Bitcraft.BitBlock.dynamic_size_resolver()
.
options
Options
Options are the same as segment/3
, and additionally:
:element_size
- The size in bits of each array element. Defaults to8
.
NOTE: The :type
is the same as segment/3
BUT it applies to the
array element.
@spec build_encoding_exprs([block_segment()], String.t(), String.t()) :: {bin_expr_ast :: term(), map_expr_ast :: term()}
This is a helper function used internally for building the encoding expressions.
@spec decode_segments([block_segment()], map(), term(), dynamic_size_resolver()) :: map()
Internal helper for decoding the block segments.
Defines a bit-block struct with a name and segment definitions.
@spec encode_segments([block_segment()], map()) :: bitstring()
Internal helper for encoding the block segments.
Defines a segment on the block with a given name
and size
.
See Kernel.SpecialForms.<<>>/1
for more information about the
segment types, size, unit, and so on.
options
Options
:type
- Defines the segment data type the set of bits will be mapped to. SeeKernel.SpecialForms.<<>>/1
for more information about the segment data types. Defaults to:integer
.:sign
- Applies only to integers and defines whether the integer is:signed
or:unsigned
. Defaults to:unsigned
.:endian
- Applies toutf32
,utf16
,float
,integer
. Defines the endianness,:big
or:little
. Defaults to:big
.:default
- Sets the default value on the block and the struct. The default value is calculated at compilation time, so don't use expressions for generating values dynamically as they would then be the same for all records. Defaults tonil
.