Код на языке C# нужно перевести в код на Rust. Точнее, требуется такая процедура перевода (разработка продолжается на C#), чтобы в любой момент можно было получить работающий код на Rust. Эту задачу я решал для языков Java, Python, JavaScript и PHP, написав конвертер из C# в эти языки. Концепция такого конвертирования была изложена в статье UniSharping пару лет назад. Я разрабатывал этот конвертер, чтобы переводить код своего проекта SDK Pullenti (лингвистический анализ текста). И подумалось мне: а не замахнуться ли на Rust? Да, слышал разные отзывы, что язык необычный и пр., но попытка же не пытка… Тем более, что у одного из заказчиков группа программистов увлечённо пишет на нём. Сразу скажу, что в полном объёме, как для других языков, этого сделать не получилось — не хватило сил. Может, ещё вернусь к этой задаче. Полтора месяца было потрачено на борьбу с собой и языком, удалось довести конвертер до состояния, что морфологический блок начал переводиться и даже компилироваться (значит, и работать) на Rust-е. Разумеется, за это время модуль морфологии можно было написать с нуля, но за ним маячили ещё около 500 классов C#, создаваемые и отлаживаемые почти 10 лет, а их переписать не так просто. В этой статье хочу поделиться своими впечатлениями от языка Rust, а также описать приёмы, которые я использовал для конвертирования. Говорят, что мастер не ищет лёгкий путей. Это в полной мере относится к Rust, так как многое простое и привычное на других языках становится сложным, при этом сложное не становится простым. Вы как бы попадаете в другой мир с абсурдной на первый взгляд логикой, которая становится далеко не сразу понятна после освоения базовых концепций. Неважно, на чём вы писали до сих пор: Си++, Java, Python и пр., но когда оказывается, что после добавления в список объект нельзя использовать: it = new ...(); list.add(it); it.val = ..., а вот так можно: it = new ...(); it.val = ...; list.add(it);, то это обескураживает. Или чтобы реализовать перекрёстные ссылки между объектами класса Foo, нужно использовать конструкцию Option<Rc<RefCell<Foo>>>, а для доступа к полю val этого класса вызывать foo.unwrap().borrow().val. Выскажу своё личное мнение: мне представляется, что прогресс в области программирования идёт в направлении оптимизации труда программиста, чтобы меньшими усилиями достигать большего эффекта. Случай Rust уникален тем, что не вписывается в эту тенденцию. Удивительным для меня фактом является рост его популярности (сейчас Rust вошёл в 20-ку). Но почему? Вот вопрос.  По производительности на моей задаче Rust не произвёл впечатления — выигрыш по сравнению с C# получился всего в 2 раза. Подчеркну, что это на моей частной задаче морфологического анализа, которую удалось перевести в эквивалентный код (наверняка человек написал бы оптимальнее). В разных статьях сравнения производительности приводятся разные данные, и в целом создаётся впечатление, что Rust и C/C++ близки по скорости. Но существенно разнятся по сложности написания кода. Утверждается, что Rust сильно уменьшает вероятность утечек памяти по сравнению с С/C++, доступа за границу массива и прочее, но какой ценой... Единственным разумным для меня объяснением роста популярности Rust является то, что Си "поднадоел" за почти 50 лет, и молодое поколение программистов желает чего-то нового, не обязательно лучшего. Как молодёжь 80-х добровольно ехала из обжитых городов строить БАМ (такая железная дорога на Дальнем Востоке), стойко перенося трудности и лишения, застревая потом в таёжных городках и посёлках. Подобно и здесь. Си-шника я ещё могу понять, так как он получает trait-ы (типа interface в Java и C#), на которых можно худо-бедно реализовывать ООП, и ещё некоторые полезные штучки. Но вот что здесь искать программистам других языков, кроме романтики новой экосистемы, в создании которой можно поучаствовать? Мне показалось, что при переходе на Rust большее теряешь, чем находишь. Базовое отличие Rust от других языков — в парадигме работы с кучей (heap). В древних языках всё просто — выделение и освобождение памяти в куче происходит явно операторами типа new/delete. Это С\С++, Паскаль, Фортран и др. Возникает утечка памяти, если delete не вызвать. Потом решили упростить программистам жизнь, избавив от необходимости явно освобождать память. Этот процесс перенесли на уровень так называемых сборщиков мусора, которые сами заботятся об освобождении в нужный момент, программист же только выделяет через new. Это имеет место во всех известных мне современных языках: Java, C#, Python, JavaScritp, PHP и пр. В Rust освобождение памяти происходит тоже автоматически, но сразу при окончании жизни объекта, например, когда он выходит из области видимости. Скажем, если внутри блока создать объект { ... let x = Foo {...это конструктор}; ... }, то память автоматически освободится при выходе управления из этого блока. И вот весь геморрой — из-за этой парадигмы. Приходится вводить понятие владельца объекта, ссылки на владельца, изменяемой (mut) ссылки на владельца, время жизни и другие понятия, порождающие ограничения языка. Скажем, изменяемая ссылка может быть только одна, и вот аналог C# чтения в буфер buf из потока stream.Read(buf, 0, buf.Length) не будет компилироваться, поскольку в первом аргументе buf перезаписывается и должен быть mut-ссылкой, поэтому в третьем аргументе уже никак этот buf использовать нельзя. А вот так можно: int len = buf.Length; stream.Read(buf, 0, len);. Далее опишу решения, которые я использовал в конвертере для перевода кода C# в Rust. Напомню, что именно это и было исходной задачей — конвертирование существующего кода. Несколько лет назад я озаботился тем, чтобы перевести свой SDK на C# в язык Java. Опробованные конвертеры не подошли, так как выдавали на выходе грязный код, который ещё править и править. Да, они заточены на задачу разовой миграции, а мне хотелось иного — продолжать разработку на C# и получать на выходе сразу исполняемый код, работающий эквивалентно. Пришлось писать самому. Этому была посвящена статья UniSharping. Если кратко, то в общем случае невозможно решить эту задачу. Однако, если придерживаться некоторых ограничений при написании кода C#, то невозможное становится возможным. Например, в Java отсутствует аналог оператора yield, так давайте избавимся от него в исходном коде C# — невелика потеря!
С каждым новым поддержанным языком исходный код на C# слегка корректировался. Для Java пришлось отказаться от плагинной техники динамической загрузки DLL, поскольку в Java и других языках понятие сборки просто отсутствует. Для Python пришлось убрать одинаковые названия методов в классе, поскольку сигнатура Питона включает только имя, а типы аргументов у него не указываются. У JavaScript обнаружилось отсутствие типа long (есть byte, short, int, float, double, а вот на long-е разработчики сэкономили), пришлось мне в коде SDK C# заменить все long на int, благо их оказалось немного. В PHP ждала засада в виде представления string как последовательность байт кодировки utf-8 с невозможностью быстрого доступа к i-му символу строки без перебора. Тут я уже ничего у себя переделывать не стал, а использовал их штатные функции mb_, из-за чего производительность получилась чудовищно низкой. В Rust со строками такая же ситуация, но тут я поступил по-другому. Также плодотворной оказалась идея использования директив препроцессора с коде C#, когда нужно в зависимости от языка слегка что-нибудь подправить: #if JAVA || PYTHON… #else… #endif — конвертер понимает такие конструкции. Огромная сложность автоматического перевода — это библиотечные классы и методы. Хорошо, когда есть полный аналог в другом языке, а если нет? Тогда приходится реализовывать на конечном языке эту функциональность и добавлять её в файлы генерируемого кода. Для Rust это пришлось делать более, чем для других языков. Итак, приступим. Выражения C#, операторы ветвления, циклы, функции — для всего этого Rust имеет эквиваленты, тут всё просто. Только циклы for(...; ...; ...) в большинстве случаев приходится разворачивать в while — к этому я был хорошо подготовлен Питоном. С простыми типами byte, int, float и пр. тоже просто, на то они и простые. А вот с непростыми сложнее. Для непростого типа T из C# в Rust следует использовать три разных типа: T (для владельца объекта), &T (неизменяемая ссылка) и &mut T (изменяемая ссылка). Можно считать, что в C# произошло как бы слияние в один тип трёх разных типов — с C# он один, а в Rust это три типа, причём в нужных местах этот тип должен быть одним из этих трёх. Вот как этот фрагмент можно представить в Rust: Итак, принципиальным моментом в Rust является владение экземпляром, которое переходит при присваивании значения: в операторе =, return и передаче аргументом без указания &. После этого предыдущая переменная и все ссылки становятся недействительными, эстафета как бы передаётся другой переменной. А как в коде C# понять в каждом случае, какую из этих трёх разновидностей использовать: T, &T или &mut T? Хороший вопрос, и я его решил так: По умолчанию аргументы методов являются &T или &mut T в зависимости от того, модифицируются ли они или нет в самом методе (это автоматом определяет конвертер), у методов, реализующих property { get; set; } возвращаемые значения &T, всё остальное — T. В коде C# сразу за типом можно указать через комментарий /*&*/ или /*&mut*/ нужный вариант. Например, для списка ссылок подсказка конвертеру будет List<T/*&*/>, а если и сам список является ссылкой, то List<T/*&*/>/*&*/. Здесь некоторые из дочитавших до этого места разочарованно произнесут: мы то думали, что конвертер сам понимает, а ему приходится указывать, ещё и коверкать исходный код нелепыми вставками. Согласен, после разоблачения фокус воспринимается уже не так. Но это — решение, и лучше мне не удалось найти. К тому же оказалось, что в моём случае морфологического блока таких вставок получилось не так уж много. Строки в Rust представляются последовательностью байт кодировки utf-8 (как и в PHP). Думаю, здесь 2 причины. В C#, Java и др. строки есть последовательность символов char размером 16 бит (в Си 8 бит, в Си++ 8 и 16), что есть ограничение с точки зрения разработчиков Rust. Сейчас Unicode уже 32-битный, а вдруг в будущем он вообще станет 64-битным? А они будут к этому готовы. Другая причина субъективная — основатель англоязычный, и его слабо волнуют проблемы за пределами 7-битной ASCII. А в моей переводимой библиотеке идёт интенсивная работа со строками и доступом к её элементам str[i]. Как быть?  Решение — реализовать класс (struct в терминологии Rust), содержащий как вектор символов, так и сам string. Когда нужно работать как массивом элементов, то используется поле chars, иначе — штатный String. Для этой структуры реализованы различные стандартные методы C# работы со строками, если аналогов не находилось в Rust. Например, вот метод Substring(int start, int len) для получения подстроки: А строки-лексемы конвертер представляет так, ссылаясь в коде &STR_HELLO для ссылки или STR_HELLO.clone() для владения: Разумеется, в Rust есть множество типов для работы с коллекциями, но они по ряду причин не подошли. Если сразу писать на Расте, то может и ничего, но транслировать из кода C# оказалось затруднительно, поэтому пришлось как и для строк написать обёртки над Vec и HashMap и использовать их. Причём получилось 3 обёртки для каждого типа в зависимости от типа элементов: для простых типов, для ссылок &T и для владений T. Массивы array[] я транслировал в Rust так же, как и List.
 В Rust нет привычных всем null и базового класса object, практически отсутствует и приведение типов. То, что в C# любой тип можно "упаковать" в object и затем "распаковать" его — для Rust это за пределами возможного. Я не придумал лучшего решения, чем следующее. Если в существующем коде используется object, то как правило в реальности в конкретном месте фигурирует ограниченный набор типов значений для этого object. Поэтому можно создать служебный класс, содержащий в отдельных полях значения этих типов, и использовать его, указав конвертеру в виде подсказки /*=имя*/ после object. Здесь мы знаем, что object принимает значения только int, string и Item, причём это именно ссылка, а не владение Item — им владеют в другом месте. Создаём класс ObjValue, который игнорируется компилятором C#, но который воспринимается конвертером. Да, громоздко. Но ведь это же конвертер генерирует! Главное, что работает.
Обратим внимание: для шарпового obj = cnt.First на Rust получается obj = ObjValue::from_item(Some(Rc::clone(cnt.borrow().get_first().as_ref().unwrap()))). Что говорите, это жесть? Нет, это Раст! Разумеется, человек напишет короче, здесь лишь попытка дать универсальное решение доступа к члену класса. Аналогом класса C# в Rust выступает struct, аналогом интерфейса — trait. Трейты множественно наследуются, структуры — вообще не наследуются. Структура может реализовывать любое число трейтов. То есть как бы в C# убрали наследование от другого класса, но оставили интерфейсы: из ООП полноценной осталась только инкапсуляция. Ну и на том спасибо. Есть какой-то хитрый способ через виртуальные таблицы как-то моделировать наследование и полиморфизм, разобраться в котором мне попросту не хватило мозгов. Я придумал следующий способ. Для класса A, от которого идёт наследование, всегда генерируется один trait, в который переносятся все методы, а для публичных полей генерируется функции get и set (как для property). В struct наследного класса B добавляется экземпляр класса A (struct B { base : A, другие поля }), и этот B также реализует этот trait от A. Причём если нужен доступ к полю или методу A, то используется self.base.x.
Приведу пример из кода реализации двунаправленного списка. Вот работа конвертера (некоторые фрагменты будут удалены для краткости). Это сгенерированный базовый trait. Это реализация базового класса. А вот наследный класс: Обращение к объектам Item и ItemChild везде идёт через ITrait, так что вызов inc() будет именно той функции, объект которой находится за этим trait — а это и есть полиморфизм! Что и требовалось доказать. У каждой ссылки &T должно быть явно или неявно задано так называемое время жизни (lifetime), чтобы не оказалось так, чтобы ссылка жила дольше самого объекта, на который ссылается. Если использовать ссылки в полях структур, то для самой структуры тоже нужно указывать время жизни: struct A<'a> { ref : &'a Item, ... }. При этом получается как бы новый тип, при использовании которого нужно учитывать это 'a. Это должно ещё коррелировать со временем жизни самого объекта. Короче, когда таких ссылок становится много, наступает lifetime-hell, как я его назвал. Да, Rust тоже внёс свой вклад в коллекцию этих хеллов!  Решение подсказали опытные товарищи: использовать конструкцию Option<Rc<RefCell<T>>>. Выше в примерах кода она уже встречалась. И применять правило — если на объекты класса ссылаются в нескольких местах, то использовать только эту конструкцию для ссылок. Хотя и это не гарантирует корректного освобождения памяти, так как при циклической зависимости обратная ссылка должна быть Option<Weak<RefCell<T>>>. "Вот тут, Василий Иванович, я и сломался! — Дурак ты, Петька..." Не стану описывать все нюансы конвертации, но признаюсь, что задачу удалось решить лишь частично. Из всего SDK получилось перевести только блоки морфологии и онтологии, что составляет примерно 10% от планируемого. Остальное оказалось непосильным, да и время закончилось, пора возвращаться к своим лингвистическим задачам. Первый "подход к снаряду" показал, что задача в принципе решается, но требует относительно большой корректировки исходного кода C# — это подсказки конвертеру и переписывание недопустимых для Rust фрагментов. А выигрыш производительности получился у меня всего в два раза. Как язык, Rust не прост, совсем не прост… Советских людей учили, что в жизни всегда есть место подвигу. Программирование на Rust — это, конечно, не подвиг, но что-то героическое в этом есть! Я это оценил на собственной шкуре и приветствую героев! Разработчик