Программирование RustUnsafe и памятьИнженер по системному программированию на Rust

При передаче текста из Rust в C какой инвариант требуется сохранить, чтобы чтение до нулевого байта не вышл...

При передаче текста из Rust в C какой инвариант требуется сохранить, чтобы чтение до нулевого байта не вышло за границы объекта?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Указатель должен обозначать действительную последовательность инициализированных байтов, внутри доступной памяти заканчивающуюся нулевым байтом. Эта память должна оставаться выделенной и неизменной настолько долго, насколько C-код может читать указатель.

Для обычной Rust-строки UTF-8 сам по себе такой инвариант не гарантирует: Rust хранит длину отдельно, а C-функция часто ищет конец по нулевому байту. CString создаёт подходящее представление, отклоняя внутренние нулевые байты и добавляя завершающий нулевой байт.

Исторический контекст

Классический интерфейс C передаёт строку как указатель на первый байт и не передаёт её длину. Поэтому вызывающая или вызываемая сторона должна найти конец последовательности по специальному нулевому байту.

В Rust строки и срезы устроены иначе: их длина хранится явно, а наличие нулевого байта внутри текста допустимо. Типы CString и CStr отделяют представление, пригодное для C, от обычных Rust-строк и делают соответствующий контракт явным.

Постановка проблемы

Если завершающего нулевого байта нет в доступной памяти, C-функция продолжит чтение за пределами объекта. Это может привести к неопределённому поведению, утечке содержимого соседней памяти или аварийному завершению.

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

Подробное решение

CString гарантирует отсутствие внутренних нулевых байтов и добавляет один нулевой байт в конце. CStr представляет уже существующую C-строку и при создании из raw pointer требует, чтобы указатель был ненулевым, адресовал доступную память с корректными байтами и приводил к нулевому байту в пределах допустимого объекта.

use std::ffi::{CStr, CString}; use std::os::raw::c_char; unsafe fn read_c_string(p: *const c_char) { let s = CStr::from_ptr(p); println!("{}", s.to_string_lossy()); } fn main() { let text = CString::new("привет").unwrap(); unsafe { read_c_string(text.as_ptr()) }; }

CString::as_ptr не передаёт владение памятью: объект text должен жить всё время синхронного вызова. Если C-библиотека сохраняет указатель, одного времени жизни локальной переменной недостаточно — нужно явно определить модель владения и освобождения.

Ручное создание буфера с нулевым байтом возможно, но требует самостоятельно доказать отсутствие внутренних нулей, корректность терминатора, инициализацию и время жизни. Передача пары указатель-длина безопаснее для API, который её поддерживает, но не заменяет C-строку для функции, которая принимает только const char* и ищет терминатор.

Ситуация из практики

Библиотека журналирования C принимает const char* и синхронно читает строку до нулевого байта. Рассматривались три варианта: передать указатель на внутренний буфер Rust-строки, вручную скопировать байты в Vec<u8> с терминатором или создать CString.

Первый вариант не даёт C требуемого строкового представления. Второй может работать, но переносит на разработчика проверку внутренних нулей, стабильности адреса и времени жизни буфера; изменение Vec после получения указателя может сделать указатель недействительным.

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

Что кандидаты часто упускают

  1. Достаточно ли UTF-8 для передачи строки в C?

Нет. UTF-8 определяет кодирование последовательности байтов, но не способ определения её длины. C-функции, работающие с нуль-терминированными строками, требуют отдельного завершающего нулевого байта; кроме того, конкретная C-библиотека может ожидать другую кодировку.

  1. Что произойдёт при внутреннем нулевом байте в передаваемом тексте?

C-функция обычно остановится на первом нулевом байте и увидит только префикс. CString::new возвращает ошибку для такого содержимого, что позволяет не скрывать потерю данных; если бинарные данные допускают нули, для них следует использовать интерфейс с явной длиной.

  1. Можно ли сохранить указатель из CString::as_ptr после выхода из области видимости CString?

Нет, если после этого C-код может читать указатель. Уничтожение CString освобождает принадлежащую ему память, и сохранённый адрес становится висячим. Для долгого хранения нужно передать владение согласованным способом, например через специально оговорённый FFI-контракт, и обеспечить освобождение тем аллокатором и тем API, которые предусмотрены этим контрактом.