Решил я в конце новогодних каникул немного заняться саморазвитием. Выбор пал на язык, на котором надо переписать ElasticSearch, а то и вообще все. Дополнить я это решил Уэб-технологиями, а именно священным граалем желанным отказом от Javascript.

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

Ну что,

Базовые знания #

Материалов, чтобы изучить Rust — предостаточно. Я прочел A half hour to learn Rust, паттерны и периодически заглядывал на Rust by Example. Ну и StackOverflow, куда же без него.

Официальный сайт языка предлагает его ставить скачиванием скрипта через curl | sh. Это триггернуло мое образование и я на такое не согласился. Безопасность начинается очень весело.

Страница с альтернативными путями установки содержит еще очень удобный способ: запустить комбо curl | sh в командной строке (sic!). Ну или использовать установщики из tar.gz. Альтернативный путь для Убунты:

snap install rustup --classic

Snap тоже то еще дно, но его я переживу, хотя гореть ему вместе с электроном в одном котле.

Дальше методом тыка я понял, что нужно сделать так:

rustup install default
rustup default stable

Написать об этом для бумеров на сайте в очевидном месте видимо никто не решился, потому что все равно правильный путь — это curl | sh. Все ставится локально для пользователя, по крайней мере по умолчанию.

После этого пишем сложную функцию

fn main() {
    println!("Hello World!");
}

Компилируем через rustc 1.rs и готово, можно получить приветствие через ./1!

Системщики могут посмотреть на LLVM с помощью rustc --emit=llvm-ir 1.rs, который выглядит довольно страшно.

Первые шаги с Wasm #

Тут может возникнуть закономерный вопрос

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

(тут мог быть ваш каламбур про wasm → wasp)

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

По теме Wasm + Rust есть туториал, в котором есть примеры и скупые объяснения. Начать предлагают традиционно с curl | sh:

curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

Благо, тут хотя бы пишут нормальную альтернативу:

cargo install wasm-pack

Затем предлагают сразу сгенерировать все что надо через cargo-generate, вместо того, чтобы показать, как что-то подобное делать с нуля. Я и в этот раз не сделал, как предлагали, а просто скопировал git-репозиторий с шаблоном, чтобы его поковырять. Шаблонные данные поправил руками.

Дальше нужно сбилдить проект

~/.cargo/bin/wasm-pack build

Поставить npm, если его еще нет

sudo npm install npm@latest -g

А потом инициализировать шаблон приложения с Wasm:

npm init wasm-app www

Экосистема, аднака! Которая еще и выдаст эмодзи в консоль.

(дед во мне прям кряхтит от кринжа)

После этого у нас будет готова HTML-страничка, которая умеет делать простой alert. Миллиард терабайт node_modules в комплекте.

Но мы с вами живем в мире, где нельзя просто так взять и сделать alert на простой HTML.

Если просто открыть эту html-ку в браузере, то придет мой старый врагCORS.

Поэтому надо еще и запустить сервер

npm run start

А потом еще и поколдовать с webpack, чтобы он запускался не только на localhost. Проверил — даже на моем не самом свежем планшете работает.

Манипуляция DOM #

В 2018 году на Хабре писали, что такое делать нельзя, а если и можно, то с извращениями. Отлично, значит этим мы и займемся. Поверхностное изучение вопроса привело меня к библиотеке stdweb. Документация там так себе, тупо ссылки на соответствующие разделы из JS. Да и вообще это не полноценное API, а биндинги к JS.

На основе примера я начал стряпать что-нибудь простенькое, типа передвижения объекта.

Первые грабли #

Первый раз все компилировалось весьма медленно. Удалось словить веселые приколы с дефисом против подчерка в имени файла. Потом вляпался в отличия статической строки от динамической — надо руками кастовать между этими типами, да и макрос для форматирования строки тоже не блещет изяществом:

let x = 5;
let y = 6;
alert(format!("Example of format {} {}", x, y).as_str());

Привет плюсовому c_str()!

Трейты, с одной стороны прикольная вещь, а с другой — работают практически как имплиситы в Scala, со всеми вытекающими. К ним еще вернусь.

Компилятор услужливо подсказывает в некоторых местах: чувак, используй snake_case. А тут у тебя импорт не используется. В основном его предупреждения были по делу.

Через некоторое время у меня все успешно скопилировалось, чтобы упасть с паникой при открытии страницы. Понять, что не так — почти невозможно, стектрейс с кишочками из библиотеки торчит.

И это с учетом console_error_panic_hook, который должен делать ошибку понятной. Я вышел с этим вопросом в интернет, но нашел только странный баг в Firefox. Потыкался в console_error_panic_hook — что подключать его, что не подключать — результат одинаковый.

Озарение #

Потом понимаю — что запускаю-то я сервер через npm. Горячая загрузка и все такое — это классно, но имеет ли код при таком способе запуска доступ к DOM? Ссаный фронтенд, где факт того, что у тебя запустилось, ни хрена не значит :(.

Переделываю свой код на основе другого примера. Не работает.

Ладно, наверно я дебил, пробую скомпилировать сам пример, но там ошибка компиляции:

error: missing documentation for a function

Закомментировал линтер, ура, скомилировалось, и…. Uncaught (in promise) Error: undefined. Bellissimo. Потом еще выясняется, что эта библиотека толком и не поддерживается уже.

Возврат к wasm-bindgen и новые грабли #

Ок, я тупой, надо было дальше читать туториал по wasm-bindgen и не выпендриваться. Моей ошибкой (кроме использования неактуальной библиотеки) было непонимание, что предыдущие действия были нужны для создания npm-модуля для подключения через package.json. А мне-то нужна была фронтовая библиотека, и для прямой загрузки в браузере нужно было использовать флаг --target web для wasm-pack. Запуск веб-сервера все равно нужен, но делать это можно любым инструментом, даже python3 -m http.server.

Хочу сделать двигающийся объект, смотрю пример про request_animation_frame (тут я его немного сократил):

#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
    let f = Rc::new(RefCell::new(None));
    let g = f.clone();

    let mut i = 0;
    *g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
        if i > 300 {
            body().set_text_content(Some("All done!"));
            let _ = f.borrow_mut().take();
            return;
        }

        i += 1;
        let text = format!("requestAnimationFrame has been called {} times.", i);
        body().set_text_content(Some(&text));

        request_animation_frame(f.borrow().as_ref().unwrap());
    }) as Box<dyn FnMut()>));

    request_animation_frame(g.borrow().as_ref().unwrap());
    Ok(())
}

Первое впечатление — жесть, как вообще это читать? Однобуквенные переменные, куча кастов — это точно про безопасность? Вообще по wasm-bindgen дока приемлемая, но в примерах, по ощущениям, люди совсем не стесняются писать “лишь бы работало” и использовать однобуквенные переменные.

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

Следующая ошибка —

expected an `FnMut<()>` closure, found `[closure@src/lib.rs:68:51: 84:6]

Компилятор показывает услужливо место, что не так. Услужливо дает возможность прочитать объяснение ошибки для даунов (rustc --explain E0277): так мол и так, ты совсем дебил, нужно определить трейт для типа. Смотрю в пример, импорты, Cargo.toml — все то же самое, что и в примере. Качаю пример — он билдится. Думаю, что еще может быть не так, методом тыка обнаруживаю, что если использовать свою функцию в замыкании, то возникает такая ошибка. Ссылочная прозрачность, епрст! Будущий я поймет, что проблема была во владении переменными, но даже с учетом этого описание ошибки все равно трешовое.

Еще одна ошибка с типами:

x += dx;
expected `i32`, found `i8`

Вот честно, я даже не хочу разбираться, в каком хитром кейсе я словлю проблем при добавлении 8-битного числа к 32-битному и как правильно сделать тут преобразование типов. Тут уже опускаешься даже ниже плюсов. При этом для ублажения компилятора достаточно сменить i8 на i32 — со сложением двух i32 проблем-то точно не будет, кек.

Дальше было трахание с указателями, статиками, замыканиями — очень интересно конечно… но хотелось бы попроще. Я вроде на современном языке пишу, а не на кроссплатформенном ассемблере.

Порадовал комментарий к замыканиям из примера wasm-bindgen:

Normally we'd store the handle to later get dropped at an appropriate
time but for now we want it to be a global handler so we use the
`forget` method to drop it without invalidating the closure. Note that
this is leaking memory in Rust, so this should be done judiciously!

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

Хочу расшарить одну переменную на два места, чтобы одна функция читала, другая писала. Тут открывается миллион типов указателей, внутри которых еще и руками боксинг нужно делать. Плюсы отдыхают. Кое-как извратился через копию переменной и Rc. Уже устал от этого всего.

Еще всякие забавные мелочи были, например, приколы с неймингом: обнаружил, что есть flat_map, но не для Option. После Scala это выглядит странно. Еще забавное — нет унарного плюса (хотя мне бы он пригодился для читаемости).

Extension-методы — больше похожи на implicit class в Scala. Но еще и интерфейс надо определять обязательно (по крайней мере, как я понял). Не очень удобно для разового расширения, но enterprise-джавистам понравится.

Немного фронтенд-треша #

Проблемы у меня возникли и по самой сути задачи двигающегося объекта: пришлось копаться с отличиями offsetWidth, clientWidth, scrollWidth, настоящими и CSS-пикселями, position fixed и absolute, нюансами margin (который зависит от тега и от браузера) и так далее. Без поллитры помощи друга не обошлось. Это, конечно, добавило мне “любви” к фронтенду.

Рефакторинг #

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

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

error[E0277]: expected a `FnMut<()>` closure, found `[closure@src/dom_utils.rs:100:51: 110:6]`

Попробовал еще раз, с мутабельностью. Если использовать явно функцию — то все ок, если ее же передавать параметром — ошибка. Оказалось, что надо еще знать отличие между замыканием и указателем на функцию. Был еще один веселый прикольчик с

error[E0310]: the parameter type `TContext` may not live long enough

но это легко решилось.

Захотел выделить бойлерплейт для создания wasm-замыканий, но оказалось, что это сложно. Я поленился, потому что, по видимому, решение было близко к тому, что сокращение кода не сократило бы его, и решил попробовать другую фичу — макросы. Любители аббревиатур тут просто плясать могут: надо помнить, например, чем отличается tt от ty. Особенно весело читать это в первый раз в примерах. Через некоторое количество тупки у меня все-таки получилось сделать то, что я хотел, через макрос.

Потом я подумывал еще отрефакторить создание флага для остановки движения, но в какой-то момент решил, что хватит это терпеть. От перестановки кусочков местами нужно опять перепродумывать владение, потом наложатся еще замыкания… Это мне принесет не очень много нового опыта, а вот горения — предостаточно.

Заключение #

Что получилось в итоге — можно посмотреть тут, а исходники — тут.

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

Я ожидал язык высокого уровня, который за счет хороших абстракций решает проблемы с утечками памяти, присущие плюсам. А в итоге надо в голове держать полную модель памяти (и это чуть ли не сложнее чем помнить new/delete в старых плюсах), кто у кого что занял. Ссылочной прозрачности нет, и при простом рефакторинге нужно многое перепродумывать. Ошибки компилятора отлично говорят об ошибках, когда угадывают, что ты хотел, но почти бесполезны в ином случае.

У меня сложилось впечатление, что язык на самом деле низкого уровня, просто с сахаром. Надо понимать все чуть ли не на уровне ассемблера, но знать концепции на уровне Scala. Везде, везде сраные детали реализации. С числами два стула — либо использовать адекватные типы данных и постоянно конвертировать, либо забить и использовать везде один (как сделал я).

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