In this post we will see how to use enums in Rust. In C, an enum is a user-defined data type that consists of a set of named integer constants. These constants are assigned sequential values by default, starting with zero, but can be explicitly set to any value. C enums do not have any associated data or behavior. In Rust, enums are more powerful and flexible than C enums. Rust enums can hold arbitrary data types and can have methods and behavior associated with them.

As usual we will start with a C program which uses enum and then we will convert to rust program.


#include <stdint.h>
#include <stdio.h>

enum ip_version {
    IP_VERSION_V4,
    IP_VERSION_V6,
};

struct ip_address {
    enum ip_version version;
    union {
        uint8_t v4[4];
        uint8_t v6[16];
    } address;
};

void print_ip_address(struct ip_address *ip) {
    switch (ip->version) {
        case IP_VERSION_V4:
            printf("IPv4: %d.%d.%d.%d\n",
                ip->address.v4[0], ip->address.v4[1], ip->address.v4[2], ip->address.v4[3]);
            break;
        case IP_VERSION_V6: 
            printf("IPv6: ");
            for (int i=0; i < sizeof(ip->address.v6); i += 2) {
                printf("%02x%02x", ip->address.v6[i], ip->address.v6[i+1]);
                if (i < sizeof(ip->address.v6)-2) {
                    printf(":");
                }
            }
            printf("\n");
            break;
        default:
            printf("Invalid type\n");
            break;
    }
}

void main() {
    struct ip_address v4 = {
        .version = IP_VERSION_V4,
        .address.v4 = {0xc0, 0x30, 0x0, 0x2}
    };
    struct ip_address v6 = {
        .version = IP_VERSION_V6,
        .address.v6 = {0}
    };
    for (int i=0; i < sizeof(v6.address.v6); i++) 
        v6.address.v6[i] = i;

    print_ip_address(&v4);
    print_ip_address(&v6);   
}

C like Rust Program

Lets convert the above C program into rust as it is.
The following rust program uses only basic enum similar to C hence the program looks similar to rust.

enum IpVersion {
    V4,
    V6,
}
union IPAddressCombined {
    v4: [u8; 4],
    v6: [u8; 16],
}

struct IpAddress {
    version: IpVersion,
    address: IPAddressCombined,
}

fn print_ip_address(ip: &IpAddress) {
    match ip.version {
        IpVersion::V4 => unsafe {
            println!("IPv4: {}.{}.{}.{}", ip.address.v4[0], ip.address.v4[1], ip.address.v4[2], ip.address.v4[3])
        },
        IpVersion::V6 => unsafe {
            print!("IPv6: ");
            for i in (1..15).step_by(2) {
                print!("{:02x}{:02x}", ip.address.v6[i], ip.address.v6[i+1]);
                if i < 14 {
                    print!(":")
                }
            }
            println!();
        },
    }
}
fn main() {
    let v4 = IpAddress {
        version: IpVersion::V4,
        address: IPAddressCombined {
            v4: [0xc0, 0x30, 0x0, 0x2],
        }
    };
    let mut v6 = IpAddress {
        version: IpVersion::V6,
        address: IPAddressCombined {
            v6: [0;16],
        }
    };
    for i in 0..16{
        unsafe {v6.address.v6[i] = i as u8;}
    }
    print_ip_address(&v4);
    print_ip_address(&v6);
}

Rusty Program

Lets make this program simpler using Rust enum and match.
Rust makes this program much more readable because of the enum.


enum IpAddress {
    IPv4([u8; 4]),
    IPv6([u8; 16]),
}

impl std::fmt::Display for IpAddress {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            IpAddress::IPv4(v) => {
                write!(f, "IPv4: ").unwrap();
                write!(f, "{}.{}.{}.{}", v[0], v[1], v[2], v[3])
            }
            IpAddress::IPv6(v) => {
                write!(f, "IPv6: ").unwrap();
                for i in 0..15 {
                    write!(f, "{:02x}:", v[i]).unwrap()
                }
                write!(f, "{:02x}", v[15])
            }
        }
    }
}

fn main() {
    let v4 = IpAddress::IPv4([0xC0, 0xb, 0x0, 0xa]);
    let v6 = IpAddress::IPv6([0;16]);
    println!("{v4}");
    println!("{v6}");
}

If we compare the C and Rust programs, the first thing we notice is Rust programs does not need extra struct and union to hold the address – the enum itself holds the data. Further more rust does not need “default” switch case for this enum since the compiler ensures the the data structure can be initialized with only V4 or V6.

Enum with different data types

In C enum is just integer data type. In rust, enum can take different data types which makes reduced code. For example if you want to keep track if ETA for flights and if ETA is not available want to show the reason for delay. The following code shows how to define a enum for that purpose using both String and custom structure inside a enum.


enum FlightInfo {
    Cancelled, // Just like C
    Delayed(String), // We embed data inside enum
    Time{hour: u8, min: u8}, // The embedded data can names too
}

fn display_flight_info(flight_info: &FlightInfo) {
    match flight_info {
        FlightInfo::Cancelled => println!("Cancelled"),
        FlightInfo::Delayed(reason) => println!("Delayed because {reason}"),
        FlightInfo::Time { hour, min } => println!("Estimated Arrival Time {hour}:{min}"),
    }
}

fn main() {
    let flight_164 = FlightInfo::Cancelled;
    let flight_165 = FlightInfo::Time{
        hour: 1,
        min: 10,
    };
    let flight_166 = FlightInfo::Delayed("Weather".to_string());
    
    display_flight_info(&flight_165);
    display_flight_info(&flight_166);
    display_flight_info(&flight_164);
}

In the next article we will see how two powerful enums Result and Option make rust fun and safe to write programs.

This article can be viewed as video here

Categorized in:

Tagged in: