Skip to content

Latest commit

 

History

History
177 lines (126 loc) · 14.8 KB

File metadata and controls

177 lines (126 loc) · 14.8 KB

Шаманим выход из безвыходного положения

Все что будет происходить в этой статье можно повторять только на свой страх и риск, только в случае крайней нужды

В прошлых сериях я вас всячески убеждал, что очень-очень желательно принимать 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

  1. Вторая часть толстого указателя (ссылки) зовется метаданными (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)

  2. В стандартной библиотеке, что идет вместе с Rustc в настоящее время внутри String хранится длина и зарезервированная вместимость как числовые usize значения. Однако ничего не мешает какой-нибудь альтернативной версии, если такая когда-либо будет, там могут храниться, как во многих версиях C++ std::string, указатели data, data_end, capacity_end

  3. Есть еще третий. std::mem::MaybeUninit тоже не вызывает деструкторов. Но он нам не очень подходит. У нас-то все инициализировано.

  4. Даже если это может быть лишь воображаемый гномик у вас в голове, в порыве членовредительства. И никто другой никогда так не напишет.

  5. За исключением совсем простых случаев, писать unsafe код в Rust невероятно тяжело корректно. В Rust есть значительное количество строгих нюансов, например: Не нарушать модель заимствования и алиасинга: stacked borrows, tree borrows и еще черт знает какие borrows, которые появятся в будущем. Их очень легко проморгать создав, например, неявно ссылку, которая, быть может, существует лишь в течение одной строчки (*x).field = y. Я несколько сомневаюсь в корретности рассмотренного примера с точки зрения безумного формализма: внутри String сидит не просто *mut u8, а Unique<u8>. А он навешивает дополнительные ограничения по алиасингу. Вроде бы, мы их не нарушаем, поскольку не создаем &mut String или &mut u8 нигде в процессе. И вроде бы Miri, который проверяет весь этот формализм в процессе исполнения, с нами согласен. Но это не значит, что все останется также в будущих версиях.

  6. Вообще String::from_raw_parts в своей документации требует, чтоб буфер, который мы в него отдаем, был получен из глобального Rust-аллокатора. Мы это требование нарушаем уже в таком простом примере: строковый литерал аллоцирован в .rodata статически. Тут мы стоим на развилке. Будем ли мы следовать строгому формализму "Это неопределенное поведение. Например, когда-нибудь в будущем эта фунция добавит assert и сломает нам код, так что так делать нельзя ни в коем случае". Либо же мы все-таки сторонники практического здравого смысла: "Это требование существует для безопасной передачи владения объекту String и не проверяется при вызове (из-за накладных расходов). Если мы отключим управление буфером: отключим деструктор, не дадим вызывать мутирующие методы, проблемы не возникнет."