Rust и Wasm
Решил я в конце новогодних каникул немного заняться саморазвитием. Выбор пал на язык, на котором надо переписать 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 — нет, спасибо.