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§
- Network
Bytes - Represents an immutable view into a sequence of bytes which stands for different values packed together using network byte ordering.
- Network
Bytes Mut - Offers mutable access to a sequence of bytes which stands for different values packed together using network byte ordering.