Предлагаю вашему вниманию перевод статьи "Working with C unions in Rust FFI" за авторством Herman J. Radtke III.
Примечание: Эта статья предполагает, что читатель знаком с Rust FFI, порядком байтов (endianess) и ioctl.
При создании биндингов к коду на С мы неизбежно столкнёмся со структурой, которая содержит в себе объединение. В Rust отсутствует встроенная поддержка объединений, так что нам придётся выработать стратегию самостоятельно. В С объединение — это тип, который хранит разные типы данных в одной области памяти. Существует много причин, по которым можно отдать предпочтение объединению, такие как: преобразование между бинарными представлениями целых чисел и чисел с плавающей точкой, реализация псевдо-полиморфизма и прямой доступ к битам. Я сфокусируюсь на псевдо-полиморфизме.
Как пример, давайте получим MAC адрес, основанный на имени интерфейса. Перечислим действия, необходимые для его получения:
Если вас интересуют детали о получении MAC адреса, посмотрите эту инструкцию.
Нам необходимо использовать объявленную в С ioctl функцию и передать туда ifreq структуру. Посмотрев в /usr/include/net/if.h, мы увидим, что ifreq определена следующим образом:
Сложности возникают с объединением ifr_ifru. Взглянув на возможные типы в ifr_ifru, мы видим, что не все из них одинакового размера. short занимает два байта, а u_int32_t — четыре. Ещё больше усложняют ситуацию несколько структур неизвестного размера. Чтобы написать правильный код на Rust, важно выяснить точный размер ifreq структуры. Я создал небольшую программу на С и выяснил, что ifreq использует 16 байт для ifr_name и 24 байт для ifr_ifru.
Вооружившись знаниями о правильном размере структуры, мы можем начать представлять её в Rust. Одна из стратегий — создать специализированную структуру для всех типов объединения.
Мы можем использовать IfReqShort для запроса SIOCGIFINDEX. Эта структура меньше, чем ifreq структура в С. Хотя мы и предполагаем, что будет записано только 2 байта, внешний ioctl интерфейс ожидает 24 байта. Для безопасности давайте добавим 22 байта выравнивания (padding) в конце:
Затем мы должны будем повторить этот процесс для каждого типа в объединении. Я нахожу это несколько утомительным, так как нам придётся создать множество структур и быть очень внимательными, чтобы не ошибиться с их размером. Другой способ представить объединение — это иметь буфер сырых байтов. Мы можем сделать единственное представление структуры ifreq в Rust следующим образом:
Этот буфер-объединение может хранить байты любого типа. Теперь мы можем определить методы для преобразования сырых байтов в нужный тип. Мы избежим использования небезопасного (unsafe) кода, отказавшись от использования transmute. Давайте создадим метод для получения MAC адреса, преобразовав сырые байты в sockaddr C-тип.
Такой подход оставляет нам одну структуру и метод для преобразования сырых байтов в желаемый тип. Посмотрев снова на наше ifr_ifru объединение, мы обнаружим, что существует по крайней мере два других запроса, которые тоже требуют создания sockaddr из сырых байтов. Применяя принцип DRY, мы можем реализовать приватный метод IfReq для преобразования сырых байтов в sockaddr. Однако, мы можем сделать лучше, абстрагировав детали создания sockaddr, short, int и т.д. от IfReq. Всё что нам необходимо — это сказать объединению, что нам нужен определённый тип. Давайте создадим IfReqUnion для этого:
Мы реализовали методы для каждого из типов, которые составляют объединение. Теперь, когда наши преобразования управляются IfReqUnion, мы можем реализовать методы IfReq следующим образом:
В итоге у нас есть две структуры. Во первых, IfReq, которая представляет структуру памяти ifreq в языке С. В ней мы реализуем метод для каждого типа ioctl запроса. Во вторых, у нас есть IfRequnion, которая управляет различными типами объединения ifr_ifru. Мы создадим метод для каждого типа, который нам нужен. Это менее трудоёмко, чем создание специализированной структуры для каждого типа объединения, и предоставляет лучший интерфейс, чем преобразование типа в самой IfReq.
Вот более полный готовый пример. Предстоит ещё немного работы, но тесты проходят, и в коде реализуется описанная выше концепция.
Будьте осторожны, этот подход не идеален. В случае ifreq нам повезло, что ifr_name содержит 16 байтов и выровнено по границе слова. Если бы ifr_name не было выровнено по границе четырёхбайтного слова, мы столкнулись бы с проблемой. Тип нашего объединения [u8; 24], которое выравнивается по границе одного байта. У типа размером 24 байта было бы другое выравнивание. Вот короткий пример иллюстрирующий проблему. Допустим, у нас есть С-структура, содержащая следующее объединение:
Эта структура имеет размер 8 байт. Два байта для х, ещё два для выравнивания и четыре байта для у. Давайте попробуем изобразить это в Rust:
Структура Foo имеет размер только 6 байт: два байта для х и первые два u8 элемента, помещённые в то же четырёхбайтовое слово, что и х. Эта едва заметная разница может вызвать проблемы при передаче в С-функцию, которая ожидает структуру размеров в 8 байт.
До тех пор пока Rust не будет поддерживать объединения, такие проблемы сложно будет решить корректно. Удачи, но будьте осторожны!
Примечание: Эта статья предполагает, что читатель знаком с 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 не будет поддерживать объединения, такие проблемы сложно будет решить корректно. Удачи, но будьте осторожны!