Все что будет происходить в этой статье можно повторять только на свой страх и риск, только в случае крайней нужды
В прошлых сериях я вас всячески убеждал, что очень-очень желательно принимать borrowed типы в ваших API вместо &Owned. И я очень надеюсь, что вы с этим согласились.
Но что же делать, если такое ужасно неправильное API предоставлено вам внешней зависимостью? И никакими мольбами до мэйнтейнеров не достучаться, а делать fork и поддерживать свою собственную версию вам не позволяют бюджет и планы.
Что ж. Если по-хорошему не получается, придется встать на скользкую тропу зла.
Я уже говорил, что существует способ получить &String из &str без аллокации и копировадия при создания нового объекта String. И этот способ ужасен. Но иногда приходится идти на крайние меры.
Что мы знаем про &str?
Это ссылка на последовательность UTF-8 байт. И поскольку длина этой последовательности нам статически неизвестна, она где-то хранится. Хранится она в самой ссылке. &str это так называемый толстый (fat) указатель (ссылка): он состоит из обычного указателя *const u8 -- на начало последовательности, и длины последовательности: usize 1.
А что мы знаем про String?
Это объект, который управляет выделенным буфером байт под UTF-8 последовательность. У этого объекта внутри есть (в каком-то виде 2):
- указатель на выделенный буфер:
data: *mut u8 - длина строки в этом буфере:
length: usize - и вместимость буфера на случай, если мы будем что-нибудь в строку дописывать:
capacity: usize
Что нам мешает собрать &String { data: s.ptr(), length: s.len(), capacity: s.len() } ?
На самом деле ничего, кроме того, что поля String приватные и у нас нет к ним доступа. Ну и еще всякие нюансы, что String это владеющий тип, а значит при выходе из скоупа он будет уничтожен -- вызовется деструктор, который попытается деаллоцировать этот буффер. А он черт знает кому принадлежит на самом деле, так что мы получим в конечном итоге double free или use-after-free.
Но давайте решать проблемы по мере их поступления.
Приватные поля? не проблема вовсе!
let s = "Hello World";
// String имеет публичный API для наших грязных дел
let s = unsafe { String::from_raw_parts(
s.as_ptr() as *mut u8,
s.len(),
s.len(),
) };И если мы исполним эти строчки, то закономерно получим
Exited with signal 6 (SIGABRT): abort program
free(): invalid pointer
Проблема в том, что вызван деструктор, который попытался деаллоцировать RO-protected строку. Решение проблемы? Не вызывать деструктор!
В Rust для этого есть целых два 3 механизма:
- Функция
std::mem::forget:forget(s)-- и деструкторsне будет вызван. Мне очень нравится эта функция тем, что ее название ясно говорит о том, когда ее стоит использовать: FORGET ABOUT IT. Потому что вы можете забыть (вот такой каламбур) вызвать функциюforgetи получить неопределенное поведение. - Обертка
std::mem::ManuallyDrop:let s = ManuallyDrop::new(s)-- и уsне будет вызван деструктор. Если вы используетеManuallyDropпри создании объекта, то забыть и случайно вызвать деструктор вам уже просто так не даст система типов.
use std::mem::ManuallyDrop;
let s = unsafe {
// ManuallyDrop::new не помечен unsafe
// Но для такого случая вполне разумно оставить его внутри
// unsafe блока, чтоб подчеркнуть:
// Это все часть нашего небезопасного шаманства. И без него
// все будет плохо
ManuallyDrop::new(
String::from_raw_parts(
s.as_ptr() as *mut u8,
s.len(),
s.len(),
))
};И теперь, если мы еще раз исполним этот ужас, уже ничего не упадет.
А дальше все просто: ManuallyDrop<T> реализует Deref<Target = T>, так что мы можем получить из него (ManuallyDrop<String>) так желанную &String.
Ужасное решение. Но оно работает. Если бы мы были в C++, мы могли бы на этом удовлетвориться и пойти решать другие более важные задачи.
Но у нас тут Rust. А в Rust, при написании unsafe {} мы должны стремиться к благой цели и идеалу: предоставить такую безопасную обертку, чтоб никто об наши ржавые гвозди не поранился и не заработал инфекцию неопределенного поведения. Нельзя так просто взять и сказать: это у вас skill issue, просто делайте хорошо, а плохо не делайте.
Если мы так и оставим переменную s: ManuallyDrop<String> видимой и доступной, то кто-нибудь 4 может
- Сохранить ее к себе в структуру и использовать позже, когда исходная ссылка перестанет быть действительной
- Вызовет безопасный метод
ManuallyDrop::into_innerи строка снова будет угрожать вызовом деструктора
Так что мы должны сделать две вещи:
- Спратать ножи. То есть изолировать это безобразие в структуре с приватными полями
- Снабдить ее lifetime параметром, ведь мы же по сути ссылочный тип конструируем. Пусть и странный.
use std::mem::ManuallyDrop;
use std::marker::PhantomData;
pub struct StringRef<'a> {
data: ManuallyDrop<String>,
_lifetime: PhantomData<&'a str>,
}И предоставляем конструктор:
impl<'a> StringRef<'a> {
pub fn new(s: &'a str) -> Self {
Self {
// Safety: деструктор String не будет вызван.
// Никакие мутирующие методы &mut String нельзя вызвать
// через StringRef. Только получить &String.
data: unsafe { ManuallyDrop::new(
String::from_raw_parts(
s.as_ptr() as *mut u8,
s.len(),
s.len(),
)) },
_lifetime: PhantomData
}
}
}А также доступ к &String через реализацию Deref.
use std::ops::Deref;
impl Deref for StringRef<'_> {
type Target = String;
fn deref(&self) -> &String {
self.data.deref()
}
}И теперь, наконец, то мы можем передать &str как &String без дополнительных unsafe манипуляций. Всё совершенно 5 безопасно. Верьте мне, я инженер.
fn bad_print(s: &String) {
println!("{s}");
}
fn good_print(s: &str) {
bad_print(&StringRef::new(s))
}
fn main() {
let s = "Hello World";
good_print(s);
}И оно работает. Печатает "hello world" и не падает. Всё? Можно расходиться?
Нет, нужно еще кое-что проверить. Нужно потестировать эту структуру с Miri -- runtime интерпретатором-анализатором для поиска неопределенного поведения.
Miri молчит. Miri доволен. Значит, по крайнем мере на текущий момент 6, это безопасно с довольно высоким шансом.
Footnotes
-
Вторая часть толстого указателя (ссылки) зовется метаданными (Metadata). Для
&strи&[T]метаданным выступает длина последовательности. Для&dyn Trait-- указатель на виртуальную таблицу. Если чтоBox<str>,Arc<[T]>,Rc<dyn Train>и даже просто*const dyn Trait-- это все примеры толстых указателей. Толстые указатели доставляют хлопот при их передаче через ffi (foreing function interface). Потому как они толстые иsizeof(fat pointer) == 2 * sizeof(normal pointer)↩ -
В стандартной библиотеке, что идет вместе с Rustc в настоящее время внутри
Stringхранится длина и зарезервированная вместимость как числовыеusizeзначения. Однако ничего не мешает какой-нибудь альтернативной версии, если такая когда-либо будет, там могут храниться, как во многих версиях C++ std::string, указателиdata,data_end,capacity_end↩ -
Есть еще третий.
std::mem::MaybeUninitтоже не вызывает деструкторов. Но он нам не очень подходит. У нас-то все инициализировано. ↩ -
Даже если это может быть лишь воображаемый гномик у вас в голове, в порыве членовредительства. И никто другой никогда так не напишет. ↩
-
За исключением совсем простых случаев, писать unsafe код в Rust невероятно тяжело корректно. В Rust есть значительное количество строгих нюансов, например: Не нарушать модель заимствования и алиасинга: stacked borrows, tree borrows и еще черт знает какие borrows, которые появятся в будущем. Их очень легко проморгать создав, например, неявно ссылку, которая, быть может, существует лишь в течение одной строчки
(*x).field = y. Я несколько сомневаюсь в корретности рассмотренного примера с точки зрения безумного формализма: внутриStringсидит не просто*mut u8, аUnique<u8>. А он навешивает дополнительные ограничения по алиасингу. Вроде бы, мы их не нарушаем, поскольку не создаем&mut Stringили&mut u8нигде в процессе. И вроде бы Miri, который проверяет весь этот формализм в процессе исполнения, с нами согласен. Но это не значит, что все останется также в будущих версиях. ↩ -
Вообще
String::from_raw_partsв своей документации требует, чтоб буфер, который мы в него отдаем, был получен из глобального Rust-аллокатора. Мы это требование нарушаем уже в таком простом примере: строковый литерал аллоцирован в .rodata статически. Тут мы стоим на развилке. Будем ли мы следовать строгому формализму "Это неопределенное поведение. Например, когда-нибудь в будущем эта фунция добавит assert и сломает нам код, так что так делать нельзя ни в коем случае". Либо же мы все-таки сторонники практического здравого смысла: "Это требование существует для безопасной передачи владения объектуStringи не проверяется при вызове (из-за накладных расходов). Если мы отключим управление буфером: отключим деструктор, не дадим вызывать мутирующие методы, проблемы не возникнет." ↩