Что должен учитывать Rust-код при передаче аргументов в C variadic-функцию, чтобы тип, прочитанный через va_arg, совпал с фактически переданным?
Rust-код обязан учитывать стандартные продвижения типов C в variadic-вызовах: float передаётся как double, а целые типы уже́ int обычно преобразуются в int или unsigned int. Читающий код C должен использовать именно фактически переданный тип; несовпадение с va_arg приводит к неопределённому поведению.
Кроме того, нужно соблюдать ABI функции, её форматную строку или иной контракт интерпретации аргументов. Сам факт совпадения размера типов не делает variadic-вызов безопасным.
Variadic-функции появились в C для API с переменным числом аргументов, например форматированного вывода. У вызывающей стороны нет обычной сигнатуры для каждого аргумента, поэтому соглашение о представлении и продвижении типов стало частью ABI и контракта функции.
Rust сохраняет возможность вызывать такие функции через C ABI, но не может проверить соответствие переменных аргументов коду, который извлекает их из va_list. Поэтому эта часть корректности остаётся обязанностью unsafe-кода и документации C API.
Тип, указанный в исходном выражении Rust, не всегда совпадает с типом, реально поступившим в C variadic-функцию. Например, передача f32 приводит к передаче double, а передача небольшого целого типа может привести к передаче int.
Если C-код извлекает такой аргумент как исходный float или как другой несовместимый целый тип, он нарушает контракт va_arg. Последствия не ограничиваются неверным значением: возможны повреждение чтения последующих аргументов, неверное использование регистров или стека и неопределённое поведение.
В фиксированной части объявления Rust нужно точно описать параметры, а для переменной части — передавать значения с учётом правил C. На практике это означает, что значение f32 передают как f64, а небольшие целые значения явно приводят к ожидаемому продвинутому типу, обычно i32 или u32 в зависимости от контракта.
Минимальная схема выглядит так:
Здесь вызывающий код передаёт значение с плавающей точкой как f64, а целое — как i32. Это корректно только при условии, что контракт log_values действительно извлекает аргументы как double и int; Rust не проверяет это автоматически.
Нельзя полагаться на одно лишь числовое совпадение размеров. Важны тип категории, правила продвижения, ABI и способ, которым принимающая функция интерпретирует последовательность аргументов. Безопасная Rust-обёртка должна ограничить допустимые варианты аргументов или вообще заменить variadic API типизированными функциями.
Rust-модуль вызывает стороннюю C-библиотеку журналирования. В документации сказано, что спецификатор %f извлекает double, а %d — int. Разработчик передаёт f32 и i16, считая, что библиотека сама использует исходные типы.
Рассматривались два варианта. Первый — оставить прямой variadic-вызов: он прост, но создаёт риск несовпадения типов при каждом новом месте вызова. Второй — перед вызовом явно преобразовывать значения к f64 и i32, а формат и набор аргументов скрыть в узкой обёртке; этот вариант требует больше кода, зато централизует проверку контракта.
Выбран второй вариант. В результате значения извлекаются C-кодом в тех типах, которые предусмотрены ABI и документацией, а потенциально опасная логика остаётся в одном проверенном месте.
Достаточно ли передать f32, если C-функция ожидает значение, занимающее те же четыре байта?
Нет. В variadic-вызове C значение float подвергается продвижению и передаётся как double. Читающий код должен извлечь double; попытка извлечь float нарушает контракт независимо от исходного размера f32.
Можно ли считать variadic-вызов безопасным, если форматная строка хранится в Rust и не изменяется?
Нет. Неизменность строки защищает только от изменения самого формата. Нужно ещё доказать соответствие каждого спецификатора фактическому типу переданного аргумента, включая продвижение типов и требования конкретной C-библиотеки.
Почему безопасная обёртка не может просто принимать произвольный список Rust-значений?
Потому что обычная проверка типов Rust не связывает список variadic-аргументов с форматом или логикой va_arg внутри C-функции. Обёртка должна заранее ограничить поддерживаемые типы и самостоятельно привести их к ожидаемым ABI-типам либо предоставить отдельные типизированные операции. Иначе вызывающий код всё равно сможет сформировать несовместимую последовательность аргументов.