Код на rust

Обновлено: 02.07.2024

Этот цикл статей является вольным переводом книги «Rust by Example», которую пишет Хорхе Апарисио на Github.

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

Содержание

1. Привет, мир!

Это код традиционной программы «Hello World»:

println! это макрос (мы рассмотрим их позже), который печатает текст в консоль.

Программа может быть сгенерирована с помощью компилятора Rust rustc:

rustc создаст бинарный файл «hello», который можно запустить:

2. Форматированный вывод

Макрос println! не только выводит в консоль, а также способен форматировать текст и сериализованные значения. Корректность проверяется во время компиляции.


Дополнительная информация о форматировании здесь: std​::fmt​

3. Литералы и операторы

Целые числа 1, с плавающей точкой 1.2, символы 'a', строки "abc" , логические true и значения пустого типа () могут быть выражены с помощью литералов.

Также целые числа можно выразить через шестнадцатеричное, восьмеричное или двоичное обозначение, используя один из префиксов: 0x, 0o или 0b.

В числовые литералы можно вставлять подчёркивания для читабельности, например, 1_000 такой же, как и 1000, а 0.000_001 такой же, как и 0.000001.

Мы должны сказать компилятору, какой из литералов мы используем. Сейчас мы будем использовать суффикс u , указывающий, что литерал является целым числом без знака, суффикс i чтобы указать, что это знаковое целое число. Мы рассмотрим систему типов в 5 главе, а также подробную информацию о аннотировании литералов.

Доступные операторы и их приоритет похож на C-подобных языках.

4. Переменные

Значения (как и литералы) могут быть связаны с переменными, используя обозначение let.

4.1 Изменяемость
4.2 Области и видимость

Переменные имеют локальную область, и имеют видимость в блоке (блок представляет собой набор операторов, заключённых в фигурные скобки <>). Кроме того, допускается скрытие переменной.

4.3 Предварительное объявление

Можно сперва объявлять переменные, а инициализировать их позже. Но эта форма редко используется, так как это может привести к использованию неинициализированных переменных.


Компилятор запрещает использование неинициализированных переменных, так как это привело бы к непредсказуемым последствиям.

5. Типы

Rust обеспечивает безопасность с помощью статической проверки типов. Тип переменной может быть явно указан при её объявлении. Тем не менее, в большинстве случаев, компилятор сможет определить тип переменной из контекста, если это очевидно.

  • целые числа: i8 , i16 , i32 , i64 и int (размер зависит от платформы)
  • целые числа без знака: u8 , u16 , u32 , u64 и uint (размер зависит от платформы)
  • с плавающей точкой: f32 , f64
  • char значения Unicode: 'a', 'α' и '∞' (4 байта каждый)
  • bool true или false
  • кортежи ()
5.1 Приведение типов

Rust не предоставляет неявного преобразования типов (coercion) между примитивами, но, явное приведение типов (casting) может быть достигнуто с помощью ключевого слова as.

5.2 Литералы

В числовых литералах тип может быть аннотирован, добавив тип в качестве суффикса, за исключением uint , использующей суффикс u и int , который использует суффикс i .

  • fun(&foo) используется, чтобы передать аргумент в функцию по ссылке, а не по значению fun(foo) .
  • std::mem::size_of_val является функцией, но вызывается с указанием полного пути. Код можно разделить на логические единицы, называемые модулями. Здесь функция size_of_val определена в модуле mem , а модуль mem определен в крэйте std .
5.3 Логический вывод

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

Отсутствует необходимость в аннотации типа переменной, компилятор счастлив как и программист!

5.4 Псевдонимы (алиасы)

Оператор type может быть использован, чтобы задать новое имя существующему типу. Тип должен быть в стиле CamelCase, либо компилятор выдаст предупреждение. Исключением из этого правила являются примитивные типы: uint , f32 и другие.


Основное применение псевдонимов это снижение количества кода, например, тип IoResult является псевдонимом типа Result<T, IoError> .

image

Всем привет. Недавно познакомился с новым для себя языком программирования Rust. Я заметил, что он отличается от других, с которыми мне до этого доводилось сталкиваться. Поэтому решил покопать глубже. Результатами и своими впечатлениями хочу поделиться:

  • Начну с главной, на мой взгляд, особенности Rust
  • Опишу интересные детали синтаксиса
  • Объясню, почему Rust, скорее всего, не захватит мир

Сразу поясню, что я около десяти лет пишу на Java, так что рассуждать буду со своей колокольни.

Rust является низкоуровневым языком, на выходе компилятор выдает бинарник, для работы которого не нужны дополнительные ухищрения. Вся логика по удалению ненужных объектов интегрируется в код в момент компиляции, т.е. сборщика мусора во время выполнения тоже нет. В Rust так же нет пустых ссылок и типы являются безопасными, что делает его даже более надежным чем Java.

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

Эту концепцию можно продемонстрировать в следующем куске кода. Из метода main() вызывается test(), в котором создается рекурсивная структура данных MyStruct, реализующая интерфейс деструктора. Drop позволяет задать логику для выполнения, перед тем как объект будет уничтожен. Чем-то похоже на финализатор в Java, только в отличие от Java, момент вызова метода drop() вполне определен.

Вывод будет следующим:

Т.е. перед выходом из test() память была рекурсивно очищена. Позаботился об этом компилятор, вставив нужный код. Что такое Box и Option опишу чуть позже.

Таким образом Rust берет безопасность от высокоуровневых языков и предсказуемость от низкоуровневых языков программирования.

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

Тут Rust вообще впереди планеты всей. Если большинство языков пришли к тому, что надо отказаться от множественного наследования, то в Rust наследования нет вообще. Т.е. класс может только имплементировать интерфейсы в любом количестве, но не может наследоваться от других классов. В терминах Java это означало бы делать все классы final. Вообще синтаксическое разнообразие для поддержания OOP не так велико. Возможно, это и к лучшему.

Для объединения данных есть структуры, которые могут содержать имплементацию. Интерфейсы называются trait и тоже могут содержать имплементацию по умолчанию. До абстрактных классов они не дотягивают, т.к. не могут содержать полей, многие жалуются на это ограничение. Синтаксис выглядит следующим образом, думаю комментарии тут не нужны:

Из особенностей на которые я обратил внимание, стоит отметить следующее:

  • У классов нет конструкторов. Есть только инициализаторы, которые через фигурные скобки задают значения полям. Если нужен конструктор, то это делается через статические методы.
  • Метод экземпляра отличается от статического наличием ссылки &self в качестве первого аргумента.
  • Классы, интерфейсы и методы также могут быть обобщенными. Но в отличие от Java, эта информация не теряется в момент компиляции.

Еще немного безопасности

Как я уже говорил Rust уделяет большое внимание надежности кода и пытается предотвратить большинство ошибок на этапе компиляции. Для этого была исключена возможность делать ссылки пустыми. Это мне чем-то напомнило nullable типы из Kotlin. Для создания пустых ссылок используется Option. Так же как и в Kotlin, при попытке обратиться к такой переменной, компилятор будет бить по рукам, заставляя вставлять проверки. Попытка же вытащить значение без проверки может привести к ошибке. Но этого уж точно нельзя сделать случайно как, например, в Java.

Мне еще понравилось то, что все переменные и поля классов по умолчанию являются неизменяемыми. Опять привет Kotlin. Если значение может меняться, это явно надо указывать ключевым словом mut. Я думаю, стремление к неизменяемости сильно улучшает читабельность и предсказуемость кода. Хотя Option почему-то является изменяемым, этого я не понял, вот код из документации:

Killer problem

Главный недостаток происходит из главной особенности. За все приходится платить. В Rust очень неудобно работать c изменяемыми графовыми структурами данных, т.к. на любой объект должно быть не более одной ссылки. Для обхода этого ограничения есть букет встроенных классов:

  • Box — неизменяемое значение на куче, аналог оберток для примитивов в Java
  • Cell — изменяемое значение
  • RefCell — изменяемое значение, доступное по ссылке
  • Rc — reference counter, для нескольких ссылок на один объект

И это неполный список. Для первой пробы Rust, я опрометчиво решил написать односвязный список с базовыми методами. В конечном счете ссылка на узел получилась следующая Option<Rc<RefCell<ListNode>>>:

  • Option — для обработки пустой ссылки
  • Rc — для нескольких ссылок, т.к. на последний объект ссылаются предыдущий узел и сам лист
  • RefCell — для изменяемой ссылки
  • ListNode — сам следующий элемент

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

На Kotlin то же самое выглядит намного проще:

Как выяснил позже подобные структуры не являются характерными для Rust, а мой код совсем неидиоматичен. Люди даже пишут целые статьи:

Тут Rust жертвует читабельностью ради безопасности. Кроме того такие упражнения еще могут привести к зацикленным ссылкам, которые зависнут в памяти, т.к. никакой garbage collector их не уберет. Рабочий код на Rust я не писал, поэтому мне сложно сказать насколько такие трудности усложняют жизнь. Было бы интересно получить комментарии практикующих инженеров.

Перечисления

В Rust называются enum. Только помимо ограниченного числа значений они еще могут содержать произвольные данные и методы. Таким образом это что-то среднее между перечислениями и классами в Java. Стандартный enum Option в моем первом примере как раз принадлежит к такому типу:

Для обработки таких значений есть специальная конструкция:

А также

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

  • Любители функционального программирования не будут разочарованы, для них есть лямбды. У итератора есть методы для обработки коллекции, например, filter и for_each. Чем-то похоже на стримы из Java.
  • Конструкция match так же может быть использована для более сложных вещей, чем обычные enum, например, для обработки паттернов
  • Есть большое количество встроенных классов, например, коллекций: Vec, LinkedList, HashMap и т.д.
  • Можно создавать макросы
  • Есть возможность добавлять методы в существующие классы
  • Поддерживается автоматическое выведение типов
  • Вместе с языком идет стандартный фреймворк для тестирования
  • Для сборки и управления зависимостями используется встроенная утилита cargo

Этот раздел необходим для полноты картины.

Сложность изучения

Долгий процесс изучения Rust во многом следует из предыдущего раздела. Перед тем как написать вообще хоть что-то придется потратить время на освоение ключевой концепции владения памятью, т.к. она пронизывает каждую строчку. К примеру, простейший список у меня занял пару вечеров, в то время как на Kotlin то же самое пишется за 10 минут, при том что это не мой рабочий язык. Помимо этого многие привычные подходы к написанию алгоритмов или структур данных в Rust будут выглядеть по другому или вообще не сработают. Т.е. при переходе на него понадобится более глубокая перестройка мышления, просто освоить синтаксис будет недостаточно. Это далеко не JavaScript, который все проглотит и все стерпит. Думаю, Rust никогда не станет тем языком, на котором учат детей в школе программирования. Даже у С/С++ в этом смысле больше шансов.

Мне показалась очень интересной идея управления памятью на этапе компиляции. В С/С++ у меня опыта нет, поэтому не буду сравнивать со smart pointer. Синтаксис в целом приятный и нет ничего лишнего. Я покритиковал Rust за сложность реализации графовых структур данных, но, подозреваю, что это особенность всех языков программирования без GC. Может быть, сравнения с Kotlin было и не совсем честным.

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

Если вас заинтересовал Rust, то вот несколько ссылок:

    — хорошая книга, есть так же в электронном варианте — официальная документация, есть примеры — список статей и ruRust/general — каналы в Gitter — Reddit

UPD: Всем спасибо за комментарии. Узнал много полезного для себя. Исправил неточности и опечатки, добавил ссылок. Думаю, такие обсуждения сильно способствуют изучению новых технологий.

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

В большинстве языков вам придётся переписывать всю библиотеку с нуля, а первые результаты появятся ближе к концу проекта. Такие порты, как правило, довольно дороги и подвержены ошибкам, а часто они выходят из строя на полпути. Джоэл Сполски гораздо лучше меня объясняет это в статье о том, почему полные переделки проектов — плохая идея.

Портировать библиотеки на Rust по одной функции.

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

Если вы нашли статью полезной или заметили ошибку, дайте знать в баг-трекере блога!

Прежде чем что-то сделать, нужно создать новый проект. У меня есть шаблон, который устанавливает CI и лицензии для cargo-generate.


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

В данном случае мы портируем jakogut/tinyvm,

TinyVM — это небольшая, быстрая, лёгкая виртуальная машина, написанная на чистом языке ANSI C.

Чтобы проще ссылаться на неё в будущем, добавим репозиторий в качестве подмодуля в наш проект.


Теперь посмотрим на исходный код. Для начала, README.md инструкций по сборке.

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

Сборка выполняется на UNIX-подобных системах с make и GCC.

Внешних зависимостей нет, сохраните стандартную библиотеку С.

Сборка выполняется с помощью make или make rebuild.

Чтобы собрать отладочную версию, добавьте DEBUG=yes после make. Чтобы собрать двоичный файл с включённым профилированием, добавьте PROFILE=yes после make.

Ладно, давайте заглянем в каталог tinyvm и посмотрим, будет ли сборка просто работать.


Мне очень нравится, когда библиотеки C компилируются прямо из коробки без необходимости устанавливать случайные пакеты *-dev или возиться с системой сборки.

К сожалению, библиотека не содержит никаких тестов, поэтому мы не сможем (сразу) убедиться, что отдельные функции переведены правильно, но она содержит пример интерпретатора, который мы можем использовать для изучения функциональности высокого уровня.

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

Вот тут-то и появляются сценарии сборки. Наша стратегия заключается в том, чтобы крейт Rust использовал скрипт сборки build.rs и крейт cc для вызова эквивалентных команд к нашему вызову make . Оттуда мы можем подключиться к libtvm из Rust точно так же, как и к любой другой родной библиотеке.

Нужно будет добавить крейт cc в качестве зависимости.


А также убедиться, что build.rs компилирует исходный код libtvm .

Примечание
Если вы просмотрели документацию крейта cc , то, возможно, заметили метод Build::files() , который принимает итератор путей. Мы могли бы программно обнаружить все файлы *.c внутри vendor/tinyvm/libtvm , но поскольку мы портируем код по одной функции, гораздо проще удалить отдельные вызовы .file() по мере портирования.

Нам также нужен способ сообщить Rust, какие функции он может вызвать из libtvm . Обычно это делается путём записи определений для каждой функции в блоке extern, но, к счастью, существует инструмент под названием bindgen, который может читать заголовочный файл в стиле C и генерировать определения для нас.

Сгенерируем привязки из vendor/tinyvm/include/tvm/tvm.h .


Нужно будет добавить в наш крейт модуль ffi .


Глядя на каталог src/ в tinyvm , мы находим исходный код интерпретатора tinyvm .


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

А пока давайте переведём его непосредственно в Rust и вставим в каталог examples/ .


В качестве проверки также можем запустить виртуальную машину и убедиться, что всё это работает.

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

Вместо этого давайте поищем самое простое.


Этот файл tvm_htab.с выглядит многообещающе. Я почти уверен, что htab расшифровывается как «хэш-таблица», а стандартная библиотека Rust уже содержит высококачественную реализацию. Мы должны быть в состоянии поменять это достаточно легко.

Посмотрим на заголовочный файл tvm_htab.h и проверим, с чем имеем дело.


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

Мы можем проверить, имеет ли что-нибудь доступ к внутренним элементам хэш-таблицы, временно переместив определения структуры в tvm_htab.c и посмотреть, всё ли ещё компилируется.


А затем ещё раз запускаем make :


Похоже, всё по-прежнему работает, теперь приступаем ко второй фазе; создаём идентичный набор функций, которые под капотом используют HashMap<K, V> .

Ограничившись заглушкой с самым минимумом, получаем:


Также нужно объявить модуль htab и реэкспортировать его функции из lib.rs .


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

Стена ошибок повторяющихся символов

Исправление на самом деле довольно простое.


И попытка запустить пример tvmi снова терпит крах, как и следовало ожидать от программы, полной unimplemented!() .


При добавлении поддержки FFI для нового типа проще всего начать с конструктора и деструктора.

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

Предупреждение
Важно, чтобы вызывающие абоненты уничтожали HashTable только с помощью функции tvm_htab_destroy () !

Если они не сделают этого и вместо этого попытаются вызвать free() напрямую, у нас почти наверняка возникнет плохая ситуация. В лучшем случае это приведет к большой утечке памяти, но также вполне возможно, что наш Box в Rust не использует ту же кучу, что malloc() и free () , а это означает, что освобождение объекта Rust от C может повредить кучу и оставить всё в сломанном состоянии.

Добавление элементов в хэш-карту почти так же просто реализовать.

Примечание
Важно убедиться, что мы здесь используем CString , а не обычный String , в качестве ключа хэш-таблицы, потому что *const c_char может содержать любые ненулевые байты, тогда как String в Rust требует, чтобы строка была валидной UTF-8.

Вероятно, нам сойдет с рук преобразование CStr в &str , а затем в String с владением, потому что большинство входных данных будут ASCII, но учитывая, что нам понадобится один или два unwrap() , проще просто сделать всё правильно и сохранить CString .

Две функции *_find() можно делегировать прямо во внутренний HashMap<CString, Item> .

Единственное, где нужно быть осторожным, — это убедиться, что правильное значение возвращается, когда элемент не может быть найден. В данном случае, посмотрев на tvm_htab.c мы видим, что tvm_htab_find() возвращает −1 , а tvm_htab_find_ref() возвращает NULL .


Теперь, когда мы фактически реализовали функции заглушки, всё должно снова работать.

Самый простой способ это проверить — запустить наш пример.


И чтобы перепроверить, мы можем запустить его через valgrind , чтобы убедиться, что нет утечек памяти или чего-то хитрого с указателями.

Виртуальная машина tinyvm использует упрощённую форму ассемблера, похожую на традиционный ассемблер Intel x86. Первым шагом при парсинге ассемблера tinyvm является запуск препроцессора, который интерпретирует операторы %include filename и операторы %define identifier value .

Такого рода манипуляции с текстом намного проще выполнить с помощью типов &str в Rust, поэтому давайте посмотрим на интерфейс, который должен реализовать наш крейт.


Использование char ** и int * для переменных src и src_len поначалу может показаться немного странным, но если бы вы написали эквивалент в Rust, то получили бы что-то вроде этого:


Код C просто использует выходные параметры для замены строки src на месте, потому что он не может возвращать как новую строку, так и код ошибки.

Прежде чем сделать что-то ещё, нужно написать тест для tvm_preprocess() . Таким образом, мы можем гарантировать, что наша функция Rust функционально эквивалентна оригиналу.

Мы взаимодействуем с файловой системой, поэтому нужно будет вытащить крейт tempfile.


Нам также понадобится крейт libc , потому что мы будем передавать строки libtvm , которые ему, возможно, потребуется освободить.


Глядя на исходный код, мы видим, что функция tvm_preprocess() будет продолжать разрешать %include и %define до тех пор, пока их не останется ни одного.

Сначала давайте создадим тест, чтобы убедиться, что препроцессор обрабатывает %define . Мы знаем, что этот код уже работает (в конце концов, это код от tinyvm ), так что никаких сюрпризов быть не должно.


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

Нам также нужно проверить, включая ещё один файл.

Примечание
В качестве примечания, тест изначально был написан так, чтобы вложить всё в три слоя глубиной (например, top_level.vm включает в себя nested.vm, которая включает в себя really_nested.vm), чтобы убедиться, что он обрабатывает более одного уровня %include , но независимо от того, как он был написан, тест продолжал сегфолтить.

Затем я попробовал запустить исходный двоичный файл C tvmi …


Оказывается, оригинальный tinyvm по какой-то причине падает, когда у вас несколько слоёв include .

Итак, теперь у нас несколько тестов, так что можем начать реализовывать tvm_preprocess() .
Во-первых, нужно определить тип ошибки.


Глядя на функции process_includes() и process_derives(), кажется, что они сканируют строку в поисках определённой директивы, а затем заменяют эту строку чем-то другим (либо содержимым файла, либо ничем, если строка должна быть удалена).

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


Теперь у нас есть хелпер process_line_starting_with_directive() , так что можно реализовать парсер %include .


К сожалению, парсер %define немного сложнее.


Чтобы получить доступ к тексту в нашей хэш-таблице, нужно будет дать элементу Item пару вспомогательных методов:


На этом этапе неплохо добавить ещё несколько тестов.


На этом этапе мы воспроизвели большую часть логики предварительной обработки, так что теперь просто нужна функция, которая будет продолжать расширять операторы %include и обрабатывать %define до тех пор, пока их не останется.

Совет
Возможно, вы заметили, что наша функция tvm_preprocess() не имеет никакой логики предварительной обработки и больше похожа на адаптер для перевода аргументов и возвращаемых значений, а также для обеспечения правильного распространения ошибок.

Это не случайность.

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

Создание тонкой оболочки вокруг нашей функции preprocess() также облегчает последующую задачу: когда большая часть кодовой базы написана на Rust, мы можем удалить оболочку и вызвать preprocess() напрямую.

Теперь функция tvm_preprocess() определена, и мы должны быть готовы к работе.


Ой, линкер жалуется, что и preprocessing.rs , и tvm_preprocessor.c определяют функцию tvm_preprocess() . Похоже, мы забыли удалить tvm_preprocessor.c из сборки…


Попробуем ещё раз.

Помните тот прошлый пример, где tvmi падал, получая три уровня глубины кода? Как приятный побочный эффект, после переноса кода на Rust вложенные уровни просто работают.

Примечание
Возможно, вы также заметили, что функция preprocess() не использует ни одной функции хэш-таблицы из tvm_htab.h . После портирования модуля на Rust мы просто используем типы Rust напрямую.

В этом вся прелесть этого процесса. Как только вы перенесли что-то на Rust, вы можете применить это, чтобы использовать типы/функции непосредственно — и мгновенно выиграть в обработке ошибок и эргономике.

Если вы все еще читаете статью, поздравляю, мы только что портировали два модуля с tinyvm на Rust.

К сожалению, эта статья уже довольно длинная. Но надеюсь, что к настоящему моменту вы уловили общую картину.

Читайте также: