Работа с С-объединениями (union) в Rust FFI
Примечание: Эта статья предполагает, что читатель знаком с Rust FFI (перевод), порядком байтов (endianess) и ioctl.
При создании биндингов к коду на С мы неизбежно столкнёмся со структурой, которая содержит в себе объединение. В Rust отсутствует встроенная поддержка объединений, так что нам придётся выработать стратегию самостоятельно. В С объединение — это тип, который хранит разные типы данных в одной области памяти. Существует много причин, по которым можно отдать предпочтение объединению, такие как: преобразование между бинарными представлениями целых чисел и чисел с плавающей точкой, реализация псевдо-полиморфизма и прямой доступ к битам. Я сфокусируюсь на псевдо-полиморфизме.
Как пример, давайте получим MAC адрес, основанный на имени интерфейса. Перечислим действия, необходимые для его получения:
- Указать тип запроса, который будет использоваться с ioctl. Если я хочу получить MAC (или аппаратный) адрес, я указываю SIOCGIFHWADDR.
- Записать имя интерфейса (что-то типа eth0) в
ifr_name
. - Сделать запрос, используя ioctl. В результате удачного запроса данные запишутся в
ifr_ifru
.
Если вас интересуют детали о получении MAC адреса, посмотрите эту инструкцию.
Нам необходимо использовать объявленную в С функцию ioctl и передать туда структуру ifreq. Посмотрев в /usr/include/net/if.h, мы увидим, что ifreq определена следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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. Одна из стратегий — создать специализированную структуру для всех типов объединения.
1 2 3 4 5 | #[repr(C)] pub struct IfReqShort { ifr_name: [c_char; 16], ifru_flags: c_short, } |
Мы можем использовать IfReqShort
для запроса SIOCGIFINDEX
. Эта структура меньше, чем структура ifreq
в С. Хотя мы и
предполагаем, что будет записано только 2 байта, внешний ioctl
интерфейс ожидает 24 байта. Для безопасности давайте
добавим 22 байта выравнивания (padding) в конце:
1 2 3 4 5 6 | #[repr(C)] pub struct IfReqShort { ifr_name: [c_char; 16], ifru_flags: c_short, _padding: [u8; 22], } |
Затем мы должны будем повторить этот процесс для каждого типа в объединении. Я нахожу это несколько утомительным, так как
нам придётся создать множество структур и быть очень внимательными, чтобы не ошибиться с их размером. Другой способ
представить объединение — это иметь буфер сырых байтов. Мы можем сделать единственное представление структуры ifreq
в Rust следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | #[repr(C)] pub struct IfReq { ifr_name: [c_char; 16], union: [u8; 24], } ``` Этот буфер-объединение может хранить байты любого типа. Теперь мы можем определить методы для преобразования сырых байтов в нужный тип. Мы избежим использования небезопасного (unsafe) кода, отказавшись от использования transmute. Давайте создадим метод для получения MAC адреса, преобразовав сырые байты в тип языка С `sockaddr`. ```rust 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
для этого:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #[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
следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #[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 байта было бы другое
выравнивание. Вот короткий пример иллюстрирующий проблему. Допустим, у нас есть С-структура, содержащая следующее
объединение:
1 2 3 4 5 6 | struct foo { short x; union { int; } y; } |
Эта структура имеет размер 8 байт. Два байта для х, ещё два для выравнивания и четыре байта для у. Давайте попробуем изобразить это в Rust:
1 2 3 4 5 | #[repr(C)] pub struct Foo { x: u16, y: [u8; 4], } |
Структура Foo имеет размер только 6 байт: два байта для х и первые два u8 элемента, помещённые в то же четырёхбайтовое слово, что и х. Эта едва заметная разница может вызвать проблемы при передаче в С-функцию, которая ожидает структуру размером в 8 байт.
До тех пор пока Rust не будет поддерживать объединения, такие проблемы сложно будет решить корректно. Удачи, но будьте осторожны!
Примечание переводчика: уже существует несколько предложений по добавлению нужной функциональности в язык.