На каком условии можно безопасно представить значение C-перечисления как Rust enum на границе FFI?
Значение можно представить как Rust enum только после проверки, что его числовой дискриминант входит в множество допустимых вариантов этого enum. Атрибут repr(C) задаёт совместимое представление и ABI, но не разрешает произвольные значения, которые может передать C.
Если значение неизвестно Rust-коду, его нельзя без проверки превращать в enum: нужно отклонить его или сохранить как целое число в отдельном типе-обёртке.
В C перечисления тесно связаны с целочисленными типами. C-код может получить значение, отсутствующее среди объявленных элементов enum: из-за ошибки, расширения протокола, несовпадающих версий библиотек или битовых флагов.
Rust enum описывает закрытое множество допустимых вариантов. Это позволяет компилятору и оптимизатору исходить из того, что значение имеет корректный дискриминант. На границе FFI это предположение нельзя принимать без проверки входных данных.
Представим, что C передал числовое значение 2, а Rust enum содержит только варианты 0 и 1. Если Rust-код создаст из этого числа значение enum без проверки, будет нарушен инвариант валидности типа.
Последствия могут включать неопределённое поведение, некорректное сопоставление с вариантами и ошибочные оптимизации. Простое наличие совместимого размера или атрибута repr(C) не устраняет риск: представление в памяти и множество допустимых значений — разные свойства.
Сначала значение следует принять в ABI-совместимом целочисленном типе, затем явно сопоставить его с вариантами enum. Неизвестное значение должно приводить к ошибке, специальному результату или иной явно определённой ветке обработки.
Здесь repr(C) помогает согласовать представление enum с C, но безопасность обеспечивает именно проверка raw. Нельзя полагаться на безусловное преобразование или transmute из целого числа в enum.
Если неизвестные значения должны проходить через Rust без потери информации, лучше использовать целочисленный тип-обёртку, например структуру с полем c_int. Преобразование такой обёртки в enum выполняется только в проверяющей функции.
Отдельно нужно отличать обычное перечисление от битовых флагов. Для флагов произвольные комбинации битов могут быть легитимными, поэтому обычный enum обычно не подходит: следует хранить целое число и проверять допустимые биты либо использовать специализированный тип флагов.
Плагин на C передаёт режим работы числом. Текущая версия Rust знает режимы Read и Write, но будущая версия плагина может прислать новый режим Append.
Вариант с безусловным преобразованием в enum прост, но приводит к нарушению инварианта при обновлении только одной стороны. Вариант с отбрасыванием всех неизвестных значений безопасен, однако требует определить поведение: вернуть ошибку, выбрать режим по умолчанию или прекратить операцию.
Хранить значение всегда как целое число гибче и сохраняет совместимость, но переносит проверки в каждый участок кода, где нужен конкретный режим. Практичное решение — проверять значение на FFI-границе, возвращать ошибку для неизвестного режима, а для протоколов с ожидаемым расширением использовать целочисленную обёртку до отдельного этапа интерпретации.
1. Достаточно ли repr(C) для безопасного обмена enum?
Нет. repr(C) регулирует layout и ABI enum, чтобы стороны одинаково интерпретировали его представление. Он не превращает любой битовый шаблон, полученный из C, в допустимое значение Rust enum. Проверка диапазона или явное сопоставление всё равно необходимы.
2. Как следует передавать C-перечисление, если оно используется как набор флагов?
Обычный Rust enum не подходит для произвольных комбинаций флагов: комбинация может не совпадать ни с одним его вариантом. Нужно принять целочисленное значение, проверить разрешённые биты и предоставить операции над отдельным типом флагов. Неизвестные биты следует либо отклонять, либо явно сохранять как совместимые с будущими версиями.
3. Что делать, если Rust должен сохранять и возвращать неизвестные значения?
Нельзя временно помещать их в Rust enum, а затем рассчитывать восстановить исходное число. Следует использовать тип-обёртку над ABI-совместимым целым числом и преобразовывать его в enum только там, где требуется обработка известных вариантов. Это сохраняет значение без нарушения инвариантов Rust и позволяет корректно реализовать forward compatibility.