Как Rust обеспечивает безопасность указателей во время компиляции?

Я где-то читал, что на языке, который содержит указатели, компилятор не может полностью решить во время компиляции, будут ли все указатели правильно использованы и/или действительны (обратитесь к живому объекту) по разным причинам, поскольку это будет по существу решением проблемы прекращения. Это не удивительно, интуитивно, потому что в этом случае мы могли бы вывести поведение во время выполнения программы во время компиляции, аналогично тому, что указано в этом связанном вопросе.

Однако, из того, что я могу сказать, язык Rust требует, чтобы проверка указателя выполнялась целиком во время компиляции (не существует undefined поведения, связанного с указателями, "безопасных" указателей, по крайней мере, и нет "недопустимого указателя" или исключение "нулевой указатель" ).

Предполагая, что компилятор Rust не решает проблему остановки, где ложь лежит?

  • Это так, что проверка указателя не выполняется полностью во время компиляции,, а интеллектуальные указатели Rust все еще вводят некоторые служебные данные во время выполнения по сравнению с, скажем, необработанными указателями в C?
  • Или возможно, что компилятор Rust не может принимать полностью правильные решения, и иногда ему нужно просто доверять программному программисту ™, возможно, используя одну из аннотаций времени жизни (те, которые имеют синтаксис <'lifetime_ident>)? В этом случае означает ли это, что гарантия безопасности указателя/памяти не равна 100%, и все еще полагается на программиста, написавшего правильный код?
  • Другая возможность заключается в том, что указатели Rust не являются "универсальными" или ограниченными в некотором смысле, поэтому компилятор может полностью вывести свои свойства во время компиляции, но они не так полезны, как e. г. raw указатели на C или умные указатели на С++.
  • Или, может быть, это нечто совершенно другое, и я неправильно интерпретирую один или несколько из
    { "pointer", "safety", "guaranteed", "compile-time" }.

Ответ 1

Отказ от ответственности. Я немного тороплюсь, так что это немного извивается. Не стесняйтесь очищать его.

Один проницательный трюк, который создатели языка ненавидят и торгуют; в основном это: Rust может только рассуждать о времени жизни 'static (используется для глобальных переменных и других объектов всей цельной программы) и времени жизни переменных стека (то есть локальных): он не может выразить или рассуждать о времени жизни распределений кучи.

Это означает несколько вещей. Прежде всего, все типы библиотек, которые имеют дело с распределением кучи (т.е. Box<T>, Rc<T>, Arc<T>), все принадлежат тому, на что указывают. В результате на самом деле им не нужны времена жизни, чтобы существовать.

Когда вам нужно время жизни, вы получаете доступ к содержимому смарт-указателя. Например:

let mut x: Box<i32> = box 0;
*x = 42;

Что происходит за кулисами на этой второй строке:

{
    let box_ref: &mut Box<i32> = &mut x;
    let heap_ref: &mut i32 = box_ref.deref_mut();
    *heap_ref = 42;
}

Другими словами, поскольку Box не является волшебным, мы должны сообщить компилятору, как превратить его в обычный, заимствованный указатель мельницы. Для этого важны черты Deref и DerefMut. Возникает вопрос: каково ровно время жизни heap_ref?

Ответ на это в определении DerefMut (из памяти, потому что я тороплюсь):

trait DerefMut {
    type Target;
    fn deref_mut<'a>(&'a mut self) -> &'a mut Target;
}

Как я уже говорил, Рурш абсолютно не может говорить о "временах жизни кучи". Вместо этого он должен привязать время жизни выделенного кучи i32 к единственному другому времени жизни, которое у него есть: время жизни Box.

Это означает, что "сложные" вещи не имеют выраженной жизни и, следовательно, должны владеть тем, чем они управляют. Когда вы конвертируете сложный интеллектуальный указатель/дескриптор в простой заимствованный указатель, это тот момент, когда вы должны представить всю жизнь, и обычно вы просто используете время жизни самого дескриптора.

На самом деле, я должен уточнить: "временем жизни дескриптора", я действительно имею в виду "время жизни переменной, в которой хранится данный дескриптор": сроки жизни действительно для хранения, а не для значений. Как правило, для новичков Rust возникают проблемы, когда они не могут понять, почему они не могут сделать что-то вроде:

fn thingy<'a>() -> (Box<i32>, &'a i32) {
    let x = box 1701;
    (x, &x)
}

"Но... Я знаю, что коробка будет продолжать жить, почему компилятор говорит, что это не так?!" Поскольку Rust не может рассуждать о временах жизни кучи и должен прибегать к привязке времени жизни &x к переменной x, а не к распределению кучи, к которому он имеет значение.

Ответ 2

В этом случае проверка указателя не выполняется полностью во время компиляции, а интеллектуальные указатели Rust все еще вводят некоторые служебные данные во время выполнения по сравнению с, скажем, необработанными указателями в C?

Существуют специальные проверки выполнения во время выполнения, которые не могут быть проверены во время компиляции. Они обычно находятся в ящике cell. Но в общем, Rust проверяет все во время компиляции и должен выдавать тот же код, что и в C (если ваш C-код не делает undefined материал).

Или возможно, что компилятор Rust не может принимать правильные решения, и иногда ему просто нужно доверять программе Programmer ™, возможно, используя одну из аннотаций на всю жизнь (те, которые содержат синтаксис < 'lifetime_ident > ). В этом случае, означает ли это, что гарантия безопасности указателя/памяти не равна 100%, и все еще полагается на программиста, написавшего правильный код?

Если компилятор не может принять правильное решение, вы получаете ошибку времени компиляции, сообщающую вам, что компилятор не может проверить, что вы делаете. Это может также ограничить вас тем, что вы знаете, но компилятор этого не делает. В этом случае вы всегда можете пойти в код unsafe. Но, как вы правильно предполагали, компилятор отчасти зависит от программиста.

Компилятор проверяет реализацию функции, чтобы убедиться, что она делает то, что говорит жизнь. Затем, на месте вызова функции, он проверяет, правильно ли программист использует эту функцию. Это похоже на проверку типов. Компилятор С++ проверяет, возвращаете ли вы объект правильного типа. Затем он проверяет на сайте вызова, если возвращаемый объект хранится в переменной правильного типа. Ни в коем случае программист функции не нарушает обещание (кроме случаев, когда используется unsafe, но вы всегда можете позволить компилятору обеспечить, чтобы в вашем проекте не использовался unsafe)

Руста постоянно улучшается. Если компилятор станет более умным, в Rust могут возникнуть дополнительные проблемы.

Другая возможность заключается в том, что указатели Rust не являются "универсальными" или ограничены в некотором смысле, поэтому компилятор может полностью вывести свои свойства во время компиляции, но они не так полезны, как e. г. raw указатели на C или интеллектуальные указатели на С++.

В C есть несколько вещей, которые могут пойти не так:

  • оборванные указатели
  • double free
  • нулевые указатели
  • дикие указатели

Это не происходит в безопасной ржавчине.

  • У вас никогда не может быть указателя, указывающего на объект, который больше не находится в стеке или куче. Это доказано во время компиляции через время жизни.
  • У вас нет ручного управления памятью в Rust. Используйте Box для выделения ваших объектов (похожих, но не равных unique_ptr в С++)
  • Опять же, никакого ручного управления памятью. Box es автоматически освобождает память.
  • В безопасном Rust вы можете создать указатель на любое место, но вы не можете разыменовать его. Любая ссылка, которую вы создаете, всегда привязана к объекту.

В С++ есть несколько вещей, которые могут пойти не так:

  • все, что может пойти не так в C
  • SmartPointers поможет вам не забыть позвонить free. Вы все еще можете создавать оборванные ссылки: auto x = make_unique<int>(42); auto& y = *x; x.reset(); y = 99;

Rust исправляет те:

  • см. выше
  • пока существует y, вы не можете изменять x. Это проверяется во время компиляции и не может быть обойдено с помощью большего количества направлений или структур.

Я где-то читал, что на языке, который содержит указатели, компилятор не может полностью решить во время компиляции, будут ли все указатели правильно использованы и/или действительны (обратитесь к живому объекту) по разным причинам, поскольку это по существу будет решать проблему прекращения.

Rust не доказывает, что все указатели используются правильно. Вы все еще можете писать фиктивные программы. Rust доказывает, что вы не используете недопустимые указатели. Руста доказывает, что у вас никогда не было нулевых указателей. Руста доказывает, что у вас никогда не было двух указателей на один и тот же объект, если все эти указатели не изменяются (const). Rust не позволяет вам писать какую-либо программу (так как это будет включать программы, которые нарушают безопасность памяти). Прямо сейчас Rust все еще мешает вам писать какие-то полезные программы, но есть планы разрешить писать более (юридические) программы в безопасной Rust.

Это неудивительно, потому что в этом случае мы могли бы вывести поведение во время выполнения программы во время компиляции, аналогично тому, что указано в этом связанном вопросе.

Пересмотрите пример в указанном вами вопросе о проблеме остановки:

void foo() {
    if (bar() == 0) this->a = 1;
}

Вышеупомянутый код С++ будет выглядеть одним из двух способов в Rust:

fn foo(&mut self) {
    if self.bar() == 0 {
        self.a = 1;
    }
}

fn foo(&mut self) {
    if bar() == 0 {
        self.a = 1;
    }
}

Для произвольного bar вы не можете это доказать, потому что он может получить доступ к глобальному состоянию. Rust скоро получает функции const, которые могут использоваться для вычисления материала во время компиляции (аналогично constexpr). Если bar равно const, становится тривиальным, чтобы доказать, что self.a задано 1 во время компиляции. Помимо этого, без pure функций или других ограничений содержимого функции, вы никогда не сможете доказать, установлен ли self.a на 1 или нет.

В настоящее время ржавчина не заботится о том, вызван ли ваш код или нет. Он заботится о том, сохраняется ли память self.a во время назначения. self.bar() никогда не может уничтожить self (кроме кода unsafe). Поэтому self.a всегда будет доступен внутри ветки if.

Ответ 3

Большая часть безопасности ссылок на ржавчину гарантируется строгими правилами:

  • Если у вас есть ссылка const (&), вы можете клонировать эту ссылку и передавать ее, но не создавать изменяемую ссылку &mut.
  • Если ссылка на объект mutable (&mut) существует, никакая другая ссылка на этот объект не может существовать.
  • Ссылка не позволяет пережить объект, на который он ссылается, и все функции, обрабатывающие ссылки, должны объявлять, как ссылки из их ввода и вывода связаны, используя аннотации времени жизни (например, 'a).

Таким образом, с точки зрения выразительности мы эффективно более ограничены, чем при использовании простых указателей raw (например, создание структуры графа невозможно с использованием только безопасных ссылок), но эти правила могут быть эффективно полностью проверены во время компиляции.

Тем не менее, по-прежнему можно использовать raw-указатели, но вы должны заключить код, связанный с ними, в блок unsafe { /* ... */ }, сообщая компилятору "Поверьте мне, я знаю, что я здесь делаю". Это то, что делают некоторые специальные интеллектуальные указатели внутри, такие как RefCell, что позволяет вам проверять эти правила во время выполнения, а не на время компиляции, чтобы получить выразительность.