Module bytes

Module bytes 

Source
Expand description

Defines traits which allow byte slices to be interpreted as sequences of bytes that stand for different values packed together using network byte ordering (such as network packets).

The main use of these traits is reading and writing numerical values at a given offset in the underlying slice. Why are they needed? Given a byte slice, there are two approaches to reading/writing packet data that come to mind:

(1) Have structs which represent the potential contents of each packet type, unsafely cast the bytes slice to a struct pointer/reference (after doing the required checks), and then use the newly obtained pointer/reference to access the data.

(2) Access fields by reading bytes at the appropriate offset from the original slice.

The first solution looks more appealing at first, but it requires some unsafe code. Moreover, de-referencing unaligned pointers or references is considered undefined behaviour in Rust, and it’s not clear whether this undermines the approach or not. Until any further developments, the second option is used, based on the NetworkBytes implementation.

What’s with the T: Deref<Target = [u8]>? Is there really a need to be that generic? Not really. The logic in this crate currently expects to work with byte slices (&[u8] and &mut [u8]), but there’s a significant inconvenience. Consider NetworkBytes is defined as:

struct NetworkBytes<'a> {
    bytes: &'a [u8],
}

This is perfectly fine for reading values from immutable slices, but what about writing values? Implementing methods such as fn write_something(&mut self), is not really possible, because even with a mutable reference to self, self.bytes is still an immutable slice. On the other hand, NetworkBytes can be defined as:

struct NetworkBytes<'a> {
    bytes: &'a mut [u8],
}

This allows both reads and writes, but requires a mutable reference at all times (and it looks weird to use one for immutable operations). This is where one interesting feature of Rust comes in handy; given a type Something<T>, it’s possible to implement different features depending on trait bounds on T. For NetworkBytes, if T implements Deref<Target = [u8]> (which &[u8] does), read operations are possible to define. If T implements DerefMut<Target = [u8]>, write operations are also a possibility. Since DerefMut<Target = [u8]> implies Deref<Target = [u8]>, NetworkBytes<&mut [u8]> implements both read and write operations.

This can theoretically lead to code bloat when using both &[u8] and &mut [u8] (as opposed to just &mut [u8]), but most calls should be inlined anyway, so it probably doesn’t matter in the end. NetworkBytes itself implements Deref (and DerefMut when T: DerefMut), so this line of reasoning can be extended to structs which represent different kinds of protocol data units (such as IPv4 packets, Ethernet frames, etc.).

Finally, why Deref and not something like AsRef? The answer is Deref coercion, which in this case means that a NetworkBytes value will automatically coerce to &[u8] (or &mut [u8]), without having to go through an explicit as_ref() call, which makes the code easier to work with.

Method names have the unchecked suffix as a reminder they do not check whether the read/write goes beyond the boundaries of a slice. Callers must take the necessary precautions to avoid panics.

Traits§

NetworkBytes
Represents an immutable view into a sequence of bytes which stands for different values packed together using network byte ordering.
NetworkBytesMut
Offers mutable access to a sequence of bytes which stands for different values packed together using network byte ordering.