Rust для чего используется
Зачем нужен язык программирования Rust?
Rust набирает популярность, но при этом многие до сих пор не понимают его ценности и функций. Мы расскажем про основные преимущества языка программирования Rust.
Что общего у Rust и других языков?
В перечисленные определения сложно поверить, это выглядит нереалистичным заявлением, так как ранее все языки выбирали одну из сторон: надёжность или производительность.
Уклон в сторону надёжности лучшим образом демонстрирует язык Haskell, который имеет компилируемую природу и обеспечивает высокие показатели безопасности. Всё, что можно компилировать, будет исправно работать. Главный недостаток – это низкая производительность, сложно представить проект, требующий высокой скорости написанный на Haskell.
Rust вобрал в себя лучшие характеристики C++ и Haskell, а также смог сохранить достаточную практичность и функциональность от остальных конкурентов.
В чем же прелесть языка Rust?
Волшебные характеристики Rust стали доступными при помощи основ компилирования и информации о сущности владельца (owner), о программисте, который только временно отлаживает или занял проект (mutable borrow), а также об обычном зрителе (immutable borrow).
При программировании на Java или же C++, приходится удерживать данную информацию в памяти, хотя вид данных несколько отличается. В Rust это реализуется с помощью языковых конструкций, данная информация облегчает компилятору задачу установления личности и правильности подбора модели поведения. Благодаря компилятору можно гарантировать устранение потенциальных и частых проблем в ходе выполнения кода.
Этому языку необходим несколько отличающийся подход. Несмотря на непривычность, алгоритм достаточно очевидный и эффективный. Теперь мы определимся с основами языка, которые способны завести в тупик при начале изучения:
Язык Rust и почему его надо съесть
Итак, хотим представить вашему вниманию недавнего именинника (15 мая 2016 года ему исполнился год) — Rust. Это универсальный язык программирования, разрабатываемый компанией Mozilla, три основных принципа которого: скорость, безопасность и эргономика. Сами создатели нескромно считают его одним наиболее вероятных наследников C/C++. Согласно опросу портала StackOverflow, именно Rust сегодня наиболее любимый разработчиками язык. Итак, давайте подробнее разбираться в том, что же он из себя представляет.
Rust для новичка
Не хочется никого обманывать, поэтому далее ответственное заявление: Rust довольно сложен для обучения. Во-первых, это обусловлено молодостью языка и, как следствие, малым количеством литературы. Во-вторых, выучить его человеку далёкому от программирования, возможно будет даже проще, чем знакомому с другими языками. Так, например, готового IT-специалиста будет сильно раздражать необходимость прописывать малейшие операции, а отсутствие как такового наследования в языке и просто поставит в тупик.
Впрочем, Rust быстро развивается (каждые 6 недель выходит новый релиз), сообщество растёт, а найти информацию в интернете уже не составляет никакого труда.
Как изучать
Почти всё необходимое вы можете найти на официальном сайте. Кроме того, сообщество последователей Rust весьма обширно и доброжелательно, поэтому за советом всегда можно обратиться в IRC (есть русский раздел) и официальный форум. Кроме того, понемногу стали появляться книги, в том числе электронные. Пока трудно оценить их качество, но факт такой имеется.
Для тех, кто прошёл начальную стадию знакомства, на GiHub можно найти уйму полезного материала, в том числе RFC и коммиты. Кроме того, вы можете посетить лично или хотя бы посмотреть интернет-трансляцию одной из конференций по Rust, намеченных на вторую половину года. Вот календарь:
Ну а чтобы познакомиться с теми, кто считает Rust своим призванием, и заодно спросить у них все премудрости, можете связаться с организаторами и участниками встреч поклонников языка в Москве.
Особенности
Немного дублируя то, о чём было сказано ранее, выделим основные плюсы и минусы языка Rust.
Плюсы:
Минусы:
На самом деле к отличиям, вроде замены наследования на способности, быстро привыкаешь. Как только глаза привыкли, руки набились, Rust превращается во вполне рабочий язык, проще и функциональнее C++, но уступающий по «красивости» многим другим языкам программирования. Фактически же главное отличие Rust от конкурентов и предшественников — именно скорость и безопасность.
Востребованность
На сегодняшний день Rust популярен сред разработчиков игр, графики и операционных систем. Однако по понятным причинам количество стационарных мест, где бы требовались узкопрофильные знатоки Rust, в мире и уже тем более в России крайне невелико. Тем не менее, пока не видно никаких предпосылок, что язык канет в лету, больше похоже на планомерный захват мира. А значит хорошие навыки использования Rust в будущем помогут найти высокооплачиваемую интересную работу как в нашей стране, так и за рубежом.
Еще востребованного: профессия «Веб-разработчик».
Итак, хотим представить вашему вниманию недавнего именинника (15 мая 2016 года ему исполнился год) — Rust. Это универсальный язык программирования, разрабатываемый компанией Mozilla, три основных принципа которого: скорость, безопасность и эргономика. Сами создатели нескромно считают его одним наиболее вероятных наследников C/C++. Согласно опросу портала StackOverflow, именно Rust сегодня наиболее любимый разработчиками язык. Итак, давайте подробнее разбираться в том, что же он из себя представляет.
Rust для новичка
Не хочется никого обманывать, поэтому далее ответственное заявление: Rust довольно сложен для обучения. Во-первых, это обусловлено молодостью языка и, как следствие, малым количеством литературы. Во-вторых, выучить его человеку далёкому от программирования, возможно будет даже проще, чем знакомому с другими языками. Так, например, готового IT-специалиста будет сильно раздражать необходимость прописывать малейшие операции, а отсутствие как такового наследования в языке и просто поставит в тупик.
Впрочем, Rust быстро развивается (каждые 6 недель выходит новый релиз), сообщество растёт, а найти информацию в интернете уже не составляет никакого труда.
Как изучать
Почти всё необходимое вы можете найти на официальном сайте. Кроме того, сообщество последователей Rust весьма обширно и доброжелательно, поэтому за советом всегда можно обратиться в IRC (есть русский раздел) и официальный форум. Кроме того, понемногу стали появляться книги, в том числе электронные. Пока трудно оценить их качество, но факт такой имеется.
Для тех, кто прошёл начальную стадию знакомства, на GiHub можно найти уйму полезного материала, в том числе RFC и коммиты. Кроме того, вы можете посетить лично или хотя бы посмотреть интернет-трансляцию одной из конференций по Rust, намеченных на вторую половину года. Вот календарь:
Ну а чтобы познакомиться с теми, кто считает Rust своим призванием, и заодно спросить у них все премудрости, можете связаться с организаторами и участниками встреч поклонников языка в Москве.
Особенности
Немного дублируя то, о чём было сказано ранее, выделим основные плюсы и минусы языка Rust.
Плюсы:
Минусы:
На самом деле к отличиям, вроде замены наследования на способности, быстро привыкаешь. Как только глаза привыкли, руки набились, Rust превращается во вполне рабочий язык, проще и функциональнее C++, но уступающий по «красивости» многим другим языкам программирования. Фактически же главное отличие Rust от конкурентов и предшественников — именно скорость и безопасность.
Востребованность
На сегодняшний день Rust популярен сред разработчиков игр, графики и операционных систем. Однако по понятным причинам количество стационарных мест, где бы требовались узкопрофильные знатоки Rust, в мире и уже тем более в России крайне невелико. Тем не менее, пока не видно никаких предпосылок, что язык канет в лету, больше похоже на планомерный захват мира. А значит хорошие навыки использования Rust в будущем помогут найти высокооплачиваемую интересную работу как в нашей стране, так и за рубежом.
Еще востребованного: профессия «Веб-разработчик».
Краткая история Rust: от хобби до самого популярного ЯП по данным StackOverflow
Rust — это язык системного программирования, создатели которого уделили внимание трем вещам: параллелизму, скорости и безопасности. И хотя Rust считается молодым языком программирования — его первая стабильная версия вышла в 2015 году — он разрабатывается уже более десяти лет.
Сегодня мы бы хотели заглянуть в прошлое и рассказать историю языка Rust, показать, как изменились его функции и возможности за время разработки и привести конкретные примеры внедрения этого ЯП на практике.
Личный проект (2006–2010)
Технология из прошлого, которая призвана спасти будущее от самого себя
— Грэйдон Хор (Graydon Hoare), разработчик Rust
Это одна из цитат Грэйдона Хора, которую озвучил Стив Клабник (Steve Klabnik) из команды разработчиков проекта Rust во время своей презентации на конференции ACM в 2016 году (слайды к презентации вы можете найти по ссылке, а для того, чтобы перемещаться между слайдами, используйте стрелки на клавиатуре). Эти слова хорошо отражают тот факт, что Rust — не революционный язык, имеющий передовые функции. Он просто включает в себя множество рабочих методов из «старых» языков (в том числе C++), повышая их безопасность.
Занимаясь языком, Грэйдон установил определенные правила. Он отмечал, что в первую очередь необходимо уделять внимание семантике языка, а работа над синтаксисом — это последнее дело. Поэтому в ранней реализации Rust ключевые слова были не длиннее пяти символов — язык был «кратким» и использовал такие операторы, как log, ret и fn.
Например, первый код на Rust, который увидел свет, выглядел так:
Как отмечает Стив Клабник, со временем это ограничение было снято: часть ключевых слов «удлинили», например, ret превратился в return, а часть заменили совсем. Для сравнения, в современной реализации языка вывод строки «Привет, мир!» выглядит так:
Также за время эволюции часть концепций и ключевых слов языка была убрана. Когда над языком работал Грэйдон, Rust был объектно-ориентированным и использовал оператор obj для описания объектов. Сейчас как таковое ООП языком не поддерживается, но Rust дает возможность реализовать многие его понятия с помощью абстракций.
Rust также работал с функциями параметрического полиморфизма. Концепции обобщенного программирования в языке сохранились и сейчас (оформление кода вы можете найти в этом документе), однако десять лет назад для обозначения типа параметров использовались квадратные скобки:
Самостоятельно над Rust Грэйдон работал на протяжении четырех лет. За это время ему удалось воплотить в жизнь примерно 90% задуманных функций (часть из которых имела довольно грубую реализацию). Среда для выполнения кода была завершена на 70%. Всего за это время Хор написал 38 тыс. строк кода для компилятора на OCaml.
Переход к Mozilla (2010–2012)
Я не считаю, что у языка должны быть какие-то главные особенности.
Он должен состоять из набора понятных и надежных модулей, которые хорошо «работают» в комбинации друг с другом
— Грэйдон Хор (Graydon Hoare), разработчик Rust
По прошествии четырех лет, Грэйдон решил показать свой прототип менеджеру в Mozilla. В компании проявили интерес к проекту, поскольку искали инструмент для перестройки стека браузера на более простых технологиях, чем C++. Поэтому в компании создали команду (во главе с Грэйдоном) для работы над Rust, который стал основой браузерного движка Servo.
Тогда движок Mozilla не мог полноценно работать с мультиядерными системами, поскольку имел однопоточные схемы обработки контента. Например, однопоточными были функции формирования содержимого окна и запуска JavaScript. Rust позволил разделить код рендеринга на мини-задачи, выполняемые параллельно и экономящие ресурсы центрального процессора.
Кроме ускорения работы за счет распараллеливания операций, Rust позволил повысить защищенность браузера. На тот момент Firefox был реализован на C++ и содержал 4,5 млн строк кода. C++ — это «точный» язык программирования, требующий повышенного внимания к деталям, поэтому ошибки программистов могли приводить к возникновению серьезных уязвимостей. Задачей Rust стало снижение влияния человеческого фактора с помощью компилятора.
В 2010 году разработчики языка сменили используемый до этого компилятор OCaml на компилятор, написанный на Rust. В 2011 году Грэйдон опубликовал сообщение о том, что компилятор сумел успешно «собрать» сам себя, а в 2012 команда Rust объявила о релизе альфа-версии компилятора — его документация была не полной, а скорость создания билда оказалась далека от идеальной, однако он уже поддерживал большинство функций языка и кросс-компиляцию.
Годы typesystem (2012–2014)
Наша целевая аудитория — «разочарованные разработчики C++»
В этот момент Грэйдон отошел от работы над Rust и переключился на другие проекты. Как рассказывает Стив Клабник, после этого система управления стала более «распределенной». Была сформирована федеративная структура, в которой за изменения, вносимые в разные части проекта, отвечала отдельная группа разработчиков.
Команда продолжила расти, и в ней стали появляться люди, разбирающиеся в сложных системах типов. Поэтому началось активное развитие typesystem, и все больше аспектов языка выносились в библиотеки.
Например, на ранних этапах Rust был реализован «сборщик мусора» (GC — Garbage Collector), который Грэйдон внедрил для повышения защищенности памяти. Однако потом разработчики пришли к выводу, что они могут обеспечить тот же уровень надежности с помощью системы типов, и от GC отказались.
Это решение также сказалось на системе указателей, используемой в Rust. До удаления «сборщика мусора» в языке было три основных указателя:
Период с 2012 по 2014 год — это время, когда сообщество Rust начало обретать форму. В нем образовалось три больших «лагеря»: пользователи C++, пользователи скриптовых языков и функциональные программисты. Их экспертиза повлияла на язык — постепенно он стал сочетать в себе парадигмы функционального и процедурного программирования.
В марте 2014 года также был сформирован RFC-процесс, который использовался для представления значимых изменений в языке. Решение строилось по образу и подобию Python PEP, и сейчас в нем сделано 3 тыс. коммитов. Причем в RFC попадает любое изменение, даже вносимое разработчиками. По правилам команды Rust, никто не может вносить крупные изменения, не обсудив решение с сообществом.
Релиз (2015)
Мы не знаем наверняка, что из этого получится
В начале 2015 года была выпущена версия Rust 1.0 Alpha. В ней стабилизировали ядро языка, развили систему макросов, и, наконец, закрепили за целочисленными типами int и uint названия isize и usize. В начале второго квартала того же года Rust 1.0 перешел в бету — к этому моменту репозиторий crates.io имел 1700 крэйтов (структурная единица компиляции), а количество скачиваний из репозитория превысило один миллион.
В мае 2015 года состоялся официальный релиз — Rust 1.0. Это ознаменовало начало стабильности. С этого момента все вносимые изменения должны были иметь обратную совместимость, что позволило использовать Rust в реальных проектах. Он начал находить применение в таких сферах, как game dev, веб-разработка и разработка операционных систем.
Переход в продакшн (май 2016)
Если язык хорош лишь в чем-то одном, то это — провал
В 2015 году площадка StackOverflow провела опрос среди разработчиков, в котором их попросили отметить, с какими языками программирования они работали и с какими хотели бы познакомиться. Тогда Rust занял третью строчку рейтинга. Однако годом позднее он переместился на первое место — 79% пользователей изъявили желание продолжить работу с ним.
Один из резидентов Hacker News назвал главными достоинствами языка прозрачность и простоту документации. Другие пользователи также отмечали открытость и доброжелательность Rust-сообщества, которое всегда готово помочь с изучением особенностей ЯП.
При этом многие разработчики решают продолжить работу с этим языком из-за его механизмов безопасности. Как сказал один из пользователей Reddit: «Программирование на Rust — это как паркур со страховкой и в защите; иногда это выглядит странно, но вы можете делать многие трюки, не боясь сломать себе что-нибудь».
С момента релиза стабильной версии Rust начался период его полноценного использования в продакшн. Одной из первых компаний, которые применили Rust в своем проекте, стала Mozilla. Часть «внутренностей» Firefox 45 для Linux были переписаны на Rust, а начиная с версии Firefox 47, Rust-код присутствует и в версии для Windows. Их Project Quantum, анонсированный в октябре 2016 года, также имеет в своем составе компоненты Servo.
Rust используется и в Dropbox — на этом ЯП написано ядро их продукта. Компания Dropbox создала свое новое облачное хранилище Magic Pocket, в которое перенесла информацию с Amazon S3. Изначально оно было реализовано на языке Go, но при больших нагрузках проблемой становилось высокое потребление памяти и низкая предсказуемость поведения кода на Go. Для решения этих проблем был частично задействован Rust.
В прошлом году использовать Rust для обработки пакетов JavaScript начали в npm. Rust помог исключить задержки в системе, работающей с 350 миллионами пакетов в день. Специалист службы поддержки npm Эшли Уильямс (Ashley Williams) рассказывала об опыте использования Rust на конференции RustFest в Украине. Видео вы найдете по ссылке.
Rust также используем и мы в компании Bitfury. На этом языке программирования реализован наш фреймворк для создания блокчейнов Exonum. Впервые мы представили платформу на конференции RustFest 2017, где показали её возможности и провели воркшоп, на котором продемонстрировали работу сервиса по созданию криптовалют (краткое руководство о том, как создать криптовалюту на Exonum вы можете найти здесь).
Реализация на Rust оказалась кстати при работе со смарт-контрактами. Благодаря этому умные контракты Exonum имеют большую производительность, чем контракты Ethereum (или Fabric). Код платформы полностью открыт и лежит в репозитории проекта на GitHub.
Rust также находит применение в сфере информационной безопасности. Такие проекты как Tor уже переносят часть кода на Rust.
В целом, сегодня Rust в своих продуктах используют 105 компаний. Полный их список (в котором также отмечена и Bitfury Group) можно найти на странице Friends of Rust на официальном сайте. И количество компаний, создающих программные продукты на Rust, постоянно увеличивается, чему разработчики языка очень рады.
Rust — молодой и дерзкий язык программирования
Говорят, что это одновременно C++ и Haskell.
Первая версия языка Rust появилась в 2010 году, и он сразу занял третью строчку в списке любимых языков разработчиков на StackOverflow. Год спустя Rust возглавил этот список и держался там несколько лет. Давайте посмотрим, почему этот язык стал таким популярным, в чём его особенности и почему вокруг него много споров.
В чём идея языка Rust
Автору языка нравилась скорость работы и всемогущество языка C++ и надёжность Haskell. Он поставил перед собой задачу совместить оба этих подхода в одном языке, и за несколько лет он собрал первую версию языка Rust.
Rust позиционируется как компилируемый системный мультипарадигмальный язык высокого уровня. Сейчас поясним, что это значит.
👉 Компилируемый язык означает, что готовая программа — это отдельный файл, который можно запустить на любом компьютере с нужной операционной системой. Для запуска не нужно устанавливать среду разработки и компилятор, достаточно, чтобы скомпилированная версия подходила к вашему компьютеру.
👉 Системный — это когда на языке пишут программы для работы системы в целом. Это могут быть операционные системы, драйверы и служебные утилиты. Обычные программы тоже можно писать на Rust — от калькулятора до системы управления базами данных. Системный язык позволяет писать очень быстрые программы, которые используют все возможности железа.
👉 Мультипарадигмальный значит, что в языке сочетаются несколько парадигм программирования. В случае Rust это ООП, процедурное и функциональное программирование. Причём, ООП в Rust пришло из C++, а функциональное — из Haskell. Программист может сам выбирать, в каком стиле он будет писать код, или совмещать разные подходы в разных элементах программы.
Синтаксис и код
За основу синтаксиса в Rust взят синтаксис из C и C++.Например, классический «Привет, мир!» на Rust выглядит так:
fn main() <
println!(«Hello, world!»);
>
Если вы знакомы с подобным синтаксисом, то сможете быстро начать писать и на Rust. Другое дело, что в Rust есть свои особенности:
let x = if new_game() < 4 >
else if reload() < 3 >
else
Последнее разберём подробно. При такой записи переменная x будет равна четырём, если функция new_game() вернёт значение true. Если этого не случится, компилятор вызовет функцию reload() и проверит, что получилось. Если true, то x примет значение 3, а если и это не сработает — то x станет равным 0.
Ещё в Rust есть сравнение переменной с образцом. В зависимости от того, с каким образцом совпало значение переменной, выполнится та или иная функция:
Главная особенность программ на Rust
Несмотря на синтаксис, похожий на C, главную особенность программ на Rust разработчики взяли из Haskell, и звучит она так:
Если программа на Rust скомпилировалась и не упала во время запуска, то она будет работать до тех пор, пока вы сами её не остановите.
Это значит, что программы на Rust почти так же надёжны, как программы на Haskell. Почти — потому что если программист использует «небезопасный» блок unsafe, который даёт ему прямой доступ к памяти, то в теории это иногда может привести к сбоям. Но даже с такими блоками Rust старается справляться сам и падает только в безнадёжных случаях.
Плюсы и минусы языка
Когда язык совмещает в себе несколько разных подходов из других языков, он получает большинство преимуществ каждого из них:
Минусы в основном связаны со скоростью развития языка. Так как Rust развивается очень быстро, то часто бывает так, что код из старой версии не работает в новой версии. Ещё к минусам можно добавить:
Что написано на Rust
Чаще всего Rust используют в тех проектах, где нужна стабильность и надёжность при высокой нагрузке и общее быстродействие программы.
На практике Rust подходит для разработки ОС, веб-серверов, системных программ мониторинга, веб-движков, а также для создания масштабируемых частей фронтенда и бэкенда. Например, вот самые известные проекты, где Rust был основным языком программирования:
Ключевые возможности Rust
Rust — новый язык программирования, разрабатываемый корпорацией Mozilla. Главная цель разработчиков — создание безопасного практичного языка для параллельных вычислений. Первая версия языка была написана Грэйдоном Хором в 2006 году, а в 2009 году к разработке подключилась Mozilla. С тех пор изменения претерпел и сам компилятор, изначально написанный на OCaml: он был успешно переписан на Rust с использованием LLVM в качестве back-end.
Основным продуктом, разрабатываемым на Rust, является новый веб-движок Servo, разработка которого также ведется Mozilla. В 2013 году к разработке Rust и Servo присоединилась корпорация Samsung Electronics, при активном участии которой код движка Servo был портирован на ARM архитектуру. Поддержка языка столь серьезными игроками IT индустрии не может не радовать и дает надежду на его дальнейшее активное развитие и совершенствование.
Целевая аудитория
Сначала я планировал написать вводную статью, которая бы рассматривала язык с самых основ, начиная с объявления переменных и заканчивая функциональными возможностями и особенностями модели памяти. С одной стороны, подобный подход позволил бы охватить как можно большую целевую аудиторию, с другой стороны, статья с похожим содержанием была бы неинтересна людям, имеющим неплохой опыт работы с языками типа C++ или Java, и не позволила бы включить в нее более глубокий анализ основных особенностей Rust, то есть именно того, что делает его привлекательным.
Поэтому я решил не описывать детально такие базовые вещи, как создание переменных, циклы, функции, замыкания и все остальное, что понятно из кода. Основная масса не совсем очевидных особенностей будет описана по мере необходимости, в процессе разбора основных возможностей Rust. В итоге статья посвящена описанию двух из трех основных возможностей языка: безопасной работе с памятью и написанию параллельных приложений. К сожалению, на момент написания статьи сетевая подсистема находилась в активной разработке, что делало включение описания работы с ней в статью совершенно бессмысленным.
Терминология
По большому счету, это одна из двух-трех доступных статей, посвященных Rust, на русском языке, поэтому какой-либо устоявшейся русской терминологии нет и мне приходится брать наиболее подходящие эквиваленты, уже знакомые по другим языкам программирования. Для удобства дальнейшего чтения документации и статей на английском языке при первом появлении русскоязычного термина в скобках приводится английский эквивалент.
Наибольшее количество проблем вызвали термины Box и Pointer. По своим свойствам что Box, что Pointer больше всего напоминают умные указатели из C++, поэтому я решил использовать термин «указатели». Таким образом, Owned boxes превратились в Уникальные указатели, а Borrowed pointers во Временные указатели.
Работа с памятью
Принципы работы с памятью – это первая из ключевых возможностей Rust, которая выгодно отличает этот язык как от языков с полным доступом к памяти (типа C++), так и от языков с полным контролем за памятью со стороны GC (типа Java). Дело в том, что, с одной стороны, Rust предоставляет разработчику возможность контролировать, где размещать данные, вводя разделение по типам указателей и обеспечивая контроль за их использованием на этапе компиляции. C другой стороны, механизм подсчета ссылок, который в окончательной версии языка будет заменен полноценным GC, обеспечивает автоматическое управление ресурсами.
Использование стека
Так, код (1) разместит объект типа Point на стеке задачи, в которой будет вызван. При копировании подобного объекта (2) будет скопирован не указатель на объект x, а вся структура типа Point.
Для информации: переменные
Как можно увидеть из примера выше, ключевое слово let используется в Rust для создания переменных. По умолчанию все переменные константные и для создания изменяемой переменной необходимо добавлять ключевое слово mut. Таким образом, создание изменяемой переменной типа Point могло бы выглядеть следующим образом let mut x = Point
Крайне важно помнить при работе с переменными, что константными оказываются именно данные, и за попытками изменить их «обманом» пристально следит компилятор.
Так, вполне можно (1) создать изменяемую переменную, указывающую на константные данные, но вот попытка (2) изменить сами данные закончится ошибкой на этапе компиляции. А вот изменение значения переменной, хранящей адрес константного объекта Point и созданной ранее, является допустимым (3).
Разделяемые указатели
Разделяемые указатели используются в качестве указателей на объекты, располагающиеся в локальной куче задачи. У каждой задачи есть собственная локальная куча, и указатели на расположенные в ней объекты никогда не могут быть переданы за ее пределы. Для создания разделяемых указателей используется унарный оператор @
В отличие от стековых объектов, при копировании копируется исключительно указатель, а не данные. Именно из этого свойства и пошло название данного типа указателей, так как поведение их очень похоже на shared_ptr из языка C++.
Также необходимо отметить тот факт, что невозможно создать структуру, содержащую указатель на собственный тип (классический пример – односвязный список). Для того чтобы компилятор разрешил подобную конструкцию, необходимо обернуть указатель в тип Option (1).
Уникальные указатели
Уникальные указатели реализуют семантику владения, благодаря чему объект может адресовать только один уникальный указатель. C++ разработчики наверняка найдут общие черты между уникальными указателями Rust и классом unique_ptr из STL.
Присвоение (1) указателю new_p указателя p приводит к тому, что new_p начинает указывать на созданный ранее объект типа Point, а указатель p деинициализируется. В случае попытки работы с деинициализированными переменными (2) компилятор генерирует ошибку use of moved value и предлагает сделать копию переменной вместо присвоения указателя с последующей деинициализацией исходного.
Благодаря явному созданию копии (1), new_p указывает на копию созданного ранее объекта типа Point, а указатель p не изменяется. Для того, что бы к структуре Point можно было применить метод clone, структура должна быть объявлена с использованием атрибута #[deriving(Clone)].
Временные указатели
Временные указатели – указатели которые могут указывать на объект, размещенный в любом из возможных типов памяти: стеке, локальном или хипе обмена, а также на внутренний член любой структуры данных. На физическом уровне временные указатели представляют собой типичные Си указатели и, как следствие, не отслеживаются сборщиком мусора и не привносят никаких дополнительных накладных расходов. В то же время, их основным отличием от Си указателей являются дополнительные проверки, проводимые на этапе компиляции для гарантии возможности безопасного использования. Для создания временных указателей используется унарный оператор &
Объект типа Point был создан (1) на стеке и временный указатель был сохранен в on_the_stack. Данный код аналогичен следующему:
Типы, отличные от стековых, приводятся к временным указателям автоматически, без использования оператора взятия адреса, что позволяет упростить написание функций (1), если тип указателя не имеет значения.
А теперь небольшая иллюстрация того, как можно получить временный указатель на внутренний элемент структуры данных.
Контроль времени жизни временных указателей довольно объемная и не совсем устоявшаяся тема. При желании с ней можно подробно ознакомится в статье Rust Borrowed Pointers Tutorial и Lifetime Notation.
Разыменование указателей
Для доступа к значениям, адресованным при помощи указателей, необходимо проводить операцию разыменования (Dereferencing pointers). При доступе к полям структурированных объектов разыменование производится автоматически.
Преобразование между указателями
Практически сразу после начала работы с Rust возникает вопрос: «Как преобразовать объект, адресуемый при помощи уникального указателя, к разделяемому или наоборот?» Ответ на данный вопрос краткий и поначалу несколько обескураживающий: никак. Если хорошо подумать над ним, то становится очевидно, что каких-либо средств подобного преобразования нет и быть не может, так как объекты находятся в разных кучах и подчиняются разным правилам, у объектов могут быть графы зависимостей, автоматическое отслеживание которых также затруднительно. Поэтому, при необходимости преобразования между указателями, которое является ни чем иным как перемещением объектов между кучами, необходимо создавать копии объектов, для чего можно воспользоваться сериализацией.
Задачи
Вторая ключевая возможность Rust – написание параллельных приложений. В плане возможностей для написания параллельных приложений Rust напоминает Erlang с его моделью акторов и обменом сообщениями между ними и Limbo с его каналами. При этом разработчику предоставляется возможность выбирать: хочет ли он копировать память при отправке сообщения или просто передать владение объектом. А при совместной работе нескольких задач с одним и тем же объектом можно легко организовать доступ один-писатель-много-читателей. Для создаваемых задач есть возможность выбрать наиболее подходящий планировщик или написать собственный.
Для информации: do-синтаксис
Перед тем как перейти к описанию работы с задачами, желательно ознакомиться с do-синтаксисом, который используется в Rust для упрощения работы с функциями высшего порядка. В качестве примера можно взять функцию each, передающую указатель (1) на каждый из элементов массива в функцию op.
При помощи функции each, используя do-синтаксис (1), можно вывести на экран каждый из элементов массива, не забывая о том, что в лямбду будет передано не значение, а указатель, который необходимо разыменовать (2) для доступа к данным:
Так как do-синтаксис является синтаксическим сахаром, то запись ниже эквивалентна записи с использованием do-синтаксиса.
Запуск задачи на выполнение
Создать и выполнить задачу в Rust очень просто. Код, относящийся к работе с задачами, сосредоточен в модуле std::task, а простейшим способом создания и старта задачи является вызов функции spawn из этого модуля.
Функция spawn принимает замыкание в качестве аргумента и запускает его на выполнение в виде задачи (не стоит забывать о том, что задачи в Rust реализованы поверх зеленых потоков). Для того чтобы получить текущую задачу, в рамках которой выполняется код, можно воспользоваться методом get_task() из модуля task. С учетом того, что в рамках задачи выполняются замыкания, не сложно предположить 3 способа запустить задачу на выполнение: передав адрес функции (1), создав замыкание «на месте» (2) или, что более верно с точки зрения идеологии языка, воспользовавшись do-синтаксисом (3).
Взаимодействие между задачами
Обмен сообщениями на низком уровне
Самым широко используемым на данный момент способом взаимодействия между задачами является модуль std::comm. Код из std::comm хорошо отлажен, неплохо задокументирован и довольно прост в использовании. Основой механизма обмена сообщениями std::comm являются потоки, манипуляция с которыми происходит посредством каналов и портов. Поток представляет собой однонаправленный механизм связи, в котором порт используется для отправки сообщения, а канал – для приема отправленной информации. Простейший пример использования потока выглядит следующим образом:
Отдельного внимания заслуживает класс шаблонного параметра Send, который означает возможность передачи при помощи потока только объектов, поддерживающих пересылку за пределы текущей задачи.
Для получения данных из потока можно воспользоваться функцией recv(), которая либо вернет данные, либо заблокирует задачу до их появления. Глядя на пример, приведенный выше, закрадывается подозрение, что он совершенно бесполезен, так как какого-то практического смысла в отправке сообщений при помощи потоков в рамках одной задачи нет. Так что стоит перейти к более практичным вещам, таким как использование потоков для передачи информации между задачами.
Первое, на что стоит обратить внимание при работе с потоками, это необходимость передавать значения, адресуемые уникальными указателями, а функция from_fn() (1) как раз создает такой массив. Так как поток является однонаправленным, то для передачи запроса (2) и получения ответа (3) понадобятся два потока. При помощи функции recv() данные считываются из потока (4), а при отсутствии таковых поток заблокирует задачу до их появления. Для отправки результата клиенту используется функция send() (5), принадлежащая не серверному, а клиентскому потоку; аналогичным образом необходимо поступить с данными для отправки серверной задаче: они записываются (6) при помощи функции send(), относящейся к серверному порту. В самом конце результат, переданный серверной задачей, считывается (7) из клиентского потока.
Таким образом, для отправки сообщений серверу и приема сообщений на стороне сервера используется поток server_chan, server_port. В силу однонаправленности потока, для получения результата вычислений сервера был создан клиентский поток, состоящий из пары client_chan, client_port.
Совместное использование потока
Хотя поток является однонаправленным механизмом передачи данных, это не приводит к необходимости создавать новый поток для каждого из желающих отправить данные, так как существует механизм, обеспечивающий работу в режиме «один-получатель-много-отправителей».
Для этого, как и для схемы «один-читатель-один-писатель», необходимо создать серверный (2) и клиентский (3) потоки и запустить серверную задачу (3). Логика серверной задачи предельно проста: считать (5) данные из серверного канала, переданные клиентом (9), вывести сообщение о получении запроса на экран и отправить результирующее количество полученных запросов print_hello (5) в клиентский поток. Так как писателей несколько, то необходимо внести изменения в тип серверного порта, преобразовав (7) его к SharedChan вместо Chan, и для каждого из писателей создать уникальную копию порта (8) посредствам метода clone(). Дальнейшая работа с портом ничем не отличается от предыдущего примера: метод send() используется для отправки данных серверу (9) с той лишь разницей, что теперь данные отправляются из нескольких задач одновременно.
Кроме иллюстрации метода совместной работы с потоком, данный пример показывает способ отправки нескольких разных типов сообщений при помощи одного потока. Так как тип передаваемых потоком данных задается на этапе компиляции, для передачи данных разных типов необходимо либо воспользоваться серриализацией с последующей передачей бинарных данных (данный метод описан ниже в разделе «Пересылка объектов»), либо передавать перечисление (1). По своим свойствам перечисления в Rust похожи на объединения из языка C или тип Variant, в той или иной форме присутствующий почти во всех высокоуровневых языках программирования.
Пересылка объектов
В тех случаях, когда необходимость пересылать значения, адресуемые исключительно уникальными указателями, становится проблемой, на помощь приходит модуль flatpipes. Данный модуль позволяет отправлять и принимать любые бинарные данные в виде массива или объекты, поддерживающие сериализацию.
Как видно из примера, работать с flatpipes предельно просто. Структура, объекты которой будут передаваться посредством flatpipes, должна быть объявлена сериализуемой (1) и десериализуемой (2). Создание flatpipes (3) технически ничем не отличается от создания обычных потоков, так же как прием (4) и отправка (5) сообщений при помощи канала и порта. Главным же отличием flatpipes от потока является создание глубокой копии объекта на отправляющей стороне и построение нового объекта на принимающей стороне. Благодаря такому подходу, накладные расходы при работе с flatpipes, по сравнению с обычными потоками, возрастают, но возможности по пересылке данных между задачами увеличиваются.
Высокоуровневая абстракция обмена сообщениями
В большинстве приведенных выше примеров создаются два потока: один для отправки данных на сервер, второй для получения данных с сервера. Подобный подход не привносит какой-то ощутимой пользы да и просто замусоривает код. В связи с этим был создан модуль extra::comm, являющийся высокоуровневой абстракцией над std::comm и содержащий в себе DuplexStream, позволяющий организовать двунаправленное общение в рамках одного потока. Само собой, если заглянуть в исходный код DuplexStream, станет ясно, что это не более чем удобная надстройка над парой стандартных потоков.
При работе с DuplexStream создается (1) единственная пара из двух двунаправленных потоков, оба из которых могут использоваться как для отправки, так и для получения сообщений. Объект server захватывается контекстом задачи и используется для получения (2) и отправки (3) сообщений в задаче сервера, а объект client – в задаче клиента (4,5). Принцип работы с DuplexStream ничем не отличается от работы с обычными потоками, но позволяет сократить количество вспомогательных объектов.
Модуль Arc
Совместное использование уникальных указателей с доступом только на чтение
Теперь объект должен относиться не только к классу Send, как это было в случае с потоком, но еще и к классу Freeze, что гарантирует отсутствие каких бы то ни было изменяемых полей или указателей на изменяемые поля внутри объекта T (такие объекты в Rust носят название deeply immutable objects).
Пусть в данном примере нет работы с потоками, но он вполне достаточен для иллюстрации работы с Arc, так как наглядно демонстрирует основной функционал этого модуля – возможность одновременно обращаться к одним и тем же данным из разных задач. Так, для совместного использования одного и того же массива, обернутого в Arc (1), надо создать клон Arc обертки (2), что сделает возможным обращение к данным как из новой (3), так и из основной (4) задач.
R/W доступ к уникальным указателям
Модуль RWArc вызывает у меня двоякие эмоции. С одной стороны, благодаря RWArc можно реализовать широко распространенную и хорошо известную большинству разработчиков концепцию “много читателей один писатель”, что, наверное, хорошо, так как концепция широко известна. С другой стороны, совместный доступ к памяти, причем не RO доступ, который был описан чуть ранее, а RW доступ, чреват проблемами с взаимоблокировками, от которых Rust как раз и должен защитить разработчиков. Лично для себя я пришел к следующему выводу: о модуле знать надо, но использовать его без крайней необходимости не стоит.
В приведенном выше примере создается (1) массив, обернутый в RWArc, благодаря чему к нему можно обращаться как на чтение (4), так и на запись (6). Кардинальное отличие примера работы с RWArc от всех предыдущих примеров – использование замыканий в функциях read() (3) и write() (5) в качестве аргумента. Чтение и запись данных, обернутых в RWArc, можно производить только в этих функциях. И, как обычно, необходимо создать копию (2) объекта для доступа к нему из замыкания, так как в противном случае оригинал станет недоступным.
Как такое вообще возможно?
Да, именно такой вопрос возникает после того, как узнаешь о том, что модули Arc и RWArc присутствуют в Rust. На первый взгляд они противоречат концепции работы с памятью в Rust в целом, и принципам работы уникальных указателей в частности. Не являясь создателем или разработчиком данного языка, я могу только лишь рассказать о том, благодаря чему подобное поведение возможно. В составе языка Rust имеется ключевое слово unsafe, позволяющее писать код, работающий с памятью напрямую, вызывать такие небезопасные с точки зрения управления памятью функции, как malloc, free, и использовать адресную арифметику. Именно эта возможность используется для обхода встроенной в Rust защиты памяти и обеспечения совместного доступа к одному и тому же объекту. Весь код, относящийся к данной функциональности, помечен как «COMPLETELY UNSAFE» и не должен использоваться конечными пользователями напрямую.
Вместо заключения
Хотя прямо сейчас язык Rust не пригоден для промышленного использования, на мой взгляд, он обладает большим потенциалом. Очень может быть, что через несколько лет Rust сможет составить конкуренцию таким замечательным языкам-динозаврам, как C и C++, как минимум в областях, связанных с написанием сетевых и параллельных приложений. В крайнем случае, я очень на это надеюсь.
Что касается статьи, то считать ее законченной, скорее всего, нельзя: во-первых, синтаксис языка наверняка претерпит еще ряд изменений, а, во-вторых, должна завершиться работа над третей из ключевых возможностей языка – поддержкой сетевых взаимодействий. Как только эта функциональность придет в более или менее завершенное состояние, я обязательно о ней напишу.