Опыт конвертирования кода C# в код Rust

Постановка задачи


Код на языке C# нужно перевести в код на Rust. Точнее, требуется такая процедура перевода (разработка продолжается на C#), чтобы в любой момент можно было получить работающий код на Rust. Эту задачу я решал для языков Java, Python, JavaScript и PHP, написав конвертер из C# в эти языки. Концепция такого конвертирования была изложена в статье UniSharping пару лет назад. Я разрабатывал этот конвертер, чтобы переводить код своего проекта SDK Pullenti (лингвистический анализ текста). И подумалось мне: а не замахнуться ли на Rust? Да, слышал разные отзывы, что язык необычный и пр., но попытка же не пытка… Тем более, что у одного из заказчиков группа программистов увлечённо пишет на нём.


Сразу скажу, что в полном объёме, как для других языков, этого сделать не получилось — не хватило сил. Может, ещё вернусь к этой задаче. Полтора месяца было потрачено на борьбу с собой и языком, удалось довести конвертер до состояния, что морфологический блок начал переводиться и даже компилироваться (значит, и работать) на Rust-е. Разумеется, за это время модуль морфологии можно было написать с нуля, но за ним маячили ещё около 500 классов C#, создаваемые и отлаживаемые почти 10 лет, а их переписать не так просто. В этой статье хочу поделиться своими впечатлениями от языка Rust, а также описать приёмы, которые я использовал для конвертирования.


Впечатление от языка 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. Напомню, что именно это и было исходной задачей — конвертирование существующего кода.


Ограничения кода C#


Несколько лет назад я озаботился тем, чтобы перевести свой 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 это три типа, причём в нужных местах этот тип должен быть одним из этих трёх.


var obj = new T(); // создали объект класса T
FuncNotModif(obj); // передали аргументом в функцию
FuncModif(obj); // здесь объект модифицируется
list.Add(obj); // добавили в список List<T>
var obj2 = obj; // другая ссылка на тот же объект
var obj3 = obj; // другая ссылка на тот же объект

Вот как этот фрагмент можно представить в Rust:


let obj = T { }; // создали объект класса T (о классах чуть ниже)
func_not_modif(&obj); // передаём неизменяемую ссылку, иначе дальше obj нельзя будет использовать
func_modif(&mut obj); // а здесь модифицирующая ссылка
list.push(&obj); // можно добавлять только в вектор ссылок Vec<&T>, иначе потом obj недоступен
let obj2 : &T = &obj; // другая ссылка на объект
let obj3 : T = obj; // а здесь владение переходит к obj3, и obj с obj2 больше нельзя использовать

Итак, принципиальным моментом в 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.


#[derive(Clone)]
pub struct NString {
    pub chars : Vec<char>,
    pub string : String,
    _is_null : bool
}
impl NString {
    pub fn from_string(s : &String) -> NString {
        NString { chars : s.chars().collect(), string : s.clone(), _is_null : false }
    }
    pub fn from_str(s : &str) -> NString {
        NString { chars : s.chars().collect(), string : s.to_string(), _is_null : false }
    }
    pub fn from_chars(s : &Vec<char>) -> NString {
        NString { chars : s.clone(), string : s.into_iter().collect(), _is_null : false }
    }
...
}

Когда нужно работать как массивом элементов, то используется поле chars, иначе — штатный String. Для этой структуры реализованы различные стандартные методы C# работы со строками, если аналогов не находилось в Rust. Например, вот метод Substring(int start, int len) для получения подстроки:


    pub fn substring(&self, pos : i32, len : i32) -> NString {
        let length : i32 = if len <= 0 { self.chars.len() as i32 - pos } else { len };
        let sub = self.chars[pos as usize .. (pos + length) as usize].to_vec();
        NString::from_chars(&sub)
    }

А строки-лексемы конвертер представляет так, ссылаясь в коде &STR_HELLO для ссылки или STR_HELLO.clone() для владения:


static STR_HELLO : Lazy<NString> = Lazy::new(|| { NString::from_str("Hello!") }); 
use once_cell::sync::Lazy;

Коллекции


Разумеется, в Rust есть множество типов для работы с коллекциями, но они по ряду причин не подошли. Если сразу писать на Расте, то может и ничего, но транслировать из кода C# оказалось затруднительно, поэтому пришлось как и для строк написать обёртки над Vec и HashMap и использовать их. Причём получилось 3 обёртки для каждого типа в зависимости от типа элементов: для простых типов, для ссылок &T и для владений T. Массивы array[] я транслировал в Rust так же, как и List.

Object


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


Если в существующем коде используется object, то как правило в реальности в конкретном месте фигурирует ограниченный набор типов значений для этого object. Поэтому можно создать служебный класс, содержащий в отдельных полях значения этих типов, и использовать его, указав конвертеру в виде подсказки /*=имя*/ после object.


            object/*=ObjValue*/ obj = "Hello";
            Console.WriteLine(obj);
            obj = 10;
            if (obj is int)
            {
                int ii = (int)obj;
                Console.WriteLine(ii);
            }
            obj = cnt.First; // объект класса Item
            if(obj is Item)
                Console.WriteLine((obj as Item).Str);

#if RUST  // компилятор C# игнорирует этот фрагмент
        //RUST object_class
        class ObjValue
        {
            public string Str;
            public int Int;
            public Item/*&*/ Item;
        }
#endif

Здесь мы знаем, что object принимает значения только int, string и Item, причём это именно ссылка, а не владение Item — им владеют в другом месте.


Создаём класс ObjValue, который игнорируется компилятором C#, но который воспринимается конвертером.


        let mut obj : ObjValue = ObjValue::from_str_(STR_HELLO.clone());
        println!("{}", &obj.to_nstring());
        obj = ObjValue::from_int(10);
        if obj.is_class("i32") {
            let mut ii : i32 = obj.int;
            println!("{}", &NString::from_string(&ii.to_string()));
        }
        obj = ObjValue::from_item(Some(Rc::clone(cnt.borrow().get_first().as_ref().unwrap())));
        if obj.is_class("Item") {
            println!("{}", obj.item.as_ref().unwrap().borrow().get_str());
        }

pub struct ObjValue {
    pub str_ : NString, 
    pub int : i32, 
    pub item : Option<Rc<RefCell<dyn IItem>>>, 
    _typ : &'static str
}

impl ObjValue {
    pub fn from_str_(val : NString) -> ObjValue {
        ObjValue { str_ : val, int : 0, item : None, _typ : "NString" }
    }
    pub fn from_int(val : i32) -> ObjValue {
        ObjValue { str_ : NString::null(), int : val, item : None, _typ : "i32" }
    }
    pub fn from_item(val : Option<Rc<RefCell<dyn IItem>>>) -> ObjValue {
        ObjValue { str_ : NString::null(), int : 0, item : val, _typ : "Item" }
    }
    pub fn null() -> ObjValue {
        ObjValue { str_ : NString::null(), int : 0, item : None, _typ : "" }
    }
    pub fn is_null(&self) -> bool { self._typ.len() == 0 }
    pub fn is_class(&self, typ : &str) -> bool { self._typ == typ }
    pub fn to_nstring(&self) -> NString {
        if self._typ == "NString" { return self.str_.clone(); }
        if self._typ == "i32" { return NString::from_string(&self.int.to_string()); }
        if self._typ == "Item" { return NString::from_str("Option<Rc<RefCell<dyn IItem>>>"); }
        NString::null()
    }
}

Да, громоздко. Но ведь это же конвертер генерирует! Главное, что работает.
Обратим внимание: для шарпового 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.
Приведу пример из кода реализации двунаправленного списка.


    //RUST RefCell
    class Item
    {
        public Item(int val) { Val = val; }
        public int Val { get; set; }
        public string Str;
        public Item/*&*/ Prev { get; set; }
        public Item/*&*/ Next { get; set; }
        public virtual void Inc() { Val += 1; }
    }
    //RUST RefCell
    class ItemChild : Item
    {
        public ItemChild(int val) : base(val) { }
        public override void Inc() { Val *= 2; }
    }

Вот работа конвертера (некоторые фрагменты будут удалены для краткости). Это сгенерированный базовый trait.


pub trait IItem {
    fn get_val(&self) -> i32;
    fn set_val(&mut self, value : i32) -> i32;
    fn get_str(&self) -> &NString;
    fn set_str(&mut self, value : NString) -> &NString;
    fn get_prev(&self) -> &Option<Rc<RefCell<dyn IItem>>>;
    fn set_prev(&mut self, value : Option<Rc<RefCell<dyn IItem>>>) -> &Option<Rc<RefCell<dyn IItem>>>;
    fn get_next(&self) -> &Option<Rc<RefCell<dyn IItem>>>;
    fn set_next(&mut self, value : Option<Rc<RefCell<dyn IItem>>>) -> &Option<Rc<RefCell<dyn IItem>>>;
    fn inc(&mut self);
    fn get_base_class(&self) -> &dyn IItem;
    fn is_class(&self, name : &str) -> bool;
    fn as_item(&self) -> &dyn IItem;
    fn as_mut_item(&mut self) -> &mut dyn IItem;
}

Это реализация базового класса.


pub struct Item {
    pub _val : i32, 
    pub m_str : NString, 
    pub _prev : Option<Rc<RefCell<dyn IItem>>>, 
    pub _next : Option<Rc<RefCell<dyn IItem>>>, 
}

impl IItem for Item {
    fn get_val(&self) -> i32 {
        return self._val;
    }
    fn set_val(&mut self, mut value : i32) -> i32 {
        self._val = value;
        return self._val;
    }
    fn get_prev(&self) -> &Option<Rc<RefCell<dyn IItem>>> {
        return &self._prev;
    }
    fn set_prev(&mut self, mut value : Option<Rc<RefCell<dyn IItem>>>) -> &Option<Rc<RefCell<dyn IItem>>> {
        self._prev = utils::clone_opt_ref(&value);
        return &self._prev;
    }
...
    fn inc(&mut self) {
        self.set_val(self.get_val() + 1);
    }
    fn as_item(&self) -> &dyn IItem { self }
    fn as_mut_item(&mut self) -> &mut dyn IItem { self }
    fn get_base_class(&self) -> &dyn IItem { self }
    fn is_class(&self, name : &str) -> bool { name == "Item" }
}

impl Item {
    pub fn new(mut __val : i32) -> Item {
        let mut self_result = Item {  _val : 0,  _prev : None,  _next : None,  m_str : NString::null() };
        self_result.set_val(__val);
        self_result
    }
}

А вот наследный класс:


pub struct ItemChild {
    pub base : Item, // экземпляр базового класса
}
impl IItem for ItemChild {
    fn get_val(&self) -> i32 {
        self.base.get_val()  // вот здесь работа через экземпляр base
    }
    fn set_val(&mut self, value : i32) -> i32 {
        self.base.set_val(value)
    }
    // а это - переопределённая как бы виртуальная функция
    fn inc(&mut self) {
        self.base.set_val(self.get_val() * 2);
    }
    ....
}

impl ItemChild {
    pub fn new(mut __val : i32) -> ItemChild {
        ItemChild {  base : Item::new(__val) };
    }
}

Обращение к объектам 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 — это, конечно, не подвиг, но что-то героическое в этом есть! Я это оценил на собственной шкуре и приветствую героев!

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 62

    +13

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

      0

      Ну если семантически языки похожи то можно в целом, но это явно не про раст и c#)

      +7

      Сделайте меня развидеть это, пожалуйста.

        +1
        Но сегодня же СУББОТА!

        Автогенерация кода на Расте — прекрасная идея, все= ниуя не понятно =)
          0
          Но сегодня же СУББОТА!

          Вот именно, пятница была вчера.

          +1

          Если это коммерческий продукт, то почему он не написан на компилируемом языке и с помощью C API предоставляет интерфейс всем желающим, от PHP до Java?

            0
            Как мне кажется, даже в случае, если просто очень хочется писать на C# и решено делать некие конвертеры под самые разнообразные языки, то стоило попробовать изначально сделать для C\C++\Rust. А полученное уже можно было бы подключить и к php, python'у, и к чему угодно через FFI, сгенерировав заголовочные файлы для всех необходимых языков.

            Да, понимаю, что для какого-нибудь C написать такой конвертер сложнее, чем для Java. Но теперь, если конвертер будет доделан на rust'е, то (наверное) особого смысла поддерживать такое большое количество языков нет, по крайней мере python и C прекрасно умеют в FFI.
              0

              Есть ведь Core RT и il2cpp.

                0

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

                +8

                Это не Rust.

                  +2
                  «и в целом создаётся впечатление, что Rust и C/C++ близки по скорости. Но существенно разнятся по сложности написания кода.»
                  И по сложности отладки.
                    +4
                    пришлось мне в коде SDK C# заменить все long на int, благо их оказалось немного

                    То есть я всё понимаю, но вот это… То есть что вы собираетесь делать если вам этот самый long действительно нужен? Что вы будете делать если вам нужен unsigned тип, а Java их не поддерживает?

                    И самое главное зачем всё это? То есть одноразово для миграции я ещё могу понять. Но зачем такое делать постоянно?
                      0
                      Постоянно — потому что разработка продолжается на C#, и при этом автоматически получаются эквивалентный код на других языках. Для SDK это полезно, чтобы не заморачиваться с интеграцией, а встраивать в свои проекты код на «родном» языке.
                        +5

                        Язык получается не родной. То что вы делаете это perevodite с русского na angliskij kak to taк.

                          0
                          Верно, не совсем родной. Ну и автоматические переводчики типа yandex-транслятора тоже не очень переводят в сложных случаях, тем не менее это лучше, чем вообще ничего. Народ пользуется.
                          +3

                          Ну так а зачем вам нужен этот "эквивалентный код на других языках"? Какую задачу вы этим решаете? Какая задача с вашей точки зрения оправдывает такое "кастрирование" С#? Или это просто самоцель?

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

                              Обычно подобные задачи решают при помощи выставления надлежащего FFI-интерфейса. И да, компонент, который подлежит встраиванию в проекты на разных языках, всё-таки больше имеет смысл писать на нативном языке.

                                +4

                                Вы это сейчас серьёзно? В таком случае надо как минимум тестировать что ваш "эквивалентный код" всё ещё выдаёт "эквивалентный результат".


                                И я действительно не понимаю в чём проблематика интеграции кода на разных ЯП в одном проекте? Мы это регулярно делаем. И сложности подобной интеграции на мой взгляд даже близко не оправдывают то, как вы "кастрируете" С# чтобы ваш способ работал. Это не С# получается а непонятно что…

                                  0
                                  Эквивалентный код действительно выдаёт эквивалентный результат, что гарантируется кучей автотестов (более 1000), которые также переводятся на конечный язык и отрабатывают без ошибок. Да и нет такой уж особой «кастрации», по крайней мере, в моём случае.
                                    +4

                                    Вы смеётесь что-ли? Если у вас какая-то ошибка в вашем "переводчике", то вообще нельзя исключить что "переведённые" автотесты будут проходить на ура, но результат аыполнения функции в той же Java всё равно может отличаться от результата в С#.
                                    Более того я на такое уже натыкался когда у нас джуны "бездумно" портировали код расчёта CRC из С# в Java.


                                    И мне теперь реально интересно что у вас за случай такой, что вам хватает настолько урезанного функционала С#. И нужен ли вам тогда вообще С# как таковой?

                                      0
                                      Я в статье указывал, что у меня SDK (Pullenti) по по лингвистической обработке естественного языка (морфология, семантика, именованные сущности). Так уж исторически сложилось, что он разрабатывается на C#.
                                        +2

                                        Ну вот у вас на странице везде упоминается скорость обработки данных как один из важных критериев.


                                        И даже ни разу не видев вашего кода я готов с вами поспорить что используя нормальный С# вместо вашего урезанного варианта можно будет выжать из системы гораздо больше в этом плане.


                                        Потому что например я уверен что async/await или даже обычное распараллеливание ваш "переводчик" не умеет и поэтому вы их не используете в полном объёме. Если вообще используете…

                                  +5

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


                                  То есть, это как если бы конвертер с другого языка в си шарп делал бы Environment.Exit(1); по малейшему поводу вместо бросания исключения.

                            0
                            Если уж так отказываться от некоторых шарповских плюшек, то почему сразу не взять haxe.org как основной язык разработки?
                            Сборка под js, java, c#, php, python, lua есть из коробки (не без оговорок конечно, но это было бы несравнимо проще, чем писать свой конвертер с нуля).
                            Для rust можно было бы наверное собрать шаред библиотеку из с++ таргета (вопрос поиска инструмента для генерации биндингов из с++).
                              +1
                              Наверное, можно и так поступить, но у меня уже есть код на C#, который создавался (и, главное, отлаживался) в течение почти 10 лет, и уже сразу не возьмёшь этот haxe. Но можно подумать о конвертере C#->Haxe!!!
                                0
                                Да, хороший вариант.
                                –4
                                Очень странный подход, как выше написали уже.
                                Rust ещё не устоялся, изменения выходят с интервалом 1..2 месяца, (пруфы: май, июнь, июль). С точки зрения практичности, Вы потратили время зря, т.к. обратная совместимость в Расте мало поддерживается.
                                Из C#-подобных языков я бы упомянул Vala и D lang.
                                Вам имеет смысл перейти на один из языков, генерирующих нативные бинарники, и гарантирующие C (не C++! ) ABI, и распространять библиотеки в таком виде.
                                Для Python`истов сгенерить обёртки в CFFI или SWIG, Джавистам завернуть в вызовы JNI, и так далее.
                                При транспиляции любого сложного кода Вы не сможете гарантировать одинаковое и надёжное поведение Ваших библиотек.
                                  0
                                  Генерируемый код действительно работает эквивалентно, что гарантируется кучей автотестов (более 1000), которые также переводятся на конечный язык и отрабатывают без ошибок, как и в исходном C#
                                    0
                                    Ясно.
                                    А можете подсказать, что почитать, чтоб легче «врубиться» в обработку естественных языков? Я сейчас игрушечными примерами балуюсь, и хочу плотно заняться NLP.
                                    +1
                                    А есть гарантия, что переведенный на другой язык тест останется корректным?
                                      0
                                      Если тест выдаёт результат, полностью совпадающий с ожидаемым, то это и есть гарантия (не 100%, разумеется, но близко к этому). А для чего же ещё нужны автотесты!?
                                        +1

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

                                          0
                                          Так и есть — для входного текста проверяется именно выход алгоритма, чтобы результат анализа полностью совпадал с ожидаемым. Это не тесты типа Assert.IsTrue(...), а полноценное сравнение сложных структур (например, выделенных сущностей), всё должно полностью совпадать.
                                            +1

                                            А где гарантия, что логика сравнения этих сложных структур перенесена правильно?

                                              0
                                              А где гарантия, что исходный код на C# функционирует правильно?
                                                0
                                                А вы его только автотестами проверяете? И при этом никто не проверяет правильность самих автотестов? Ну то есть хотя бы первый раз?
                                                  0
                                                  Разумеется, проверяет человек. Берётся текст, на нём отрабатывает алгоритм, и человек оценивает (в первый раз, при создании теста), правильный ли результат. После утверждения тест является рабочим.
                                                    0
                                                    То есть на C# у вас код проверен «вручную», а на Java нет.

                                                    При этом нет гарантий что ваши автотесты действительно правильно «переводятся» на Java и что «логика сравнения этих сложных структур перенесена правильно».
                                                      0
                                                      Ну если на Java те же тесты для тех же текстов получают одинаковый результат, с точностью до байта, как на C#, то куда уж правильнее.
                                                        0
                                                        Так вы всё-таки каждый тест проверяете что он на Java «получает одинаковый результат, с точностью до байта»? Или вы просто это предполагаете на основании того что они ошибки не выдают?
                                                          0
                                                          Разумеется, проверяется результат с точностью до байта, а не true\false или сам факт запуска.
                                                            0
                                                            Как и кем проверяется? «Вручную» каждый тест после «перевода»?

                                                            Ещё раз: если у вас ошибка где-то в «переводчике», то у вас может такое получится что функция будет неправильно «считать байты» и автотест будет неправильно «считать байты». Но при этом результаты функции и автотеста между собой будут совпадать.
                                                              0
                                                              Автотест содержит пару {T, R0}, где T — текст, R0 — эталонный результат (сущности с их атрибутами). Пусть R1 = A(T) — результат применения алгоритма A к тексту T. Тест считается удачным, если R0 = R1. Проверка сравнения строк идёт штатными функциями конечного языка, тут ошибки быть не может. Если R0 = R1 на всех конечных языках, то конвертер работает неплохо. Как минимум, результирующие SDK одинаково функционируют на автотестах.
                                                                +1
                                                                Автотест содержит пару {T, R0}, где T — текст, R0 — эталонный результат (сущности с их атрибутами). Пусть R1 = A(T) — результат применения алгоритма A к тексту T. Тест считается удачным, если R0 = R1.

                                                                И это по вашему не «тесты типа Assert.IsTrue»?..

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

                                                                Ещё как может. Сравнение строк в различных языках или даже в одном языке при разных «культурах» это та ещё песня…

                                                                Если R0 = R1 на всех конечных языках, то конвертер работает неплохо.

                                                                «Неплохо» я вижу. Но на мой взгляд такого «неплохо» достаточно мало чтобы довериться одним только автотестам.
                                                                  0
                                                                  Результат — это сущности с атрибутами, две сущности равны, если у них атрибуты полностью совпадают. Например, для персоны к атрибутам относятся фамилия, имя, отчество, должность и пр. Значения атрибутов могут быть как строковыми, так и ссылками на другие сущности, то есть имеем а общем случае на выходе граф. И вот эти конструкции должны полностью совпадать.
                                    +4
                                    Rust ещё не устоялся, изменения выходят с интервалом 1..2 месяца
                                    Вы потратили время зря, т.к. обратная совместимость в Расте мало поддерживается

                                    Релизы компилятора выходят раз в шесть недель. Но какое отношение релизы компилятора имеют к совместимости? С релиза версии 1.0 в 2015 году гарантируется обратная совместимость на уровне исходников.

                                      –1
                                      гарантируется обратная совместимость

                                      Спасибо, не знал.
                                      Но, вообще-то, при выходе новых версий компилятора ошибки / баги возможны.
                                        0

                                        Сам не пробовал, но вроде бы в проекте можно, по аналогии с версиями библиотек, зафиксировать и версию компилятора/языка.

                                          +1

                                          Да, в директорию проекта просто подкладывается конфигурационный файл с указанной версией. На практике все крупные проекты, с которыми я работал, используют stable версию языка – все фичи в которой сначала неопределенное время тестировались в nightly канале, затем 6 недель в beta.

                                          +1

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

                                      0

                                      Как-то ради интереса попробовал взять код lua и чем-то готовым сконвертировать в rust. Получился рабочий, но очень страшный код с unsafe и указателям на каждом шагу. Потом я пришёл к выводу, что в любом случае предстоит огромная работа по переписыванию на идиооматичный раст, и местами возникал соблазн поменять архитектуру. Так как делалось всё из интереса, до конца доводить не стал.
                                      По ощущениям, концепция владения очень сильно влияет на способ написания кода и реально усложняет конвертацию.
                                      Прикольно, что автор пошёл с другой стороны и поменял код до конвертации, а не после.

                                        +7
                                        Базовое отличие Rust от других языков. Это С\С++, Паскаль, Фортран и др. Возникает утечка памяти, если delete не вызвать.

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

                                        Это называется RAII, и в C++ естественно эта фича есть.


                                        Си++, Java, Python и пр., но когда оказывается, что после добавления в список объект нельзя использовать: it = new ...(); list.add(it); it.val = ..., а вот так можно: it = new ...(); it.val = ...; list.add(it);, то это обескураживает

                                        В C++ это тоже есть, move constructor. Использование объекта после std::move конечно не вызовет ошибку компиляцию, но вот падение программы может.


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

                                          +2
                                          В C#, Java и др. строки есть последовательность символов char размером 16 бит (в Си 8 бит, в Си++ 8 и 16), что есть ограничение с точки зрения разработчиков Rust. Сейчас Unicode уже 32-битный, а вдруг в будущем он вообще станет 64-битным? А они будут к этому готовы.

                                          И UTF-8, и UTF-16 это кодировки с переменной длинной символа — в обеих кодировках максимальная длина символа 32 бита. Непонятно, где здесь ограничение.
                                          UTF-8 была скорее всего выбрана из-за её большей популярности (даже Windows не так давно стал её поддерживать)
                                            0

                                            Думаю не столько из-за популярности, сколько из соображения удобства и эффективности: ASCII-текст в UTF-8 кодируется только одним байтом, а не двумя, как в UTF-16.

                                              0
                                              Выбор UTF-8 обусловлен исторически, тем, что это сейчас стандартная кодировка для 32-битного юникода, ее принимают все, кто переходил на юникод после того, как он стал 32-битным.
                                              Системы, перешедшие на юникод раньше других, когда он был 16-битным, переходили на 16-битный вариант, чтобы сохранить сишную строку, как последовательность char'ов, увеличив ее размер «всего» в два раза. Windows NT была среди этих операционок и в то время это была революция, в положительном смысле этого слова.
                                              Однако опыт этих систем выявил много проблем 16-битного представления: только LE/BE чего стоит. А потом и юникод стал 32-битным, и все последующие операционные системы и языки выбирали UTF-8, как единственную систему, одинаковую в любой архитектуре, не содержащую непредставимых в ASCII байтов и оптимизирующую память. Про это даже Джоел писал…
                                            +9
                                            Rust и C# слишком разные языки, чтобы написать сколь угодно рабочий конвертер между ними. То что считается идиоматичным в C#, считается антипаттерном в Rust. Динамический полиморфизм в расте хоть и есть, но он убивает производительность, так как компилятор не может zero-cost оптимизировать его.
                                            Или чтобы реализовать перекрёстные ссылки между объектами класса Foo, нужно использовать конструкцию Option<Rc<RefCell<Foo>>>, а для доступа к полю val этого класса вызывать foo.unwrap().borrow().val.

                                            Простое правило написания кода на Rust: если вы используете RefCell, скорее всего вы делаете что-то не так. RefCell выносит проверки заимствования в runtime, что опять же негативно влияет на оптимизации.
                                            Выскажу своё личное мнение: мне представляется, что прогресс в области программирования идёт в направлении оптимизации труда программиста, чтобы меньшими усилиями достигать большего эффекта. Случай Rust уникален тем, что не вписывается в эту тенденцию. Удивительным для меня фактом является рост его популярности (сейчас Rust вошёл в 20-ку). Но почему? Вот вопрос.

                                            Случай Rust в некоторых случаях полагает написание дополнительного кода для проверки его компилятором с целью обеспечения безопасности программы (те же lifetimes), в некоторых случаях наоборот к уменьшению кода (например тип-суммы отлично справляются с заменой паттерна visitor в ООП, уменьшая количество кода в разы). Ваша проблема в том, что вы писали неидиоматичный код. Хотя это скорее проблема проекта, так как транслировать идиоматичный C# в идиоматичный Rust кажется мне невозможным.
                                            По производительности на моей задаче Rust не произвёл впечатления — выигрыш по сравнению с C# получился всего в 2 раза.

                                            Удивлен что с тем кодом который вы показали ниже, у вас вообще получилась прибавка к производительности.
                                            В Rust освобождение памяти происходит тоже автоматически, но сразу при окончании жизни объекта, например, когда он выходит из области видимости. Скажем, если внутри блока создать объект {… let x = Foo {… это конструктор};… }, то память автоматически освободится при выходе управления из этого блока.

                                            Уточню: в данном случае память выделяется на стеке, а не на куче. Для выделения на куче следует использовать сырые или умные указатели, вроде Box, Rc, Arc.
                                            Решение — реализовать класс (struct в терминологии Rust), содержащий как вектор символов, так и сам string.

                                            Решение крайне плохое. Вы по сути аллоцируете два раза все ваши NString, так как Clone::clone копирует все данные. Клонирование в таких случаях будет больно бить по производительности, и может считаться неидиоматичным применением. Возможно, следовало бы создать свой тип строки, либо же поискать готовые решения на crates.io.
                                            Например, вот метод Substring(int start, int len) для получения подстроки:

                                            Вопрос, нужно ли вам в данном примере владение NString на выходе. Я думаю, что скорее всего не нужно было, достаточно было возвращать &str.
                                            Причём получилось 3 обёртки для каждого типа в зависимости от типа элементов: для простых типов, для ссылок &T и для владений T.

                                            Скорее всего достаточно один раз было написать коллекцию, с использование generic-параметра. Какой-то go-style у вас вышел.
                                            В Rust нет привычных всем null и базового класса object, практически отсутствует и приведение типов. То, что в C# любой тип можно «упаковать» в object и затем «распаковать» его — для Rust это за пределами возможного.

                                            На этом моменте Вас должно было озарить, что писать конвертер в Rust из c# — плохая идея. То что вы нагородили ниже крайне плохо оптимизируется компилятором, и выносит кучу ненужных проверок в runtime.
                                            Обратим внимание: для шарпового obj = cnt.First на Rust получается obj = ObjValue::from_item(Some(Rc::clone(cnt.borrow().get_first().as_ref().unwrap()))). Что говорите, это жесть? Нет, это Раст!

                                            Говорите это Rust? Нет, это попытка натянуть сову на глобус. В обычном Rust-коде за такое оторвали бы руки.
                                            Аналогом класса C# в Rust выступает struct

                                            Не совсем корректно. В Rust нет понятия класса. Аналогом struct из Rust'a в C# скорее всего выступит тот же самый struct, с некоторыми различиями.

                                            Ваш IItem с динамическим полиморфизмом это также крайне неидиоматичный код. Мало того, что динамический полиморфизм следует использовать только в случаях с крайней необходимостью, когда ничего другого уже не работает, так в данном случае достаточно было бы использовать generic-параметр, что делается в коллекциях и в самом C#.
                                            Короче, когда таких ссылок становится много, наступает lifetime-hell, как я его назвал. Да, Rust тоже внёс свой вклад в коллекцию этих хеллов!

                                            Это если бездумно вставлять lifetimes, не собо понимая зачем они нужны.

                                            В целом по статье сложилось впечатление что автор незнаком не только с Rust, но и с C#. Как я знаю, использование object в C# тоже не считается хорошим тоном. Также на лицо неумение автора применять generic-параметры, которые также присутсвуют и в C#.
                                            Вопрос к автору, а вы Rustbook открывали хотя бы?..
                                              –3
                                              Постараюсь ответить на последний абзац. Rustbook я открывал, знакомство с Rust весьма поверхностное, ограниченное месяцем, реальных проектов на нём не делал. С языком C# знаком чуть больше. Да, я сам стараюсь object не использовать, но иногда без него никак. Например, object Tag в классах WinForm (это пришло ещё из Delphi, кажется), чтобы пользователь мог записать туда что угодно. Про generic я тоже кое-что слышал, даже иногда использую их, когда настроение хорошее :).
                                              +2
                                              У любого языка есть своя идеология и концепция, диктующие то, как с использованием этого языка предполагается решать задачи. Писать автоматический конвертер не прочувствовав этих концепций чревато тем, что на выходе вы получите монстра Франкенштейна, который вызовет у всех только чувство отторжения. Если уж вы решили создать монстра Франкенштейна, то лучше абстрагироваться от желания проецировать отношение к своему детищу на прежних владельцев частей тела, из которых он собран.
                                                –2
                                                Ну когда Quake3 на Rust переводили автоматически, тоже красиво не получилось. Хоть и люди с опытом.
                                                  0
                                                  Вы правы, но в случае генерируемого SDK пользователю не надо туда влезать и разбираться с кодом, а достаточно использовать внешние функции, которые для Java и JavaScript мало отличаются от оригинала, для Python и Rust получается немного коряво.
                                                  Да и любой переводчик с естественного языка на язык представляется таким «Франкенштейном», особенно на ранних этапах своего развития. Понятно, что носитель обоих языков переведёт лучше. Но это дорого, и где их взять то, на всех?

                                                Only users with full accounts can post comments. Log in, please.