Как я вкатился в Rust (и зачем)

 Публичный пост
6 июля 2023  3497

Внимание

В посте будет код. На Rust. Вам может стать некомфортно. Вам может стать интересно. Я предупредил :)

И зачем?

Мы должны писать надёжный код, который не падает, не течет, не делает data race и не ломается, поскольку ошибки в
нашем коде критичнее некуда. Просто представьте, что Cloud API удалило не тот кластер. Поэтому я изучаю новый
инструмент, который позволит писать более безопасный код. Изучаю сам, чтоб не отвлекать инженеров от текущих задач

© менеджер во мне

А на самом деле зачем? :)

Однажды простого инженера Ваню повысили до лида, а потом ещё раз повысили, до менеджера. И стало Ване некогда писать код
и чинить продакшен, но инженер в нём окончательно ещё не умер. И стал Ваня искать, куда бы применить инженера, чтоб и
коллегам не мешать и интересно было. И нашёл Rust.

Процесс вкатывания

Картинка, отражающая реальность

ну вы поняли
ну вы поняли

Сначала новичку показывают Hello World и рассказывают про borrowing, и выглядит всё достаточно просто, ну примерно вот
так.

fn main() {
    let name = std::env::args().
        nth(1).
        unwrap_or_else(|| "%username%".to_owned());
    println!("Welcome to Rust, {name}!");
}

Потом, почти без предупреждения, это превращается во что-то такое.

impl<T> Stream for BroadcastStream<T>
    where T: 'static + Clone + Send
{
    type Item = Result<T, BroadcastStreamRecvError>;
    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>)
                 -> Poll<Option<Self::Item>> {
        let (result, rx) = ready!(self.inner.poll(cx));
        self.inner.set(make_future(rx));
        match result {
            Ok(item) => Poll::Ready(Some(Ok(item))),
            Err(RecvError::Closed) => Poll::Ready(None),
            Err(RecvError::Lagged(n)) => {
                Poll::Ready(Some(Err(BroadcastStreamRecvError::Lagged(n))))
            }
        }
    }
}

А потом ты сам пишешь такой код и не видишь в этом ничего странного.

А секрет прост - простой советский три кита Rust.

Кит первый - ownership & borrowing

Если ты что-то взял - отдай

© объяснение принципов ownership

У любой переменной в Rust есть владелец. Владение заканчивается в момент передачи переменной другому владельцу.
Обращаться к тому, чем мы уже не владеем - нельзя.
Например, вот так - нельзя.

fn main() {
    let name = "name".to_owned(); // владелец name - текущая функция
    call_fn(name); // мы отдали владение name другой функции
    println!("my name = {name}"); // тут будет ошибка и оно не скомпилируется
}

fn call_fn(name: String) -> String {
    println!("call_fn name: {name}");
}

Но если нам нужно - мы можем отдать владение назад, и тогда будет снова можно.

fn main() {
    let name = "name".to_owned(); // владелец name - текущая функция
    let name = call_fn(name); // мы отдали владение name другой функции
    // и получили его обратно как результат
    println!("my name = {name}"); // тут будет все ок
}

fn call_fn(name: String) -> String {
    println!("call_fn name: {name}");
    name // это просто return без return
}

Или мы можем одолжить (borrow) - то есть передать не владение, а ссылку на владение. И тогда тоже будет можно.

fn main() {
    let name = "name".to_owned(); // владелец name - текущая функция
    call_fn(&name); // мы одолжили нашу name другой функции
    println!("my name = {name}"); // тут все хорошо
}

fn call_fn(name: &String) { // call_fn не владеет name
    println!("call_fn name: {name}");
}

Или мы можем передать копию - и тогда наша переменная останется при нас.

fn main() {
    let name = "name".to_owned(); // владелец name - текущая функция
    call_fn(name.clone()); // мы отдали владение копией
    println!("my name = {name}"); // тут будет всё ок
}

fn call_fn(name: String) { // call_fn владеет своей копией name
    println!("call_fn name: {name}");
}
  • Работать с переменными по ссылкам неудобно - многие функции требуют владения переменной.
  • Работать с копиями - удобно, но дорого с точки зрения памяти и производительности.
  • Передавать владение туда-сюда-обратно - удобно, но требует сильно много думать и планировать все свои движения.

Кто хочет понять суть - реализуйте LinkedList на Rust.

Кит второй - Traits

Trait - это средство языка описать поведение (и ограничения) для типа.
Например, есть Send и Sync, на которые постоянно ругается компилятор при многопоточном программировании:

  • Чтоб отправить объект типа X в другой поток, он должен иметь trait Send
  • Чтоб поделиться объектом типа X с другим потоком, он должен иметь trait Sync

Как это работает?

  • Авторы библиотек для многопоточной обработки данных пишут требования: хочу Send и Sync на вход
  • Разные механизмы безопасной работы с данными (Mutex, например) говорят: я Send или я Sync
  • Программист вынужден использовать эти механизмы, иначе у него проект не компилируется
  • Data race не происходит

Можно ли это обойти?

Да, можно, но это надо прям специально хотеть. Не получится случайно передать в другой поток
недействительную ссылку.

Поначалу бесит, потом становится проще жить: если код скомпилировался, там data race происходить не будет.

Кит третий - async/await/Future

То, зачем 99% программистов используют этот язык - писать асинхронный код. Позволяет выжимать 100500 соединений на
одном ядре и делать blazing fast™.
Почти полностью состоит из бессонных ночей и ошибок компиляции из-за того,
что type X doesn't implement Send/Sync/Unpin и 'a should outlive 'static.

Писать асинхронный код так, чтоб не делать clone() всего - это действительно очень сложно.

Но, к счастью, почти всё уже написано за нас. Мы говорим асинхронный код - и подразумеваем tokio.
Так что, если упростить, то вот так можно написать echo сервер, который держит > 50 000 rps:

use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::spawn;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let listener = TcpListener::bind(("localhost", 1234)).await?;
    loop {
        let (mut client, addr) = listener.accept().await?;
        println!("[+] Incoming connection from {addr}");
        spawn(async move {
            let mut data = Vec::with_capacity(256);
            loop {
                let x = client.read_u8().await?;
                data.push(x);
                if x == b'\n' || x == b'\r' || data.len() == 256 {
                    client.write_all(&data).await?;
                    break;
                }
            }
            Ok::<(), anyhow::Error>(())
        });
    }
}

Прогресс вкатывания

Сначала всё очень сложно. Потом я написал первый LinkedList.
Потом я написал свой currrrl, который быстрее актуальной сборки curl под мой
дистрибутив.
Быстрее аж за 20-50 ms, но быстрее же!
И поддерживает все опции, которые даёт copy as cURL в Dev Mode моего браузера.

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

Итоги

Инженер во мне жив и пишет код по ночам. Менеджер во мне позволяет команде делать сложные задачи и не забирает у них
работу.
Все довольны, вроде?

А на самом деле

Да правда неплохо всё, хотя я понял кое-что про себя. Но это уже совсем другая история.

А если я тоже хочу?

  • Learn Rust by Building Real Applications курс для тех, кто любит курсы
  • Rust for Rusteans - настольная книга для тех, кто любит настольные книги
  • Lets get rusty - канал на ютубе для тех, кто любит каналы на ютубе
  • Rust by Example - официальная дока для тех, кто (ну вы поняли)
  • // TODO: add more examples from comments

Outro

Кто первый напишет, что vas3k надо переписать на Rust - тот настоящий джедай!

63 комментария 👇

Рекомендую курс по Rust от Алексея Кладова

Автор когда-то поддержку Rust для IDE от JetBrains. Сейчас он делает rust-analyzer с которым все становится лучше.

Еще с изучением Rust мне очень помог Github Copilot и книжка Rust in Action 🙂

@r13v, класс, запинил :)

  Развернуть 1 комментарий

Vas3k надо переписать на haskell

  Развернуть 1 комментарий

@urtow, одобрямс

  Развернуть 1 комментарий
🕵️ Юзер скрыл свои комментарии от публичного просмотра...

Пробовал ли ты чужой код поддерживать на расте? Неопытным глазом выглядит очень write-only

  Развернуть 1 комментарий

@Stenopolz, да, собственно, я понял, что вкатился, когда начал в чужой код вносить правки и находить там баги глазами.

  Развернуть 1 комментарий

@annmuor, мне сейчас код на расте кажется роднее и понятнее чем на пайтоне. Когда вижу пайтон хочется добавить ресалт, конвеерну обработку, энам...

  Развернуть 1 комментарий

Кстати, ты получил одобрение Rust™ Foundation на использование Rust™?

  Развернуть 1 комментарий

@fCCbp7RUf09pxYkr, они там откатились немножко :)

  Развернуть 1 комментарий

@annmuor, все ещё жду когда язык переименуют в awslang

  Развернуть 1 комментарий

Имхо rust сейчас язык #2 для data engineers.
Валидация схем? Pydantic v2.
Есть pandas, но на одой машине скучно? Давайте заменим его на Ray. У Ray dataset бекенд на polars.
А может без этого всего? datafusion.
Мне кажется я ждал когда Rust полетит последние 5 лет и сейчас вкатываться уже прям пора. ТС спасибо и лучи поддержки.

  Развернуть 1 комментарий

Rust уже не модно, переходите на Zig

  Развернуть 1 комментарий

@fCCbp7RUf09pxYkr, совршенно разные языки в своей цели. Цель Rust - сделать ошибки по памяти невероятными в safe rust, цель zig - писать код как будто ты пишешь на Си в 1989 :)

  Развернуть 1 комментарий
🕵️ Юзер скрыл свои комментарии от публичного просмотра...

@annmuor,

как будто ты пишешь на Си в 1989

Дайте два

  Развернуть 1 комментарий

😱 Комментарий удален его автором...

  Развернуть 1 комментарий
🕵️ Юзер скрыл свои комментарии от публичного просмотра...

call_fn(&name); // мы одолжили нашу name другой функции

Ооо, родимый perl

  Развернуть 1 комментарий

@glader, Perl это немного дедушка Rust. Rust частично вдохновлялся Ruby, ну а автор Ruby вдохновлялся Perl.

  Развернуть 1 комментарий

@glader, а что тут от Перла?

  Развернуть 1 комментарий
🕵️ Юзер скрыл свои комментарии от публичного просмотра...

Поначалу бесит, потом становится проще жить: если код скомпилировался, там data race происходить не будет.

А можно раскрыть мысль? Пока субъективно выглядит как то что я могу добавить к чему угодно нужный трейт, а затем передавать это "что-то" куда угодно. При этом сам объект может быть небезопасным для использования из разных тредов. Т.е. нужно просто не добавлять трейт пока не реализуешь какие-то функции для работы из разных потоков. А если у тебя в них будет ошибка, то получим всё тот же race condition.

Есть ошибка в моих рассуждениях?

  Развернуть 1 комментарий

@grbit, можешь, но не можешь.
Ты должен явно указать, что ты добавляешь этот трейт, причём это можно сделать только если unsafe rust разрешен в проекте ( Send и Sync - unsafe трейты )
Когда ты это делаешь - unsafe impl Send for X{} - ты говоришь компилятору, что ты берешь на себя ответственность за все действия X.
Ты не можешь сделать этого случайно, ты явно декларируешь, что ты подумал и подготовился.

В проектах, где unsafe запрещен, ты так сделать не можешь.
А в конечных проектах ( не библиотеках ) он чаще всего запрещен.

  Развернуть 1 комментарий

@annmuor, спасибо за пояснения.

Я понимал что случайно я этого сделать не могу.
Но теперь я не понимаю как же вообще многопоточно программировать тогда если добавлять трейт Send/Sync считается unsafe...

  Развернуть 1 комментарий

@grbit, Используя типы, которые уже сделаны разработчиками языка и библиотек и хорошо проверены: Arc/Mutex/RWLock/Atomic*

  Развернуть 1 комментарий

@annmuor, нет, ну мьютекс я положим передать могу, а что с тем же линкд лист? Что если мне по ссылке к хэш таблице доступ нужен?

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

  Развернуть 1 комментарий

@grbit, не, ты не понял идею. Все эти типы - это контейнеры.
Для однопоточного приложения допустим у тебя T = HashMap<String, String>.
Для многопоточного - Arc<Mutex<HashMap<String,String>>>

Для thread-unsafe типа только комбинация этих контейнеров даст тебе необходимые Send + Sync. Ты можешь выбрать другой Arc или другой Mutex ( или написать свой ), но ты не можешь просто сделать unsafe impl Send for HashMap<String,String>{}.

Пример с LinkedList сложен, тебе придётся очень много кода писать, поскольку LinkedList - это самореференсная структура данных в Rust, тебе придётся использовать Pin/Unpin внутри и вообще это задачка на хорошо так погрузиться в механизмы языка.

Но в общем виде - ты пишешь тип, оборачиваешь его в нужные тебе контейнеры для получения нужной тебе комбинации Send + Sync ( Mutex не везде нужен, кстати ) и язык не дает тебе совершить гонку.

  Развернуть 1 комментарий

@annmuor, контейнеры кртуо) Этого и не хватало для понимания, спасибо большое!
Я б статью добавил бы пример контейнера с хэш мапой.

А то что linked list сложный эт канешн печально. На Go пишется на изи

  Развернуть 1 комментарий

@grbit, такова цена языка без GC.

  Развернуть 1 комментарий

@grbit, двусвязный список самостоятельно реализовывать обычно не нужно. А уже готовые встроены в язык, интрузивные наверняка есть в десятке видов в крейтах. Ну и при желании можно свой написать при помощи unsafe, но лучше не надо — будет так же больно, как на C++. Только хуже, потому что на C++ все привычные к тому что программа взрывается, а на Rust если библиотека допускает взрыв при любом поведении пользователя — это баг библиотеки. И в Rust про это надо заморачиваться сильно больше, чем в C++, потому что в C++ про такие высокие материи просто сил не хватает задуматься.

  Развернуть 1 комментарий

еще из интересного, если начали присматриваться в сторону раста

  Развернуть 1 комментарий

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

  Развернуть 1 комментарий

@Crypto_mate, имхо – импосибуру.
Идея же в том что вы пишите крутые приложения БЕЗ БАГОВ, а джун отличается спосоьностью стрелять в ногу не смотря ни на что)

Но я Go-шник, а не Rust-овщик, так что жди более информированного коментатора)

  Развернуть 1 комментарий

@Crypto_mate, не уверен за джуна, Rust требует очень много подготовительных знаний, чтоб на нём писать. Про порог входа не шутка же. То есть в идеале в Rust заходить, когда ты уже поел программирования и тебе не нужно думать о базовых паттернах, типах данных и всяком таком.

Если мы говорим про переходящих сеньёров - да, таких вакансий я вижу по 5 штук в день, типа "мы пишем на Расте, нам нужен раст сеньер или сеньер на C++/Go/Java, на Раст переучим". Но перешедший сеньёр джуном и не будет, в общем-то :)

На рынке есть две ветви, где Раст востребован:

  • Крипта, web3.0, NFT и вот это всё ( потому, что безопасно и мамкины хакеры тебе буффер оверфлоу не сделают )
  • Телекоммуникации и всё такое ( потому что безопасно и очень быстро, 100500 тыщ миллионов коннектов в секунду )

Весь MAANG нанимает Rust'оводов именно за второе, поэтому джуна там не ждут, но переучить переучат.

А пет проекты - на самом деле отличный вариант. Я вот тут сел и написал за ~30 минут резолвер на стотыщ RPS чтоб проверять кросс-валидность всех зон которые мы используем.
Вот такие мелкие проекты отлично учат писать код, их можно делать в рамках работы, они затаскиваются в одно лицо и приносят всем пользу.

  Развернуть 1 комментарий
Dmitriy Rozhkov Distinguished Notion API Consultant 6 августа 2023

Я думаю, что четвертый кит — умные указатели. С одной стороны тебе придется с ними разобраться разбираясь с владением. С другой — понять эти коробочки отдельно помогает понять что вообще происходит.

  Развернуть 1 комментарий

Можно ли это обойти?

а зачем это обходить? выглядит как разумный набор правил

  Развернуть 1 комментарий

@shrddr, иногда надо, когда сам пишешь такие библиотеки, например.
Но чаще не надо.

  Развернуть 1 комментарий

Пример про borrow без перехода на &str не полон, всё же!

  Развернуть 1 комментарий

@ldvsoft, не хотелось грузить магией as_ref / into

  Развернуть 1 комментарий

@annmuor, Ну всё же канонически строчки передают в двух вариантах: String и &str ;)

  Развернуть 1 комментарий

@ldvsoft, да, но чтоб объяснить, как из &String получается &str надо прям постараться.

  Развернуть 1 комментарий
Evgeniy Maruschenko Data Engineer/Software Engineer 12 октября 2023

Там где-то потерялись между китами алгебраические типы и паттерн метчинг.

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

  Развернуть 1 комментарий

😎

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

Что вообще здесь происходит?


Войти  или  Вступить в Клуб