Category: Rust

18
Ноя
2021

🎥 10 лучших каналов YouTube по Rust

Любите пробовать все неизведанное, тогда экспериментальный и мультипарадигменный язык от Mozilla ждет вас. Разберемся с Rust по роликам. Поехали!

Rust считается негласной
альтернат…

18
Ноя
2021

🎥 ТОП-10 каналов по Rust на YouTube

Любите пробовать неизведанное? Тогда мультипарадигменный язык от Mozilla ждет вас. Разберемся с Rust по роликам десяти лучших тематических каналов на YouTube. Читайте наш ТОП.

Rust считается современной альтернативой C/C++, как Go или D, но обладает повышенной безопасностью, скоростью и мощным параллелизмом. Этот язык хорошо подходит для системного программирования, т. к. в нем отлично реализована работа с памятью.

Довольно текста, остальное вам расскажут в видеороликах авторы десяти лучших каналов YouTube, посвященных Rust.

1. ComputerScienceCenter

Русскоязычный канал, на котором вы найдете видеозаписи лекций и
короткие тематические ролики на интересные темы о компьютерных науках и программировании. Материал публикуется для ИТ-специалистов, студентов и
школьников, интересующихся программированием и математикой.

Помимо роликов о программировании на Rust канал публикует уроки по Python, алгоритмизации, анализу изображений, C/C++, Java, Kotlin и прочему. Лекторы разные: в основном нескучные и отвечающие на вопросы аудитории.

Качество звука и видео на уровне.

Больше полезной информации вы найдете на нашем телеграм-канале «Библиотека программиста».

2. dcode

Созданный австралийским фрилансером англоязычный канал пестрит
роликами по веб-разработке и программированию на
Rust.

Уроки в основном проходят на ОС Windows в минималистичном редакторе Atom – это не отвлекает от учебного процесса. Просмотрев курс, вы получите базовые знания, которых будет достаточно для дальнейшего самообучения.

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

3. Be Geek

Небольшой русскоязычный канал с 28+ тыс. подписчиков, в котором автор
разбирает множество полезных
тем, включая и Rust.

Материала именно по Rust немного, канал можно рекомендовать в качестве дополнения к любому из нашей подборки.

Автор не нудит, все по делу, с
примерами в среде. Видео и звук в порядке.

4. Let’sGetRusty

Этот канал полностью посвящен языку программирования Rust. Автор обещает подробно
разбирать основы и всякие фичи, преобразовывая их в интересные обучающие
проекты.

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

На канале мало подписчиков, но каждый ролик набирает
большое количество просмотров. Активность комментариев говорит о наличии у аудитории интереса к происходящему.

5. NEAR

Весьма необычный канал, в котором при помощи Rust происходит разбор работы блокчейна и смежных тем: смарт-контрактов, NFT, криптографии и т. д.

Темы очень интересные и сложные – не ждите базовых вещей. Если вам интересна тема блокчейна, вы попали по адресу.

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

6. Tom McGurl

Канал эксцентричного англоязычного автора, излагающего в интересной
форме обучающий материал, который он вместе с вами будет штудировать из урока в
урок.

Курс начинается с основ Rust и заканчивается структурами и енумами, макросами и всяческими запросами по HTTP. Занятный туториал, пройдя который, вы точно узнаете много нового о языке и сможете его применять в дальнейшем.

Качество звука и видео на уровне.

7. Jon Gjengset

Слоган канала: «Здесь создаются библиотеки и инструменты на языке
программирования Rust!»

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

Английская речь чистая и понятная, качество кода великолепное –
приятно обучаться. На предложенных примерах можно быстро выучить язык. Хороший
канал!

8. Tensor Programming

Это англоязычный канал со множеством роликов про разные языки программирования: Flutter, Elixir, Go, React и наш заветный Rust.

Количество роликов достаточно большое – скучать не придется. Видео различаются по длительности, но каждое из них отлично раскрывает целевую проблему.

Воды в повествовании практически нет, все по делу: канал
ориентирован на зрителя.
Качество звука и видео на уровне.

9. Doug Milford

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

Автор по сложившейся традиции разбирает язык с азов, постепенно переходя к более сложным вещам – дженерикам и 3d-графике.

Речь понятная, зрители регулярно задают автору вопросы, на которые тот отвечает. Видео и звук в порядке.

10. Edureka

Напоследок рассмотрим крупную обучающую площадку, на которой можно
получать знания практически любой направленности.

На платформе есть несколько курсов по Rust разных авторов, которые дополняют друг друга. Здесь вы сможете найти ответы (если они остались после предыдущих источников), на все интересующие вопросы т. к. ресурс действительно монументальный. Рекомендуем!

Заключение

Хотя язык программирования Rust еще не является лидером рынка и пока не заменил C/C++, он имеет массу преимуществ и хорошие перспективы. Не поленитесь и изучите этот необычный язык, если не для работы, то
для общего развития: такая движуха нужна серому веществу, чтобы не скиснуть. Удачи в обучении!

Дополнительные материалы:

22
Окт
2021

🛠 Владение и заимствование в Rust: детально о Lifetime для начинающих и более опытных

Владение и заимствование – ключевая концепция языка Rust и его неоспоримое преимущество. Необходимость освоить эту концепцию является насущной для любого разработчика, а ключом к ней станет Lifetime. Поговорим об этом детально.

Для кого эта статья
Для многих программистов, привыкших к мутабельным состояниям данных в объектно-ориентированой упаковке, Rust становится настоящим откровением. Не удивительно, ведь тут нет сборщика мусора, а безопасность памяти есть. Именно это и делает язык по-настоящему мощным, а его систему типов зубодробительно-сложной. Любой начинающий изучать Rust программист в первую очередь читает Rust Book, который очень точно переведён на русский язык. Это отличный текст: следуя современным требованиям к обучению, он создает впечатление, что все достаточно просто. Сложности начинаются во время практики. Borrow checker заставит ваш мозг всосать весь оставшийся в крови сахар, чтобы понять, почему оно не компилируется.

Значит ли это, что вам придется прочитать сложные тексты на эту тему? Скажем, когда-нибудь – да. Пока эту задачу старается решить наша статья, которая во-первых написана по-русски, а во-вторых содержит некоторую интерпретацию официальной документации, призванную снизить ставшую мемом «Крутую кривую обучения» (Steep learning curve). Для понимания вам нужно хотя бы в общих чертах представлять себе, что такое куча и стек и зачем они нужны. В той или иной степени этот вопрос раскрывают материалы по разным языкам программирования. Также очень важно выучить буквально три пункта правил владения.

В чем проблема?

Это стандартное продающее язык объяснение. Посвященные могут его пропустить.

Глобальная проблема «опасных» языков C и C++ в том, что мы самостоятельно управляем данными в куче, но не имеем способа жестко связать эти данные с переменными в стеке, временем жизни которых управляет компилятор. Значит мы не можем быть уверенными в получении корректных данных по указателю. Получить недействительный указатель очень просто.

Ещё больше проблема усугубляется потребностью писать многопоточный код, который в нескольких экземплярах имеет прямой доступ к общей для всех потоков памяти. Тут важно понимать, что запросы на модификацию данных в памяти могут быть (с точки зрения процессора) не атомарными, а значит может потребоваться несколько инструкций для одной операции. Если же выполняются два потока одновременно, то инструкции процессора, скажем, перемешиваются, и получается бардак, называемый гонкой данных (Data Races), что в свою очередь является неопределенным поведением (Undefined Behaviour, далее UB). Отладка программы в поисках причины UB – чрезвычайно трудоемкий процесс.

Разработчики Rust поняли причины этих проблем и применили практики хороших статических анализаторов кода на C/C++ (но это не точно) для создания языка, который по своей семантике не позволяет программисту отстрелить себе ногу. При этом Rust не создает прослойку между кодом и железом в виде избыточного runtime с garbage collector.

Ссылки

В целом &ссылки/*указатели нужны когда данные: находятся в куче (отдельная тема), или просто ради того, чтобы избежать избыточное копирование данных в стеке.

В Rust есть ровно два типа ссылок (про сырые указатели не говорим):

  • Ссылки на чтение (shared reference): &lnk_name – позволяет только читать данные.
  • Изменяемые (мутабельные) ссылки: &mut lnk_name – позволяет изменять данные.

Ссылки подчиняются двум правилам, называемым правилами заимствования:

  • Ссылки не должны жить дольше чем данные, на которые они ссылаются (Rust Book описывает это так: «все ссылки должны быть действительными»).
  • Мутабельная ссылка должна быть уникальна или, цитируя Rust Book: «в один момент времени может существовать либо одна изменяемая ссылочная переменная, либо любое количество неизменяемых ссылочных переменных». Забегая вперед: на мутабельных ссылках запрещен алиасинг. Англицизм тут нужен для описания конкретного эффекта внезапного изменения значения под ссылкой. Это справедливо не только для области видимости, но и для всей выполняемой ветки программы. Скажем больше, мутабельная ссылка должна быть уникальной и в нескольких потоках, что достигается с помощью Mutex.

Что такое Lifetime?

Вспомним первое правило ссылок: данные должны жить дольше чем ссылки на них, и именно эту гарантию дает язык Rust с помощью Lifetime.

Любая переменная – это память на стеке. Ссылка на другую переменную (или на данные в куче) – тоже переменная в стеке. Если ссылка проживет дольше чем сами данные, то при попытке обратиться через неё (разыменовать), произойдет обращение к освобожденной памяти – классическая ошибка сегментирования.

Lifetime – именованная область программы, ссылки в которой будут действительными. Эти области могут быть очень сложными, поскольку они соотносятся с ветвлением при выполнении программы.
Дадим формальное определение, цитируя Rustomonicon.

Вот прямо так. Не время – область кода. Для нас это абстрактное время, поскольку области кода выполняются последовательно. Lifetime – это как имя самой области, так и метка для ссылки на эту область. Запомним это.

Начнем с того, что время жизни есть не только в Rust. Обычно в C-подобных языках время жизни переменных ограничено функциями (эксперты по стандарту C/C++ поправьте, если что). И в Rust и в С/C++ можно ограничить область видимости переменных синтаксисом блоков “{…}”, однако в Rust блок гарантировано определяет как «долго» переменная проживет на стеке, в отличие от C/C++, где блок служит для семантического ограничения доступа к переменным.

Возьмём простой пример:

        fn main() {
   let num_ref;
   {
       let num = 4;
       num_ref = #
   } // тут переменная num будет уничтожена, выйдя за пределы блока.
   // Однако num_ref ссылается на её адрес,
   // но пока все в порядке, наличие висящей ссылки не проблема
 
   // а вот попытка разыменовать такую ссылку - проблема, т.е. UB
   println!("say number {}", *num_ref);
}
    

При попытке собрать эту программу компилятор выдаст ошибку


Попробуем смоделировать на C что могло получиться, если бы Rust нас не защитил.

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

        #include "stdio.h"
 
int* get_num_ptr() {
 int num = 4;
 return #
}
 
int main() {
 int* num_ref = get_num_ptr();
 printf("say num: %d", *num_ref);
}

    

Скомпилируем и исполним:

        gcc src/bin/lifetime_too_short.c -o lifetime_too_short_c && ./lifetime_too_short_c
    

Получим следующий результат:


Borrow-checker просто не позволяет нам написать подобную программу на Rust. GCC видит проблему, но все равно компилирует код. Наш пример слишком прост, чтобы он пропустил ошибку (clang программу с такой функцией и вовсе убережет от ошибки сегментирования), однако C и C++ никак не защищают от подобного с точки зрения семантики языка. В простых случаях мы можем положиться на компилятор, но нет языковых механик, которые нас уберегут от проблемы в принципе. На помощь придёт стандартная библиотека C++, хотя даже с ней можно отстрелить себе что угодно.

Мутабельные ссылки и модель алиасинга

Этот вопрос мы рассмотрим в контексте однопоточного исполнения без прерываний. Асинхронное программирование выходит за пределы темы.

Возьмем пример из Rust Book:

        fn big_problem() {
 let mut s = String::from("hello");
 let r1 = &s; // no problem
 let r2 = &s; // no problem
 let r3 = &mut s; // BIG PROBLEM
 println!("{}, {}, and {}", r1, r2, r3);
}

    

Видно что это прямое нарушение правил заимствования. Rust Book весьма лаконично описывает что такое мутабельный алиасинг фразой: «Пользователи неизменяемой ссылки не ожидают внезапного изменения значения, на которые она указывает!» Однако на этом примере совершенно не очевидно, почему это плохо.

Давайте разберем другой код, в котором это эффект в глаза не бросается, но покажет, почему мутабельная ссылка должна быть уникальной:

        fn compute(input: &u32, output: &mut u32) {
 if *input > 10 {
     *output = 1;
 }
 if *input > 5 {
     *output *= 2;
 }
 // помните что переменная `output` == `2` если `input > 10`
}

    

При логичном её использовании результат будет ожидаемым – в output будет положено число 2:

        let input = 20;
let mut output = 0;
compute(&input, &mut output); // в `output` положит 2
    

Однако в функции compute() есть один интересный эффект. Что если мы передадим в качестве обоих аргументов ссылки на один и тот же кусок памяти?

        let mut num = 20;
compute(&num, &mut num);
    
В результате после первого блока if значение input изменится и второй блок if просто не сработает, поскольку input тоже изменился и содержит значение менее 5. Это и было названо в Rust Book внезапным изменением значения, поскольку наше внимание приковано к именам переменных и подразумевает, что эти данные разные.

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

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

Rust так делать не позволяет и пытается оптимизировать функцию:

  • сохранить значения в регистрах процессора, подтвердив отсутствие обращений через указатель;
  • убедиться, что память не была перезаписана перед её чтением;
  • убедиться, что память не была прочитана перед записью;
  • позволить переупорядочить чтение и запись, не боясь что значения зависят друг от друга.

Компилятор пожелает сделать код функции, который можно выразить так:

        fn compute_on_stack(input: &u32, output: &mut u32) {
   let mut cache = *output;
   if *input > 10 {
     cache = 1;
   }
   if *input > 5 {
       cache *= 2;
   }
   *output = cache;
}
    
Rust может позволить себе такую оптимизацию, так как точно знает, что input и output указывают на разные значения, потому можно все разыменования заменить на обращение к копии данных в локальном кеше в регистре. Либеральные C и C++ такого позволить себе не могут: вдруг программист рассчитывает на мутабельный алиасинг.

Lifetime как блок и как тип

Как было сказано выше, lifetime – это область кода, в которой живет переменная/данные и эти области компилятор должен разметить, чтобы применить ограничения и выявить проблемы времени жизни. В Rust в night-сборках даже есть специальный синтаксис, который позволяет применять lifetime-метки в блоках явным образом. Этот синтаксис поможет нам «рассахаривать» исходный код в представление, которое выводит компилятор в итоге.

Например, этот код:

        let x = 0;
let y = &x;
let z = &y;
    

можно выразить так:

        'a: {
   let x: i32 = 0;
   'b: {
       let y: &'b i32 = &'b x;
       'c: {
           let z: &'c &'b i32 = &'c y;
       }
   }
}
    

Lifetime всегда указывается через апостроф. Сама метка ничего не говорит нам об относительных размерах lifetime (за исключением ‘static). Для нас имя времени жизни служит только признаком равенства или неравенства, остальное – забота компилятора. Однако вложенность блоков явно показывает, какой lifetime длиннее.

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

        let x = 0;
let z;
let y = &x;
z = y;
    

Передача ссылки за пределы scope заставит Rust вывести более длинные lifetime:

        'a: {
    let x: i32 = 0;
    'b: {
        let z: &'b i32;
        'c: {
            // Для &y используется 'b вместо 'c
            // поскольку эта ссылка передана
            // в переменную из scopa-а 'b
            let y: &'b i32 = &'b x;
            z = y;
        }
    }
  }
    

Функции и структуры могут содержать ссылки, и тогда их сигнатуры приобретут генеричный вид. Имя lifetime в таком случае является неотъемлемым входным параметром дженерика как и типовый параметр. Даже если мы не будем прописывать параметр(ы) lifetime явно, они в любом случае будут выведены компилятором сразу после появления в сигнатуре символа “&”.

        fn as_str(data: &u32) -> &str {
   let s = format!("{}", data);
   &s
}
    

Можно выразить так:

        fn as_str<'a>(data: &'a u32) -> &'a str {
   'b: {
       let s = format!("{}", data);
       return &'a s;
   }
}
    

Вопрос почему функция должна вернуть ссылку с lifetime не меньшим чем входящий, в целом, должен быть уже очевидным (мы разберем его в деталях). Естественно, данную функцию будет правильно написать так:

        fn to_string(data: &u32) -> String {
	format!("{}", data)
}
    
Поскольку String в отличие от ссылки будет не заимствоваться, а перемещаться, по сути это умный указатель: его можно переместить без создания ссылки, а сама строчка будет лежать в куче.

Попробуем «рассахарить» ещё один кусок кода, который отобразит проблему мутабельного алиасинга:

        let mut data = vec![1, 2, 3];
let x = &data[0];
data.push(4);
println!("{}", x);
    
Переменная x – ссылка на часть вектора. Так как вектор – это умный указатель, то при добавлении элемента хранилище в куче может быть перераспределено вне зависимости от нас и все ссылки на старые данные станут недействительны (ошибка сегментирования и/или UB). Однако компилятору не надо ничего знать про то, что x является ссылкой на часть вектора или что такое вектор, и как он оперирует данными в куче. Компилятору достаточно того, что нарушено правило заимствования.

Компилятор выведет следующий код из которого сразу видно проблему с lifetime:

        'a: {
    let mut data: Vec<i32> = vec![1, 2, 3];
    'b: {
        // для переменной x выводится lifetime 'b
        // (так как в этом scope-е происходит
        // обращение к x в println!)
        let x: &'b i32 = Index::index::<'b>(&'b data, 0);
        'c: {
            Vec::push(&'c mut data, 4);
        }
        println!("{}", x);
    }
}
    

Таким образом Rust видит, что x должен прожить время ‘b, чтобы быть напечатанным в println!(), что и отражено в выведенной сигнатуре функции Index::index, которая в качестве входного аргумента дженерика принимает lifetime ‘b. Ниже мы пытаемся заимствовать data с меньшим lifetime, на что и ругается компилятор. Если просто убрать println!(“{}”, x), все будет работать корректно, поскольку висящий указатель/ссылка – не проблема. Проблема – разыменовывание такой ссылки. На самом деле это может быть проблемой, если мы пишем свой деструктор, но об этом поговорим в другой статье.

Замалчивание Lifetime (Elision)

До появления версии Rust 1.0 сигнатуры функций со ссылками приходилось явно аннотировать lifetime. Позднее стало понятно, что компилятор сам может во многом разобраться и вывести все за нас, что сделает программы короче и более читаемыми.

Эта фича и называется замалчиванием lifetime или Elision. Это плюс, только вот правила выведения lifetime обратно в сингратуру функций достаточно жесткие – имеет смысл знать их наизусть. А ещё они не описаны в Rust Book явным образом.

Исправим это досадное недоразумение.

Lifetime может входить в сигнатуры тремя способами:

  • &'a T – ссылка на переменную типа T.
  • &'a mut T – мутабельная ссылка на переменную T.
  • T<'a> – сигнатура типа T в случае, если поля структур или сигнатуры методов трейтов содержат ссылки.

То есть у нас есть входные и выходные lifetime, что было видно и в предыдущем разделе. Поговорим подробнее об их взаимосвязи. Обращаю внимание, что если вам не подходит выведение замалчиваемых (elided) lifetime, вы всегда можете определить их явно.

Правила следующие (их надо запомнить):

  • Каждый замалчиваемый (elided) lifetime в сигнатуре функции уникален, т.е. в функции fn do_something(a: &str, b: &str) у аргументов a и b будут разные lifetime.
  • Если в функции только один ссылочный аргумент с замалчиваемым или явным lifetime, то все выходные lifetime будут ему равны, т.е. fn do_something(s: &’a str) -> (&’a str, &’a str) в нашем случае вернет кортеж с двумя ссылками, но это может быть и структура с более чем одним ссылочным полем
  • Если один из аргументов функции &self или &mut self, то все выходные замалчиваемые lifetime будут выведены равными lifetime ссылки на self.
  • В противном случае Rust не сможет вывести lifetime и заставит вас сделать это явным образом

Давайте теперь посмотрим на примерах:

        fn print(s: &str);                                      // молча да
fn print<'a>(s: &'a str);                               // явно
 
fn debug(lvl: usize, s: &str);                          // молча
fn debug<'a>(lvl: usize, s: &'a str);                   // явно
 
fn substr(s: &str, until: usize) -> &str;               // молча
fn substr<'a>(s: &'a str, until: usize) -> &'a str;     // явно
 
fn get_str() -> &str;                                   // так нельзя
 
fn frob(s: &str, t: &str) -> &str;                      // так нельзя
 
fn get_mut(&mut self) -> &mut T;                        // молча
fn get_mut<'a>(&'a mut self) -> &'a mut T;              // явно
 
fn args<T: ToCStr>(&mut self, args: &[T]) -> &mut Command                  // молча
fn args<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command // явно
 
fn new(buf: &mut [u8]) -> BufWriter;                    // молча
fn new<'a>(buf: &'a mut [u8]) -> BufWriter<'a>          // явно
    

Вывод

Начав читать эту статью, вы хотели избавиться от проблем. На деле все прозаично: надо понять, как в действительности работает владение и заимствование в Rust, а также воспринимать ругающийся компилятор как великое благо. Да, он заставит вас переписать код, зато этот код не выстрелит вам в ногу, руку или в голову 31 декабря в 23:30 и не заставит отлаживать себя в самое неподходящее время. В следующий раз мы заглянем ещё глубже под капот и немножко приоткроем тайну, как компилятор делает магию владения. За пределами этой статьи остались такие важные темы, как автоматическое управление данными в куче и связывание их с переменными на стеке.

22
Окт
2021

🛠 Владение и заимствование в Rust: детально о Lifetime для начинающих и более опытных

Владение и заимствование – ключевая концепция языка Rust и его неоспоримое преимущество. Необходимость освоить эту концепцию является насущной для любого разработчика, а ключом к ней станет Lifetime. Поговорим об этом детально.

Для кого эта статья
Для многих программистов, привыкших к мутабельным состояниям данных в объектно-ориентированой упаковке, Rust становится настоящим откровением. Не удивительно, ведь тут нет сборщика мусора, а безопасность памяти есть. Именно это и делает язык по-настоящему мощным, а его систему типов зубодробительно-сложной. Любой начинающий изучать Rust программист в первую очередь читает Rust Book, который очень точно переведён на русский язык. Это отличный текст: следуя современным требованиям к обучению, он создает впечатление, что все достаточно просто. Сложности начинаются во время практики. Borrow checker заставит ваш мозг всосать весь оставшийся в крови сахар, чтобы понять, почему оно не компилируется.

Значит ли это, что вам придется прочитать сложные тексты на эту тему? Скажем, когда-нибудь – да. Пока эту задачу старается решить наша статья, которая во-первых написана по-русски, а во-вторых содержит некоторую интерпретацию официальной документации, призванную снизить ставшую мемом «Крутую кривую обучения» (Steep learning curve). Для понимания вам нужно хотя бы в общих чертах представлять себе, что такое куча и стек и зачем они нужны. В той или иной степени этот вопрос раскрывают материалы по разным языкам программирования. Также очень важно выучить буквально три пункта правил владения.

В чем проблема?

Это стандартное продающее язык объяснение. Посвященные могут его пропустить.

Глобальная проблема «опасных» языков C и C++ в том, что мы самостоятельно управляем данными в куче, но не имеем способа жестко связать эти данные с переменными в стеке, временем жизни которых управляет компилятор. Значит мы не можем быть уверенными в получении корректных данных по указателю. Получить недействительный указатель очень просто.

Ещё больше проблема усугубляется потребностью писать многопоточный код, который в нескольких экземплярах имеет прямой доступ к общей для всех потоков памяти. Тут важно понимать, что запросы на модификацию данных в памяти могут быть (с точки зрения процессора) не атомарными, а значит может потребоваться несколько инструкций для одной операции. Если же выполняются два потока одновременно, то инструкции процессора, скажем, перемешиваются, и получается бардак, называемый гонкой данных (Data Races), что в свою очередь является неопределенным поведением (Undefined Behaviour, далее UB). Отладка программы в поисках причины UB – чрезвычайно трудоемкий процесс.

Разработчики Rust поняли причины этих проблем и применили практики хороших статических анализаторов кода на C/C++ (но это не точно) для создания языка, который по своей семантике не позволяет программисту отстрелить себе ногу. При этом Rust не создает прослойку между кодом и железом в виде избыточного runtime с garbage collector.

Ссылки

В целом &ссылки/*указатели нужны когда данные: находятся в куче (отдельная тема), или просто ради того, чтобы избежать избыточное копирование данных в стеке.

В Rust есть ровно два типа ссылок (про сырые указатели не говорим):

  • Ссылки на чтение (shared reference): &lnk_name – позволяет только читать данные.
  • Изменяемые (мутабельные) ссылки: &mut lnk_name – позволяет изменять данные.

Ссылки подчиняются двум правилам, называемым правилами заимствования:

  • Ссылки не должны жить дольше чем данные, на которые они ссылаются (Rust Book описывает это так: «все ссылки должны быть действительными»).
  • Мутабельная ссылка должна быть уникальна или, цитируя Rust Book: «в один момент времени может существовать либо одна изменяемая ссылочная переменная, либо любое количество неизменяемых ссылочных переменных». Забегая вперед: на мутабельных ссылках запрещен алиасинг. Англицизм тут нужен для описания конкретного эффекта внезапного изменения значения под ссылкой. Это справедливо не только для области видимости, но и для всей выполняемой ветки программы. Скажем больше, мутабельная ссылка должна быть уникальной и в нескольких потоках, что достигается с помощью Mutex.

Что такое Lifetime?

Вспомним первое правило ссылок: данные должны жить дольше чем ссылки на них, и именно эту гарантию дает язык Rust с помощью Lifetime.

Любая переменная – это память на стеке. Ссылка на другую переменную (или на данные в куче) – тоже переменная в стеке. Если ссылка проживет дольше чем сами данные, то при попытке обратиться через неё (разыменовать), произойдет обращение к освобожденной памяти – классическая ошибка сегментирования.

Lifetime – именованная область программы, ссылки в которой будут действительными. Эти области могут быть очень сложными, поскольку они соотносятся с ветвлением при выполнении программы.
Дадим формальное определение, цитируя Rustomonicon.

Вот прямо так. Не время – область кода. Для нас это абстрактное время, поскольку области кода выполняются последовательно. Lifetime – это как имя самой области, так и метка для ссылки на эту область. Запомним это.

Начнем с того, что время жизни есть не только в Rust. Обычно в C-подобных языках время жизни переменных ограничено функциями (эксперты по стандарту C/C++ поправьте, если что). И в Rust и в С/C++ можно ограничить область видимости переменных синтаксисом блоков “{…}”, однако в Rust блок гарантировано определяет как «долго» переменная проживет на стеке, в отличие от C/C++, где блок служит для семантического ограничения доступа к переменным.

Возьмём простой пример:

        fn main() {
   let num_ref;
   {
       let num = 4;
       num_ref = &num;
   } // тут переменная num будет уничтожена, выйдя за пределы блока.
   // Однако num_ref ссылается на её адрес,
   // но пока все в порядке, наличие висящей ссылки не проблема
 
   // а вот попытка разыменовать такую ссылку - проблема, т.е. UB
   println!("say number {}", *num_ref);
}
    

При попытке собрать эту программу компилятор выдаст ошибку


Попробуем смоделировать на C что могло получиться, если бы Rust нас не защитил.

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

        #include "stdio.h"
 
int* get_num_ptr() {
 int num = 4;
 return &num;
}
 
int main() {
 int* num_ref = get_num_ptr();
 printf("say num: %d", *num_ref);
}

    

Скомпилируем и исполним:

        gcc src/bin/lifetime_too_short.c -o lifetime_too_short_c && ./lifetime_too_short_c
    

Получим следующий результат:


Borrow-checker просто не позволяет нам написать подобную программу на Rust. GCC видит проблему, но все равно компилирует код. Наш пример слишком прост, чтобы он пропустил ошибку (clang программу с такой функцией и вовсе убережет от ошибки сегментирования), однако C и C++ никак не защищают от подобного с точки зрения семантики языка. В простых случаях мы можем положиться на компилятор, но нет языковых механик, которые нас уберегут от проблемы в принципе. На помощь придёт стандартная библиотека C++, хотя даже с ней можно отстрелить себе что угодно.

Мутабельные ссылки и модель алиасинга

Этот вопрос мы рассмотрим в контексте однопоточного исполнения без прерываний. Асинхронное программирование выходит за пределы темы.

Возьмем пример из Rust Book:

        fn big_problem() {
 let mut s = String::from("hello");
 let r1 = &s; // no problem
 let r2 = &s; // no problem
 let r3 = &mut s; // BIG PROBLEM
 println!("{}, {}, and {}", r1, r2, r3);
}

    

Видно что это прямое нарушение правил заимствования. Rust Book весьма лаконично описывает что такое мутабельный алиасинг фразой: «Пользователи неизменяемой ссылки не ожидают внезапного изменения значения, на которые она указывает!» Однако на этом примере совершенно не очевидно, почему это плохо.

Давайте разберем другой код, в котором это эффект в глаза не бросается, но покажет, почему мутабельная ссылка должна быть уникальной:

        fn compute(input: &u32, output: &mut u32) {
 if *input > 10 {
     *output = 1;
 }
 if *input > 5 {
     *output *= 2;
 }
 // помните что переменная `output` == `2` если `input > 10`
}

    

При логичном её использовании результат будет ожидаемым – в output будет положено число 2:

        let input = 20;
let mut output = 0;
compute(&input, &mut output); // в `output` положит 2
    

Однако в функции compute() есть один интересный эффект. Что если мы передадим в качестве обоих аргументов ссылки на один и тот же кусок памяти?

        let mut num = 20;
compute(&num, &mut num);
    
В результате после первого блока if значение input изменится и второй блок if просто не сработает, поскольку input тоже изменился и содержит значение менее 5. Это и было названо в Rust Book внезапным изменением значения, поскольку наше внимание приковано к именам переменных и подразумевает, что эти данные разные.

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

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

Rust так делать не позволяет и пытается оптимизировать функцию:

  • сохранить значения в регистрах процессора, подтвердив отсутствие обращений через указатель;
  • убедиться, что память не была перезаписана перед её чтением;
  • убедиться, что память не была прочитана перед записью;
  • позволить переупорядочить чтение и запись, не боясь что значения зависят друг от друга.

Компилятор пожелает сделать код функции, который можно выразить так:

        fn compute_on_stack(input: &u32, output: &mut u32) {
   let mut cache = *output;
   if *input > 10 {
     cache = 1;
   }
   if *input > 5 {
       cache *= 2;
   }
   *output = cache;
}
    
Rust может позволить себе такую оптимизацию, так как точно знает, что input и output указывают на разные значения, потому можно все разыменования заменить на обращение к копии данных в локальном кеше в регистре. Либеральные C и C++ такого позволить себе не могут: вдруг программист рассчитывает на мутабельный алиасинг.

Lifetime как блок и как тип

Как было сказано выше, lifetime – это область кода, в которой живет переменная/данные и эти области компилятор должен разметить, чтобы применить ограничения и выявить проблемы времени жизни. В Rust в night-сборках даже есть специальный синтаксис, который позволяет применять lifetime-метки в блоках явным образом. Этот синтаксис поможет нам «рассахаривать» исходный код в представление, которое выводит компилятор в итоге.

Например, этот код:

        let x = 0;
let y = &x;
let z = &y;
    

можно выразить так:

        'a: {
   let x: i32 = 0;
   'b: {
       let y: &'b i32 = &'b x;
       'c: {
           let z: &'c &'b i32 = &'c y;
       }
   }
}
    

Lifetime всегда указывается через апостроф. Сама метка ничего не говорит нам об относительных размерах lifetime (за исключением ‘static). Для нас имя времени жизни служит только признаком равенства или неравенства, остальное – забота компилятора. Однако вложенность блоков явно показывает, какой lifetime длиннее.

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

        let x = 0;
let z;
let y = &x;
z = y;
    

Передача ссылки за пределы scope заставит Rust вывести более длинные lifetime:

        'a: {
    let x: i32 = 0;
    'b: {
        let z: &'b i32;
        'c: {
            // Для &y используется 'b вместо 'c
            // поскольку эта ссылка передана
            // в переменную из scopa-а 'b
            let y: &'b i32 = &'b x;
            z = y;
        }
    }
  }
    

Функции и структуры могут содержать ссылки, и тогда их сигнатуры приобретут генеричный вид. Имя lifetime в таком случае является неотъемлемым входным параметром дженерика как и типовый параметр. Даже если мы не будем прописывать параметр(ы) lifetime явно, они в любом случае будут выведены компилятором сразу после появления в сигнатуре символа “&”.

        fn as_str(data: &u32) -> &str {
   let s = format!("{}", data);
   &s
}
    

Можно выразить так:

        fn as_str<'a>(data: &'a u32) -> &'a str {
   'b: {
       let s = format!("{}", data);
       return &'a s;
   }
}
    

Вопрос почему функция должна вернуть ссылку с lifetime не меньшим чем входящий, в целом, должен быть уже очевидным (мы разберем его в деталях). Естественно, данную функцию будет правильно написать так:

        fn to_string(data: &u32) -> String {
	format!("{}", data)
}
    
Поскольку String в отличие от ссылки будет не заимствоваться, а перемещаться, по сути это умный указатель: его можно переместить без создания ссылки, а сама строчка будет лежать в куче.

Попробуем «рассахарить» ещё один кусок кода, который отобразит проблему мутабельного алиасинга:

        let mut data = vec![1, 2, 3];
let x = &data[0];
data.push(4);
println!("{}", x);
    
Переменная x – ссылка на часть вектора. Так как вектор – это умный указатель, то при добавлении элемента хранилище в куче может быть перераспределено вне зависимости от нас и все ссылки на старые данные станут недействительны (ошибка сегментирования и/или UB). Однако компилятору не надо ничего знать про то, что x является ссылкой на часть вектора или что такое вектор, и как он оперирует данными в куче. Компилятору достаточно того, что нарушено правило заимствования.

Компилятор выведет следующий код из которого сразу видно проблему с lifetime:

        'a: {
    let mut data: Vec<i32> = vec![1, 2, 3];
    'b: {
        // для переменной x выводится lifetime 'b
        // (так как в этом scope-е происходит
        // обращение к x в println!)
        let x: &'b i32 = Index::index::<'b>(&'b data, 0);
        'c: {
            Vec::push(&'c mut data, 4);
        }
        println!("{}", x);
    }
}
    

Таким образом Rust видит, что x должен прожить время ‘b, чтобы быть напечатанным в println!(), что и отражено в выведенной сигнатуре функции Index::index, которая в качестве входного аргумента дженерика принимает lifetime ‘b. Ниже мы пытаемся заимствовать data с меньшим lifetime, на что и ругается компилятор. Если просто убрать println!(“{}”, x), все будет работать корректно, поскольку висящий указатель/ссылка – не проблема. Проблема – разыменовывание такой ссылки. На самом деле это может быть проблемой, если мы пишем свой деструктор, но об этом поговорим в другой статье.

Замалчивание Lifetime (Elision)

До появления версии Rust 1.0 сигнатуры функций со ссылками приходилось явно аннотировать lifetime. Позднее стало понятно, что компилятор сам может во многом разобраться и вывести все за нас, что сделает программы короче и более читаемыми.

Эта фича и называется замалчиванием lifetime или Elision. Это плюс, только вот правила выведения lifetime обратно в сингратуру функций достаточно жесткие – имеет смысл знать их наизусть. А ещё они не описаны в Rust Book явным образом.

Исправим это досадное недоразумение.

Lifetime может входить в сигнатуры тремя способами:

  • &'a T – ссылка на переменную типа T.
  • &'a mut T – мутабельная ссылка на переменную T.
  • T<'a> – сигнатура типа T в случае, если поля структур или сигнатуры методов трейтов содержат ссылки.

То есть у нас есть входные и выходные lifetime, что было видно и в предыдущем разделе. Поговорим подробнее об их взаимосвязи. Обращаю внимание, что если вам не подходит выведение замалчиваемых (elided) lifetime, вы всегда можете определить их явно.

Правила следующие (их надо запомнить):

  • Каждый замалчиваемый (elided) lifetime в сигнатуре функции уникален, т.е. в функции fn do_something(a: &str, b: &str) у аргументов a и b будут разные lifetime.
  • Если в функции только один ссылочный аргумент с замалчиваемым или явным lifetime, то все выходные lifetime будут ему равны, т.е. fn do_something(s: &’a str) -> (&’a str, &’a str) в нашем случае вернет кортеж с двумя ссылками, но это может быть и структура с более чем одним ссылочным полем
  • Если один из аргументов функции &self или &mut self, то все выходные замалчиваемые lifetime будут выведены равными lifetime ссылки на self.
  • В противном случае Rust не сможет вывести lifetime и заставит вас сделать это явным образом

Давайте теперь посмотрим на примерах:

        fn print(s: &str);                                      // молча да
fn print<'a>(s: &'a str);                               // явно
 
fn debug(lvl: usize, s: &str);                          // молча
fn debug<'a>(lvl: usize, s: &'a str);                   // явно
 
fn substr(s: &str, until: usize) -> &str;               // молча
fn substr<'a>(s: &'a str, until: usize) -> &'a str;     // явно
 
fn get_str() -> &str;                                   // так нельзя
 
fn frob(s: &str, t: &str) -> &str;                      // так нельзя
 
fn get_mut(&mut self) -> &mut T;                        // молча
fn get_mut<'a>(&'a mut self) -> &'a mut T;              // явно
 
fn args<T: ToCStr>(&mut self, args: &[T]) -> &mut Command                  // молча
fn args<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command // явно
 
fn new(buf: &mut [u8]) -> BufWriter;                    // молча
fn new<'a>(buf: &'a mut [u8]) -> BufWriter<'a>          // явно
    

Вывод

Начав читать эту статью, вы хотели избавиться от проблем. На деле все прозаично: надо понять, как в действительности работает владение и заимствование в Rust, а также воспринимать ругающийся компилятор как великое благо. Да, он заставит вас переписать код, зато этот код не выстрелит вам в ногу, руку или в голову 31 декабря в 23:30 и не заставит отлаживать себя в самое неподходящее время. В следующий раз мы заглянем ещё глубже под капот и немножко приоткроем тайну, как компилятор делает магию владения. За пределами этой статьи остались такие важные темы, как автоматическое управление данными в куче и связывание их с переменными на стеке.

20
Дек
2020

Прием пакетов с сервера Minecraft

Я установил простой сервер майнкрафта последней версии и написал программу на Rust, которая может зайти на этот сервер (вот алгоритм). Соединение через TCP (через UDP сервер не слушает) Однако у меня возникла проблема с парсингом входящих …

17
Дек
2020

В чем проблема моей попытки перевода кода с java на rust?

Я пытаюсь сделать "рукопожатие" с майнкрафт сервером на Rust. Я основываюсь на этом ответе и этом вики.
Код из ответа на java работает, однако любые запросы, посылаемые на сервер через UDPSocket на Rust выдают ошибку "Удален…

16
Дек
2020

Есть ли какие либо библиотеки/способы, чтобы программа, которую я хочу написать могла зайти на сервер в майнкрафт?

Я хочу написать бота для майнкрафта. Мне нужна какая то библиотека или гайд для того, чтобы моя программа могла задать себе ник (как в TLauncher), зайти на сервер майнкрафта (по айпи) и управлять персонажем на нем. Очень желательно чтобы в…

27
Июн
2020

Подойдет ли Rust и Java для программирования железа?

Ребят, нужен ваш совет. Я в этом деле чайник.
Хотел бы заняться робототехникой и нейронными сетями. Подойдет ли Rust и Java для программирования железа?
И посоветуйте микроконтреллеры для проффесионального программирования роботов

28
Ноя
2018

Конференция RustRush 2018

15–16 декабря в Москве пройдёт первая в России международная конференция для разработчиков на Rust — RustRush 2018. Что в программе? Основные темы конференции — веб, блокчейн, высокая производительность и системное программирование. На конференцию прие…

28
Ноя
2018

Опубликованы результаты опроса пользователей языка Rust за 2018 год

Создатели языка программирования Rust опубликовали результаты опроса разработчиков, использующих язык в своей деятельности. Подобный опрос проводится уже 3 года подряд, но в 2018 году к английской версии вопросов добавили вариа…