C-библиотека изменяет объект через raw pointer, пока Rust хранит на него &T. Какой инвариант безопасности нарушается?
Нарушается инвариант неизменяемости данных через &T: пока существует shared-ссылка, значение не должно изменяться, кроме случаев, явно разрешённых через UnsafeCell. Сам факт, что C получил raw pointer, не отменяет обязательства Rust-кода соблюдать этот инвариант. Если C изменяет объект без разрешённого механизма внутренней изменяемости или синхронизации, поведение программы становится неопределённым.
Модель ссылок Rust разделяет доступ на совместный и уникальный. &T допускает одновременное чтение, а &mut T предоставляет эксклюзивный доступ; это позволяет компилятору оптимизировать обращения к памяти, не предполагая произвольных изменений между ними.
Raw pointers нужны для низкоуровневого кода и FFI, где такие гарантии нельзя автоматически проверить. Однако переход к raw pointer не снимает инварианты исходной ссылки: если указатель получен из &T, внешний код не получает право произвольно менять объект.
Типичный риск возникает, когда Rust передаёт C адрес объекта, продолжая использовать shared-ссылку. C может записать новое значение через *mut T, а Rust затем прочитает объект через прежнюю &T или полагается на то, что его содержимое не менялось.
Проблема существует даже при одном потоке. Она связана не только с гонкой данных, но и с самим нарушением контракта ссылки. При одновременной записи из другого потока добавляется отдельный риск гонки данных и требуется подходящая атомарность или синхронизация.
В первом случае UnsafeCell отсутствует, поэтому изменение value через C несовместимо с существующей &State. Во втором случае внутреннее изменение может быть допустимым, но только при соблюдении дополнительных правил доступа и синхронизации.
Ссылка &T означает не просто «адрес, по которому можно читать». Она также утверждает, что объект корректен, живёт достаточно долго и не изменяется через конфликтующий доступ в течение времени действия ссылки. Raw pointer, созданный из этой ссылки, наследует практический контекст её использования: преобразование указателя не превращает запрещённую запись в разрешённую.
Если C должен изменять объект, безопаснее передавать ему *mut T, полученный из корректного уникального доступа, например из &mut T, и не использовать одновременно конфликтующие ссылки Rust. Такой контракт должен включать срок действия указателя, допустимость записи и отсутствие конкурирующих обращений.
Для специально предназначенного внутреннего изменения применяется UnsafeCell<T>. Он сообщает компилятору, что содержимое может изменяться даже при наличии &UnsafeCell<T>. Но UnsafeCell не предоставляет автоматически потокобезопасность, блокировку, атомарность, проверку границ или корректность операций C.
Если объект доступен из нескольких потоков, нужно отдельно обеспечить синхронизацию: например, использовать атомарные типы, mutex или другой согласованный протокол. Простая упаковка поля в UnsafeCell не делает произвольные чтения и записи безопасными.
Практическое правило: перед передачей адреса в C нужно определить, какая модель доступа действует на всём интервале вызова. Если C только читает, &T может быть совместима с контрактом. Если C пишет, нужен уникальный доступ либо явно предусмотренная внутренняя изменяемость; при этом Rust не должен сохранять конфликтующие ссылки, которыми он продолжает пользоваться.
Библиотека C принимает контекст и при вызове обратной функции обновляет поле состояния. Изначально Rust передал адрес объекта, полученный из &Context, потому что C объявлял параметр как указатель и разработчик счёл запись через него деталью реализации.
Рассматривались варианты:
*mut Context, преобразовав его из &Context. Это не решает проблему: меняется тип указателя, но не нарушенный инвариант shared-ссылки.&mut Context на время вызова. Это корректно, если C не сохраняет указатель и во время вызова нет других обращений к объекту.UnsafeCell. Это подходит, если внутреннее изменение действительно является частью дизайна и Rust-код соблюдает правила синхронизации и времени жизни.Выбрано разделение состояния и передача временного уникального доступа для синхронной операции. В результате C не получил возможность менять объект, пока Rust удерживает конфликтующую &Context, а граница FFI получила явно проверяемый контракт.
Достаточно ли преобразовать &T в *mut T, чтобы C получил право записи?
Нет. Такое преобразование меняет представление адреса, но не создаёт уникальный доступ и не отменяет инвариант &T. Запись через полученный указатель остаётся нарушением правил, если объект не использует механизм вроде UnsafeCell и отсутствует иной корректный контракт доступа.
Делает ли UnsafeCell<T> любые записи из C безопасными?
Нет. UnsafeCell разрешает внутреннюю изменяемость с точки зрения модели алиасинга Rust, но не гарантирует корректность самого указателя, время жизни объекта, правильный размер и выравнивание. При доступе из нескольких потоков всё равно нужны синхронизация или атомарные операции; иначе возможна гонка данных.
Можно ли безопасно создать &T после того, как C завершил запись через raw pointer?
Да, если к моменту создания ссылки запись полностью завершена, объект инициализирован, корректно выровнен, живёт достаточно долго, а C больше не использует указатель для конфликтующего доступа. Если C сохранил указатель и может обратиться к объекту позже, Rust должен обеспечить отсутствие такой записи на всём времени действия &T либо использовать модель с UnsafeCell и необходимой синхронизацией.