Варьируемость в системах типов определяет, как отношения подтипов между обобщенными параметрами влияют на общий тип. Подход Rust был сильно вдохновлен исследованиями в области управления памятью на основе регионов и необходимостью предотвращения уязвимостей, связанных с использованием после освобождения. Когда Rust представил изменяемые ссылки (&mut T), разработчики должны были решить, должны ли они быть ковариантными (как &T), контравариантными или инвариантными. Выбор инвариантности для &mut T вместо T был критически важен для поддержания безопасности памяти без необходимости в проверках во время выполнения.
Если бы &mut T были ковариантными относительно T, вы могли бы заменить &mut U там, где ожидается &mut V, если U является подтипом V. В терминах времени жизни, поскольку 'long является подтипом 'short (поскольку 'long переживает 'short), это означало бы, что вы могли бы присвоить &mut &'long str переменной &mut &'short str. Это кажется безобидным, но создает дыру в корректности.
&mut T является инвариантным относительно T. Это означает, что &mut &'a str и &mut &'b str являются несвязанными типами, если только 'a точно не равно 'b, независимо от отношений подтипов между временами жизни. Компилятор отклоняет код, который пытается привести их к совместимости, предотвращая присвоение краткоживущих данных туда, где ожидаются ссылки на долговечные данные через изменяемую индирекцию.
Пример кода:
fn demonstrate_invariance() { let mut long_lived: &'static str = "static string"; // Это бы скомпилировалось, если бы &mut T были ковариантными: // let short_ref: &mut &'short str = &mut long_lived; // Но поскольку &mut T инвариантны, это не проходит: // ошибка: несовпадение времен жизни // let short_ref: &mut &'_ str = &mut long_lived; let local = String::from("temporary"); // Если бы вышеуказанное было разрешено, мы могли бы сделать: // *short_ref = &local; // Теперь long_lived указывает на освобожденные данные (UAF!) } // local освобождается здесь
Команда разрабатывала менеджер конфигурации для высокопроизводительной сети. Основная структура должна была удерживать изменяемую ссылку на конфигурацию протокола, которую можно было бы заменять во время выполнения без передачи владения.
Проблема: Первичный дизайн API использовал &mut &'a Config, где 'a была продолжительностью сетевой сессии. Разработчики пытались инициализировать это с &mut &'static Config (для глобальных конфигураций по умолчанию), а затем передать это в функции, ожидающие &mut &'session Config. Компилятор отклонил это, вызвав путаницу, потому что неизменяемые ссылки (& &'static Config) работали нормально.
Рассмотренные решения:
1. Небезопасная трансмутация для принудительного преобразования Команда рассматривала возможность использования std::mem::transmute, чтобы преобразовать &mut &'static Config в &mut &'session Config. Это обошло бы проверки варьируемости компилятора. Тем не менее, это позволило бы записывать ссылку на краткоживущую конфигурацию в место, которое могло бы пережить текущую область видимости, что привело бы к непосредственному неопределенному поведению, если конфигурация была бы доступна после ее освобождения. Риск использования после освобождения в коде производственной версии сделал это неприемлемым.
2. Изменение на неизменяемые ссылки Они обдумали возможность изменения API для использования & &'a Config вместо &mut &'a Config. Поскольку совместные ссылки являются ковариантными, & &'static Config могли бы приводиться к & &'session Config. Однако это лишило бы возможности атомарной замены конфигураций во время обновлений в реальном времени, что было основной необходимостью для горячей перезагрузки настроек без перезапуска соединений.
3. Использование Cell<&'a Config> для внутренней изменяемости Этот вариант позволил бы изменения через совместную ссылку. Однако Cell<T> также является инвариантным относительно T по тем же причинам безопасности, поэтому это не решило бы проблему варьируемости. Кроме того, Cell не предоставляет синхронизации для доступа из нескольких потоков, а накладные расходы на проверки заимствования во время выполнения с помощью RefCell были сочтены слишком дорогими для горячего пути.
4. Переработка с использованием владеющих типов и индирекции Выбранное решение полностью исключило паттерн ссылка-на-ссылку. Вместо хранения &mut &'a Config структура хранила &'a mut ConfigHolder, где ConfigHolder был оберточным типом, обладающим правом собственности. Это переместило изменяемость на уровень владельца, а не ссылки, избегая ловушки варьируемости, сохраняя при этом возможность замены конфигураций. API стал более удобным, поскольку пользователям больше не нужно было управлять двойными ссылками.
Результат: Переработка привела к созданию более безопасного API, который компилировался без использования небезопасного кода. Инвариантная природа &mut T заставила команду признать потенциальный архитектурный недостаток, где предположения о длительности жизни могли быть нарушены. Финальная система предотвратила категорию ошибок, где устаревшие указатели конфигурации могли сохраняться за пределами их срока действия.
Почему Cell<T> инвариантен относительно T, и как это связано с варьируемостью &mut T? Cell<T> предоставляет внутреннюю изменяемость, позволяя модификацию через совместные ссылки. Если бы Cell<T> были ковариантными относительно T, вы могли бы поднять Cell<&'short str> до Cell<&'static str>. Затем вы могли бы сохранить ссылку на краткоживущую строку внутри и позже прочитать ее через тип Cell<&'static str>, рассматривая временные данные как статические. Это создало бы уязвимость использования после освобождения. Поэтому, как и &mut T, Cell<T> (и UnsafeCell<T>) должны быть инвариантными относительно T, чтобы предотвратить запись краткоживущих данных в слот, который заявляет, что содержит долговечные данные. Эта инвариантность распространяется на RefCell, Mutex и другие типы внутренней изменяемости.
Как PhantomData<T> влияет на варьируемость структуры, содержащей фактический T, и почему вы бы использовали PhantomData<fn(T)>, чтобы достичь контравариантности? PhantomData<T> говорит компилятору рассматривать структуру так, как будто она владеет T для целей варьируемости и проверки освобождения. По умолчанию PhantomData<T> дает структуре такую же варьируемость, как и T. Однако указатели на функции имеют особую варьируемость: fn(A) -> B является контравариантным относительно A (аргумент) и ковариантным относительно B (возврат). Если вам нужно, чтобы структура была контравариантной относительно времени жизни (означая, что Struct<'long> является подтипом Struct<'short>, когда 'long переживает 'short), вы используете PhantomData<fn(T)>. Это имеет решающее значение для создания безопасных обратных вызовов или компараторов, где отношение между временами жизни должно быть перевернуто.
В небезопасном коде, при реализации самоссылочной структуры с использованием сырых указателей, почему структура должна быть помечена как инвариантная относительно своих параметров времени жизни?
Когда структура содержит сырой указатель, указывающий на другие данные внутри самой структуры (самоссылочная), продолжительность жизни этой структуры определяет действительность указателя. Если бы структура была ковариантной относительно своего времени жизни 'a, вы могли бы сократить 'a до более короткого времени жизни 'b, тем самым утверждая, что структура существует только в течение 'b. Однако сырой указатель внутри был создан, когда структура существовала дольше, и мог указывать на данные, которые больше не являются действительными в более короткой области видимости. Инвариантность гарантирует, что структуру нельзя было бы привести к более короткому времени жизни, сохраняя инвариант безопасности, что самоссылка остается действительной на протяжении всего времени жизни, закодированного в системе типов. Поэтому Pin часто сочетается с явными маркерами варьируемости в небезопасных самоссылочных реализациях.