blob: 92b718a14ed542082edfdcda85edad5e461a1070 [file] [log] [blame] [view] [edit]
# IP types
In order to fully leverage Rust's type system, we use two traits - `Ip` and
`IpAddr` - to abstract over the versions of the IP protocol. The `Ip` trait
represents the protocol version itself, including details such as the protocol
version number, the protocol's loopback subnet, etc. The `IpAddr` trait
represents an actual IP address - `Ipv4Addr` or `Ipv6Addr`.
As much as possible, code which must be aware of IP version should be generic
over either the `Ip` or `IpAddr` traits. This allows common code to be only
written once, while still allowing protocol-specific logic when needed. It also
leverages the type system to provide a level of compile-time assurance that
would not be possible using runtime types like an enum with one variant for each
address type. Consider, for example, this function from the `device` module:
```
/// Get the IP address associated with this device.
pub fn get_ip_addr<A: IpAddr>(state: &mut StackState, device: DeviceAddr) -> Option<A>
```
Without trait bounds, this would either need to take an object at runtime
identifying which IP version was desired, which would lose type safety, or would
require two distinct functions, `get_ipv4_addr` and `get_ipv6_addr`, which would
result in a large amount of code duplication (if this pattern were used
throughout the codebase).
### Specialization
Sometimes, it is necessary to execute IP version-specific logic. In these cases,
it is necessary to have Rust run different code depending on the concrete type
that a function is instantiated with. We could imagine code along the lines of:
```
/// Get the IPv4 address associated with this Ethernet device.
pub fn get_ip_addr<A: IpAddr>(state: &mut StackState, device_id: u64) -> Option<A>;
pub fn get_ip_addr<Ipv4Addr>(state: &mut StackState, device_id: u64) -> Option<Ipv4Addr> {
get_device_state(state, device_id).ipv4_addr
}
pub fn get_ip_addr<Ipv6Addr>(state: &mut StackState, device_id: u64) -> Option<Ipv6Addr> {
get_device_state(state, device_id).ipv6_addr
}
```
This is a feature called "bare function specialization", and it unfortunately
doesn't yet exist in Rust. However, a function called "impl specialization" does
exist, and we can leverage it to accomplish something similar. While the details
of implementing this logic using impl specialization involve some annoying
boilerplate, we provide the `specialize_ip!` and `specialize_ip_addr!` macros to
make it easier. Using `specialize_ip_addr!`, the above hypothetical code can be
written today as:
```
/// Get the IPv4 address associated with this Ethernet device.
pub fn get_ip_addr<A: IpAddr>(state: &mut StackState, device_id: u64) -> Option<A> {
specialize_ip_addr!(
fn get_ip_addr(state: &EthernetDeviceState) -> Option<Self> {
Ipv4Addr => { state.ipv4_addr }
Ipv6Addr => { state.ipv6_addr }
}
);
A::get_ip_addr(get_device_state(state, device_id))
}
```
`get_ip_addr` is added as an associated function on the `IpAddr` trait, where
the implementation for `Ipv4Addr` is given in the first block, and the
implementation for `Ipv6Addr` is given in the second block. This allows us to
invoke it as `A::get_ip_addr`.
A few oddities to note: For reasons having to do with limitations of Rust
macros, the type which implements `IpAddr` must be written as `Self` in the
function definition. That's where the `Option<Self>` comes from. The body is
structured like a sort of type-level macro, where the branches are labeled by
the concrete types - `Ipv4Addr` or `Ipv6Addr` - that the function is
instantiated with. Finally, the blocks associated with these labels become the
function bodies, and so must return, in this case, `Option<Ipv4Addr>` or
`Option<Ipv6Addr>` respectively.