Мой первый опыт работы с Rust, WebAssembly и npm

Я работал над хобби-проектом Node.js, который должен был читать файловые архивы 7-Zip. Не было идеальных библиотек JavaScript, которые могли бы читать этот формат файла (ни одна из них не работала для меня), поэтому я решил написать ее сам! Реализация JavaScript работала, но была довольно медленной — и поэтому я подумал, давайте воспользуемся этой возможностью, чтобы написать библиотеку декомпрессии 7z на Rust, скомпилировать ее в WebAssembly и загрузить в Node.js или веб-браузер!

И вот мой получившийся код: Shoeset

Существует ряд языков, которые можно скомпилировать в WebAssembly, но наиболее целесообразными являются высокопроизводительные низкоуровневые языки, такие как C/C++ и Rust. Я использовал эту возможность как предлог для изучения языка Rust. Приходя из языков со сборкой мусора, меня всегда раздражали небезопасные части C/C++: выделение памяти, очистка, риск утечек памяти, переполнения буфера, паники и так далее. У Rust есть очень желательное свойство быть безопасным для памяти; компилятор сделает за вас много проверок и откажется компилировать программы с ошибками памяти.

Настройка проекта

Начало работы с Rust/WebAssembly на моем Mac было простым:

brew install rustup

Rustup — это как nvm для Node; это менеджер версий, который позволяет вам выбирать между различными версиями Rust.

Экосистема Rust в целом очень похожа на Node/npm. rustc — это компилятор, а Cargo — менеджер пакетов. Обычно вы не запускаете rustc напрямую, вы запускаете cargo build (или cargo test). Сборки Cargo можно сравнить со сборками Webpack/Rollup в JavaScript или, может быть, Maven/Gradle в проектах на основе Java.

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

Настроить новый каркас проекта было легко:

cargo init

…и это сгенерирует структуру папок!

Использование wasm-pack

Утилита wasm-pack используется для сборки пакетов WebAssembly на основе Rust. Он упаковывает груз, так что ваш код Rust скомпилирован в WebAssembly, а затем генерирует структуру папок для типичного пакета npm с пакетом package.json, README, LICENSE, так что все, что вам нужно сделать, это npm опубликуйте это!

Что я сделал во время разработки, так это npm link-ed для доступа к моей библиотеке, а в моем хобби-проекте, который будет использовать пакет, я npm link-ed библиотеку для потреблять это:

cd shoeset
npm link
cd ../my-project
npm link @eirslett/shoeset

Таким образом, мне не нужно было публиковать пакет в npm для каждого изменения. Мне просто нужно было запустить wasm-pack.

Изучение языка Rust

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

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

Борьба с проверкой заимствований

Это самое распространенное разочарование для новичков в Rust. Поскольку язык безопасен для памяти, все проверки выполняются компилятором. В Rust есть концепция владения, которая является уникальной функцией языка. Это механизм, который позволяет Rust работать без сборщика мусора. Вы передаете право собственности на переменную — или вместо этого можете заимствовать ее. Здесь на помощь приходит проверка заимствования. Ваш код просто не скомпилируется, если вы не будете заимствовать правильно! В JavaScript это не проблема, так как это высокоуровневый язык со сборкой мусора. Вещи просто работают! В Rust компилятор сделает за вас выделение и освобождение памяти, но взамен это совершенно непростительно. В большинстве случаев, как новичок, вы будете добавлять или удалять знаки &, добавлять или удалять ключевые слова mut или даже &mut и так далее, пока он не скомпилируется.

// Pseudo-code, you try every combination:
let m = “hey”; // nope
let mut m = “Hey”; // nope
let m = String::from(“Hey”);  // nope
let mut m = &String::from(“Hey”); // nope
// etc. etc

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

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

Модульная система

Вы можете загружать внешние зависимости, называемые crates (аналогично пакетам npm), добавляя их в файл Cargo.toml (аналогично package.json). Установить эти зависимости очень просто. Но я должен сказать, что в Rust очень сложная и запутанная система модулей. Мы можем быть избалованы при работе с Node.js, потому что там очень простая модульная система. Несмотря на то, что модули гармонии (названные экспортами и экспортами по умолчанию) могут сбивать с толку, вам будет их очень не хватать при работе с Rust.

Есть extern crate a, mod b, use c::d::e и комбинация этих ключевых слов, которые вы должны использовать для загрузки кода. Либо из других файлов вашего проекта, либо из внешних зависимостей. Я провел много времени, расстраиваясь из-за ошибок «неразрешенной зависимости», потому что система модулей не работала так, как я ожидал.

Неровные края

Экосистема wasm-pack работает, но явно находится на ранней стадии разработки, и многие функции отсутствуют. Например, мне не удалось скомпилировать универсальный модуль npm, который можно было загрузить либо из Node.js, или из браузера. Мне пришлось бы создать два отдельных пакета. Будем надеяться, что экосистема станет более зрелой по мере того, как WebAssembly и Rust получат все большее распространение.

Иды

Я использую IntelliJ IDEA для всех других своих работ, это редактор, с которым я знаком больше всего, поэтому я установил плагин Rust и решил попробовать его. Это, конечно, был трудный путь — подсветка синтаксиса работала нормально, но среда IDE подсвечивала ненастоящие ошибки, но не выделяла ошибки, на которые жаловался компилятор. Так что я мог бы использовать его как текстовый редактор, но плагин казался мне незрелым. Это также увеличило загрузку процессора моего Mac, и вентиляторы работали на полной скорости! Иногда вся IDE зависала на 4–5 секунд при попытке разрешить автодополнение. В целом это был раздражающий опыт, но я уверен, что эти перегибы в конечном итоге будут устранены.

Первое впечатление

Было очень весело (и разочарованно) изучать новый язык и экосистему! Между Node.js/JavaScript/npm и Rust/Cargo есть много общего. Попользовавшись Rust в течение нескольких дней и освоившись с ним, я должен сказать, что он очень многообещающий. Я бы порекомендовал всем, у кого есть особые требования к производительности (или просто хочет узнать что-то новое), попробовать Rust и WebAssembly!