Предлагаю вашему вниманию перевод статьи "Working with C unions in Rust FFI" за авторством Herman J. Radtke III.

Примечание: Эта статья предполагает, что читатель знаком с Rust FFI, порядком байтов (endianess) и ioctl.


При создании биндингов к коду на С мы неизбежно столкнёмся со структурой, которая содержит в себе объединение. В Rust отсутствует встроенная поддержка объединений, так что нам придётся выработать стратегию самостоятельно. В С объединение — это тип, который хранит разные типы данных в одной области памяти. Существует много причин, по которым можно отдать предпочтение объединению, такие как: преобразование между бинарными представлениями целых чисел и чисел с плавающей точкой, реализация псевдо-полиморфизма и прямой доступ к битам. Я сфокусируюсь на псевдо-полиморфизме.

Как пример, давайте получим MAC адрес, основанный на имени интерфейса. Перечислим действия, необходимые для его получения:

  • Указать тип запроса, который будет использоваться с ioctl. Если я хочу получить MAC (или аппаратный) адрес, я указываю SIOCGIFHWADDR.
  • Записать имя интерфейса (что-то типа eth0) в ifr_name.
  • Сделать запрос, используя ioctl. В результате удачного запроса данные запишутся в ifr_ifru.

Если вас интересуют детали о получении MAC адреса, посмотрите эту инструкцию.

Нам необходимо использовать объявленную в С ioctl функцию и передать туда ifreq структуру. Посмотрев в /usr/include/net/if.h, мы увидим, что ifreq определена следующим образом:

struct  ifreq {
        char    ifr_name[IFNAMSIZ];
        union {
                struct  sockaddr ifru_addr;
                struct  sockaddr ifru_dstaddr;
                struct  sockaddr ifru_broadaddr;
                short   ifru_flags;
                int     ifru_metric;
                int     ifru_mtu;
                int     ifru_phys;
                int     ifru_media;
                int     ifru_intval;
                caddr_t ifru_data;
                struct  ifdevmtu ifru_devmtu;
                struct  ifkpi   ifru_kpi;
                u_int32_t ifru_wake_flags;
                u_int32_t ifru_route_refcnt;
                int     ifru_cap[2];
        } ifr_ifru;
}

Сложности возникают с объединением ifr_ifru. Взглянув на возможные типы в ifr_ifru, мы видим, что не все из них одинакового размера. short занимает два байта, а u_int32_t — четыре. Ещё больше усложняют ситуацию несколько структур неизвестного размера. Чтобы написать правильный код на Rust, важно выяснить точный размер ifreq структуры. Я создал небольшую программу на С и выяснил, что ifreq использует 16 байт для ifr_name и 24 байт для ifr_ifru.

Вооружившись знаниями о правильном размере структуры, мы можем начать представлять её в Rust. Одна из стратегий — создать специализированную структуру для всех типов объединения.

#[repr(C)]
pub struct IfReqShort {
    ifr_name: [c_char; 16],
    ifru_flags: c_short,
}

Мы можем использовать IfReqShort для запроса SIOCGIFINDEX. Эта структура меньше, чем ifreq структура в С. Хотя мы и предполагаем, что будет записано только 2 байта, внешний ioctl интерфейс ожидает 24 байта. Для безопасности давайте добавим 22 байта выравнивания (padding) в конце:

#[repr(C)]
pub struct IfReqShort {
    ifr_name: [c_char; 16],
    ifru_flags: c_short,
    _padding: [u8; 22],
}

Затем мы должны будем повторить этот процесс для каждого типа в объединении. Я нахожу это несколько утомительным, так как нам придётся создать множество структур и быть очень внимательными, чтобы не ошибиться с их размером. Другой способ представить объединение — это иметь буфер сырых байтов. Мы можем сделать единственное представление структуры ifreq в Rust следующим образом:

#[repr(C)]
pub struct IfReq {
    ifr_name: [c_char; 16],
    union: [u8; 24],
}

Этот буфер-объединение может хранить байты любого типа. Теперь мы можем определить методы для преобразования сырых байтов в нужный тип. Мы избежим использования небезопасного (unsafe) кода, отказавшись от использования transmute. Давайте создадим метод для получения MAC адреса, преобразовав сырые байты в sockaddr C-тип.

impl IfReq {
    pub fn ifr_hwaddr(&self) -> sockaddr {
        let mut s = sockaddr {
            sa_family: u16::from_be((self.data[0] as u16) << 8 | (self.data[1] as u16)),
            sa_data: [0; 14],
        };

        // basically a memcpy
        for (i, b) in self.data[2..16].iter().enumerate() {
            s.sa_data[i] = *b as i8;
        }

        s
    }
}

Такой подход оставляет нам одну структуру и метод для преобразования сырых байтов в желаемый тип. Посмотрев снова на наше ifr_ifru объединение, мы обнаружим, что существует по крайней мере два других запроса, которые тоже требуют создания sockaddr из сырых байтов. Применяя принцип DRY, мы можем реализовать приватный метод IfReq для преобразования сырых байтов в sockaddr. Однако, мы можем сделать лучше, абстрагировав детали создания sockaddr, short, int и т.д. от IfReq. Всё что нам необходимо — это сказать объединению, что нам нужен определённый тип. Давайте создадим IfReqUnion для этого:

#[repr(C)]
struct IfReqUnion {
    data: [u8; 24],
}

impl IfReqUnion {
    fn as_sockaddr(&self) -> sockaddr {
        let mut s = sockaddr {
            sa_family: u16::from_be((self.data[0] as u16) << 8 | (self.data[1] as u16)),
            sa_data: [0; 14],
        };

        // basically a memcpy
        for (i, b) in self.data[2..16].iter().enumerate() {
            s.sa_data[i] = *b as i8;
        }

        s
    }

    fn as_int(&self) -> c_int {
        c_int::from_be((self.data[0] as c_int) << 24 |
                       (self.data[1] as c_int) << 16 |
                       (self.data[2] as c_int) <<  8 |
                       (self.data[3] as c_int))
    }

    fn as_short(&self) -> c_short {
        c_short::from_be((self.data[0] as c_short) << 8 |
                         (self.data[1] as c_short))
    }
}

Мы реализовали методы для каждого из типов, которые составляют объединение. Теперь, когда наши преобразования управляются IfReqUnion, мы можем реализовать методы IfReq следующим образом:

#[repr(C)]
pub struct IfReq {
    ifr_name: [c_char; IFNAMESIZE],
    union: IfReqUnion,
}

impl IfReq {
    pub fn ifr_hwaddr(&self) -> sockaddr {
        self.union.as_sockaddr()
    }

    pub fn ifr_dstaddr(&self) -> sockaddr {
        self.union.as_sockaddr()
    }

    pub fn ifr_broadaddr(&self) -> sockaddr {
        self.union.as_sockaddr()
    }

    pub fn ifr_ifindex(&self) -> c_int {
        self.union.as_int()
    }

    pub fn ifr_media(&self) -> c_int {
        self.union.as_int()
    }

    pub fn ifr_flags(&self) -> c_short {
        self.union.as_short()
    }
}

В итоге у нас есть две структуры. Во первых, IfReq, которая представляет структуру памяти ifreq в языке С. В ней мы реализуем метод для каждого типа ioctl запроса. Во вторых, у нас есть IfRequnion, которая управляет различными типами объединения ifr_ifru. Мы создадим метод для каждого типа, который нам нужен. Это менее трудоёмко, чем создание специализированной структуры для каждого типа объединения, и предоставляет лучший интерфейс, чем преобразование типа в самой IfReq.

Вот более полный готовый пример. Предстоит ещё немного работы, но тесты проходят, и в коде реализуется описанная выше концепция.

Будьте осторожны, этот подход не идеален. В случае ifreq нам повезло, что ifr_name содержит 16 байтов и выровнено по границе слова. Если бы ifr_name не было выровнено по границе четырёхбайтного слова, мы столкнулись бы с проблемой. Тип нашего объединения [u8; 24], которое выравнивается по границе одного байта. У типа размером 24 байта было бы другое выравнивание. Вот короткий пример иллюстрирующий проблему. Допустим, у нас есть С-структура, содержащая следующее объединение:

struct foo {
    short x;
    union {
        int;
    } y;
}

Эта структура имеет размер 8 байт. Два байта для х, ещё два для выравнивания и четыре байта для у. Давайте попробуем изобразить это в Rust:

#[repr(C)]
pub struct Foo {
    x: u16,
    y: [u8; 4],
}

Структура Foo имеет размер только 6 байт: два байта для х и первые два u8 элемента, помещённые в то же четырёхбайтовое слово, что и х. Эта едва заметная разница может вызвать проблемы при передаче в С-функцию, которая ожидает структуру размеров в 8 байт.

До тех пор пока Rust не будет поддерживать объединения, такие проблемы сложно будет решить корректно. Удачи, но будьте осторожны!

Комментарии (6)