| # 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. |