Как мы автоматизировали портирование продуктов с C# на C++

    Привет, Хабр. В этом посте я расскажу о том, как нам удалось организовать ежемесячный выпуск библиотек для языка C++, исходный код которых разрабатывается на C#. Речь идёт не об управляемом C++ и даже не о создании моста между неуправляемым C++ и средой CLR — речь об автоматизации генерации кода на C++, повторяющего API и функциональность оригинального кода на C#.

    Необходимую инфраструктуру, обеспечивающую трансляцию кода между языками и эмуляцию функций библиотеки .Net, мы написали сами, решив таким образом задачу, которая обычно считается академической. Это позволило начать выпускать ежемесячные релизы дотнетовских продуктов и для языка C++ тоже, получая код каждого релиза из соответствующей версии кода C#. При этом тесты, которыми был покрыт оригинальный код, портируются вместе с ним и позволяют контролировать работоспособность получившегося решения наравне со специально написанными тестами на C++.

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

    Предыстория


    Изначально наша компания занималась выпуском библиотек для платформы .Net. Эти библиотеки, в основном, предоставляют API для работы с некоторыми форматами файлов (документы, таблицы, слайды, графика) и протоколами (электронная почта), занимая определённую нишу на рынке подобных решений. Вся разработка велась на языке C#.

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

    Было решено разбить решение на две части. Первая — так называемый Портер — осуществляла бы преобразование синтаксиса исходного кода C# в Java, попутно заменяя типы и методы .Net их аналогами из библиотек Java. Вторая — Библиотека — эмулировала бы работу тех частей библиотеки .Net, для которых установить прямое соответствие с Java затруднительно или невозможно, привлекая для этого доступные сторонние компоненты.

    В пользу принципиальной реализуемости подобного плана говорило следующее:

    1. Идеологически языки C# и Java достаточно похожи — как минимум, структурой типов и организацией работы с памятью;
    2. Речь шла о портировании библиотек, необходимости в переносе GUI не было;
    3. Данные библиотеки содержали, в основном, бизнес-логику и низкоуровневые файловые операции, а самыми сложными их зависимостями зачастую были System.Net и System.Drawing;
    4. Библиотеки изначально разрабатывались так, чтобы работать под максимально широким спектром версий .Net (как Framework, так и Standard и даже Xamarin), так что различия платформ можно было в большой степени игнорировать.

    Не буду вдаваться в подробности, поскольку они заслуживают отдельной статьи (и не одной). Скажу лишь, что от запуска разработки до выхода первого продукта на Java прошло около двух лет, и с тех пор выпуск продуктов на Java стал регулярной практикой компании. За время развития проекта портер прошёл эволюцию от простой утилиты, преобразующей текст по установленным правилам, до сложного кодогенератора, работающего с AST-представлением исходного кода. Библиотека также обросла кодом.

    Успех направления Java обусловил желание компании проводить дальнейшую экспансию на новые для себя рынки, и в 2013 году был поставлен вопрос о выпуске продуктов для языка C++ по аналогичному сценарию.

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


    Для того, чтобы обеспечить выпуск плюсовых версий продуктов, требовалось создать фреймворк, который позволял бы получить из произвольного кода на C# код на C++, скомпилировать его, проверить и отдать клиенту. Речь шла о библиотеках объёмом от нескольких сотен тысяч до нескольких миллионов строк (не считая зависимостей).

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

    Разумеется, эта сложность была управляемой — как минимум, путём переноса в итоговый Java-код только патчей, вычисляемых как разность между выводом портера за две последующие ревизии кода C#. Такой подход позволял исправлять каждую портированную строку лишь один раз и в дальнейшем использовать уже доработанный код там, где изменений не вносилось. Тем не менее, при разработке плюсового портера была поставлена цель избавиться от этапа исправления портированного кода, вместо этого исправляя сам фреймворк. Таким образом, каждая, сколь угодно редкая ошибка трансляции исправлялась бы один раз — в коде портера, и этот фикс относился бы ко всем будущим релизам всех портируемых продуктов.

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

    1. Эмуляция окружения .Net в той мере, в какой это нужно для работы портированного кода;
    2. Адаптация портированного кода C# к реалиям C++ (структура типов, управление памятью, прочий сервисный код);
    3. Сглаживание различий между «переписанным C#» и собственно C++, чтобы упростить использование портированного кода программистами, не знакомыми с парадигмами .Net.

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

    Многие читатели сразу спросят о том, почему было не использовать существующие реализации вроде Mono. К тому были свои причины.

    1. Привлечением такой готовой библиотеки удалось бы удовлетворить только первому требованию, но не второму и не третьему.
    2. Реализация Mono написана на C# и, стало быть, полагается на рантайм, от которого мы как раз и отказались.
    3. Адаптация стороннего кода под наши реалии (API, система типов, система управления памятью, оптимизации кода C++, и так далее) заняла бы время, сопоставимое с разработкой собственного компонента.
    4. Потребности наших продуктов существенно уже, чем полная реализация .Net, однако отделить нужные классы и методы от ненужных при прямом переносе кода во многих случаях было бы сложно. Таким образом, пришлось бы потратить существенное время на поддержку тех классов и вызовов, которые не используются в коде продуктов.

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

    В итоге было принято решение о разработке библиотеки как набора адаптеров, предоставляющих доступ к функциям, уже реализованным в сторонних библиотеках, но через .Net-подобный API (по аналогии с Java). Это позволило бы сократить работу и использовать готовые, уже оптимизированные, компоненты C++.

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

    Ещё одним требованием стала возможность портирования не только библиотек, но и тестов к ним. Компания может похвастаться высокой культурой тестового покрытия своих продуктов, и возможность запускать на C++ те же тесты, что были написаны для оригинального кода, существенно упростила бы поиск проблем после трансляции.

    Остальные требования (формат запуска, покрытие тестами, технологии и т. п.) касались, в основном, способов работы с проектом и над проектом. Я не буду останавливаться на них.

    История


    Прежде чем продолжить, придётся сказать несколько слов о структуре компании. Компания работает удалённо, все команды в ней — распределённые. За разработку некоторого продукта обычно отвечает команда, объединённая языком (почти всегда) и географией (в основном).

    Активная работа над проектом началась осенью 2013 года. Из-за распределённой структуры компании, а также ввиду некоторых сомнений в успехе разработки был дан старт сразу трём версиям фреймворка: две из них обслуживали по одному продукту, третья покрывала сразу три. Предполагалось, что это позволит затем прекратить разработку менее эффективных решений и перераспределить ресурсы, если это потребуется.

    В дальнейшем к работе над «общим» фреймворком подключились ещё четыре команды, две из которых позже пересмотрели своё решение и отказались от выпуска продуктов для C++. В начале 2017 года было принято решение о прекращении разработки одного из «индивидуальных» решений и переводе соответствующей команды на работу с «общим» фреймворком. Остановленная разработка предполагала использование Boehm GC в качестве средства управления памятью и содержала значительно более богатую реализацию некоторых частей системной библиотеки, которая была затем перенесена в «общее» решение.

    Таким образом, к финишу — то есть, к релизу портированных продуктов — пришли две разработки: одна «индивидуальная» и одна «коллективная». Первые релизы на основе нашего («общего») фреймворка случились в феврале 2018 года. Впоследствии релизы всех шести команд, использующих данное решение, стали ежемесячными, а сам фреймворк был выпущен в качестве отдельного продукта компании. Поднимался даже вопрос о том, чтобы сделать его опенсорсным, но развития эта дискуссия на сегодняшний день не получила.

    Команда, продолжавшая самостоятельную работу над аналогичным фреймворком, также выпустила свой первый релиз на C++ в 2018 году.

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

    Организация работы над проектом


    Организация совместной работы над проектом несколькими командами успела претерпеть значительные изменения. Первоначально было решено, что за разработку, поддержку и фиксинг фреймворка будет отвечать одна многочисленная — «центральная» — команда, в то время как малочисленные «продуктовые» команды, занимающиеся выпуском конечных продуктов на C++, будут отвечать, в основном, за попытки портирования своего кода и предоставление обратной связи (информации об ошибках портирования, компиляции и выполнения). Такая схема, однако, оказалась непродуктивной, поскольку центральная команда была перегружена заявками всех «продуктовых» команд, а те не могли двигаться дальше, пока встреченные ими проблемы не будут решены.

    По причинам, во многом не зависящим от состояния данной конкретной разработки, было принято решение о расформировании «центральной» команды и переводе людей в «продуктовые» команды, которые теперь сами были ответственны за исправление фреймворка под свои нужды. При этом каждая команда сама принимала бы решение о том, использовать ли ей общие наработки или порождать собственный форк проекта. Такая постановка вопроса была актуальна для Java-фреймворка, код которого был к тому времени стабилен, однако для скорейшего наполнения библиотеки C++ требовалась консолидация усилий, так что команды по-прежнему работали сообща.

    Такая форма работы также имела свои недостатки, поэтому в дальнейшем была проведена ещё одна реформа. «Центральная» команда была восстановлена, хоть и в меньшем составе, но с иными функциями: теперь она отвечала не за собственно разработку проекта, а за организацию совместной работы над ним. Это включало поддержку среды CI, организацию практики Merge Requestов, проведение регулярных собраний с участниками разработки, поддержку документации, покрытие тестами, помощь в выработке архитектурных решений и поиске проблем, и так далее. Кроме того, команда взяла на себя работу по устранению технического долга и другим ресурсоёмким направлениям. В таком режиме разработка продолжается и по сей день.

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

    Автор этих строк присоединился к компании в середине 2016 года, начав работать в одной из команд, транслировавших свой код с использованием «общего» решения. Зимой того же года, когда было принято решение о воссоздании «центральной» комадны, я перешёл на позицию её тимлида. Таким образом, мой опыт в проекте на сегодняшний день составляет более трёх с половиной лет.

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

    Технологии


    Настало время поговорить о технологиях, используемых в проекте. Портер представляет собой консольное приложение, написанное на языке C#, поскольку в таком виде его проще встраивать в скрипты, выполняющие задачи типа «портировать-скомпилировать-прогнать тесты». Кроме того, имеется GUI-компонент, позволяющий достигать тех же целей щелчками на кнопках.

    За синтаксический разбор кода и разрешение семантики отвечает древняя библиотека NRefactory. К сожалению, на момент начала проекта Roslyn ещё не был доступен, хотя миграция на него, разумеется, стоит у нас в планах.

    Портер использует проходы по AST-дереву для сбора информации и генерации выходного кода на C++. При генерации кода C++ AST-представление не создаётся, и весь код сохраняется в виде простого текста.

    Во многих случаях портеру требуется дополнительная информация для тонкой настройки. Такая информация передаётся ему в виде опций и атрибутов. Опции применяются ко всему проекту сразу и позволяют задать, к примеру, имена макросов экспорта членов классов или определения препроцессора C#, используемые при анализе кода. Атрибуты навешиваются на типы и сущности и определяют обработку, специфичную для них (например, необходимость генерации ключевых слов «const» или «mutable» для членов классов или исключения их из портирования).

    Классы и структуры C# транслируются в классы C++, их члены и исполняемый код транслируются в ближайшие эквиваленты. Generic-типы и методы отображаются на шаблоны C++. Ссылки C# транслируются в умные указатели (сильные или слабые), определённые в Библиотеке. Более подробно о принципах работы портера будет рассказано в отдельной статье.

    Таким образом, исходная сборка C# преобразуется в проект на языке C++, который вместо библиотек .Net зависит от нашей общей библиотеки. Это показано на следующей диаграмме:



    Для сборки библиотеки и портированных проектов используется cmake. На данный момент поддерживаются компиляторы VS 2017 и 2019 (Windows), GCC и Clang (Linux).

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

    • Skia — для работы с графикой;
    • Botan — для поддержки функций шифрования;
    • ICU — для работы со строками, кодировками и культурами;
    • Libxml2 — для работы с XML;
    • PCRE2 — для работы с регулярными выражениями;
    • zlib — для реализации функций сжатия;
    • Boost — для различных целей;
    • несколько других библиотек.

    Как портер, так и библиотека покрыты многочисленными тестами. Тесты библиотеки используют фреймворк gtest. Тесты портера написаны, в основном, на NUnit/xUnit и разбиваются на несколько категорий, удостоверяя, что:

    • вывод портера на данных входных файлах совпадает с целевым;
    • вывод портированных программ после их компиляции и запуска совпадает с целевым;
    • тесты NUnit из входных проектов успешно преобразуются в тесты gtest в портированных проектах и проходят;
    • API портированных проектов успешно работает в C++;
    • влияние отдельных опций и атрибутов на процесс трансляции соответствует ожидаемому.

    Для хранения исходного кода мы используем GitLab. В качестве среды CI выбран Jenkins. Портированные продукты доступны в виде пакетов Nuget и в виде архивов для скачивания.

    Проблемы


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

    1. Различия в системе типов между .Net и C++.
      Начнём с того, что в C++ нет аналога типа Object, а для большинства библиотечных классов отсутствует RTTI. Одно это уже ставит крест на возможности напрямую отобразить типы .Net на типы STL.
    2. Высокая сложность алгоритмов портирования.
      В то время как простые случаи транслируются элементарно, по мере работы с портированным кодом обнаруживается большое количество нюансов, существенно усложняющих процесс кодогенерации. Например, в C# порядок вычисления аргументов метода определён, а в C++ — нет.
    3. Сложность поиска ошибок.
      Отладка портированного кода — отдельный вид искусства. Нюансы вроде описанного выше могут существенно влиять на работу программы, вызывая труднообъяснимые ошибки. С другой стороны, они могут оставаться незамеченными и долгое время присутствовать в виде скрытых багов.
    4. Различия в системе управления памятью.
      В C++ нет уборки мусора, и нам приходится тратить много сил на то, чтобы заставить портированный код вести себя подобно оригиналу.
    5. Необходимость дисциплины для программистов C#.
      Программистам C# необходимо свыкнуться с ограничениями, налагаемыми процессом трансляции кода на C++. Есть несколько типов ограничений, с которыми им приходится сталкиваться:

      • Ограничения на версию языка, понимаемую нашим синтаксическим анализатором;
      • Запрет на использование конструкций, в настоящее время не поддерживаемых портером (например, мы пока не умеем транслировать оператор yeild);
      • Ограничения, следующие из структуры портированного кода (например, для любого поля ссылочного типа должен существовать ответ на вопрос о том, является ли данная ссылка сильной или слабой, что не всегда так для кода на C#);
      • Ограничения, накладываемые языком C++ (например, в C# статические переменные не удаляются до завершения всех foreground-потоков).
    6. Большой объём работы.
      Очевидно, что даже используемое нашими продуктами подмножество библиотеки .Net достаточно велико, и на реализацию всех классов и методов уходит много времени.
    7. Особые требования к разработчикам.
      Из-за того, с какими задачами мы работаем, на собеседованиях нам приходится задавать те самые «школьные» вопросы, которые так обижают многих программистов. Практика показывает, что человек, недостаточно любознательный, чтобы начать рассуждать о том, как устроен внутри оператор using, не справится с нашими задачами независимо от своего опыта по реализации рутинной бизнес-логики. Необходимость знать оба языка или готовность успешно разобраться в одним из них также снижает количество доступных кандидатов. С другой стороны, любители теории компиляторов или других экзотических дисциплин легко находят в проекте свою нишу.
    8. Хрупкость системы.
      Несмотря на наличие тысяч тестов и сотен тысяч строк кода, на котором тестируется портер, периодически возникают проблемы, когда изменения, внесённые одной командой, ломают процесс портирования и/или компиляции у другой команды из-за редко используемых синтаксических конструкций или попросту разных стилей входного кода.
    9. Высокий порог вхождения в проект.
      Большинство наших задач требует глубокого анализа. Обилие подсистем и сценариев ведёт к ситуации, когда каждая новая задача требует длительного знакомства с новыми гранями проекта. Не всем разработчикам нравится, когда изучать продукт приходится раз за разом, многие чувствуют себя комфортнее в ситуации, когда вхождение случается лишь однажды.
    10. Сложности с защитой интеллектуальной собственности.
      Если код C# достаточно легко обфусцируется коробочными решениями, то в C++ для этого приходится прилагать дополнительные усилия, поскольку многие члены классов не могут быть удалены из заголовочных файлов без последствий. Трансляция обобщённых классов и методов в шаблоны также создаёт уязвимость, обнажая алгоритмы.

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

    Резюме


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

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

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

    Комментарии 27

      +4
      Спасибо за статью.
      Может, я невнимательно читал, но не увидел обоснования именно такого решения. Можно как-то прорезюмировать коротко? Я бы сервер скорее писал на C#, который предоставит весь функционал с помощью открытого и документированного протокола. А на Java потом только клиент. Что-то на подобие баз данных SQL. Немного громоздко, но по-моему менее громоздко чем портирование миллионов строк кода на плюсы и кодогенерация.
        +2
        Здравствуйте. Речь шла о том, чтобы предоставить продукт «чисто Java» или «чисто C++», который позволял бы пользоваться всей функциональностью решения без необходимости зависеть от сервера. Серверные версии с SDK под разные языки у нас, разумеется, и так продаются в виде отдельных продуктов (как self-hosted, так и cloud). Не все хотят ими пользоваться, не все готовы для решения задачи вида «записать pdfку» поднимать сервер — некоторые хотят просто подключить библиотеку к своему продукту и пользоваться ею.
        0
        А проводились ли какие-либо тесты производительности и потребления ресурсов? Это ведь очень интересно, имея два три функционально одинаковых кода, узнать как С#, C++ и Java с ними справляются…
          0
          Да, разумеется. Как правило, сначала портируется «как есть», а потом начинаются оптимизации (как библиотеки, так и генерированного кода). Поскольку код изначально оптимизирован для C#, порты, как правило, работают медленнее (очень сложно подобрать адекватные тесты, но на том, что мы видели, замедление может происходить в разы, вплоть до одного порядка). Когда это становится критичным, происходит оптимизация — так, нам пришлось отказаться от boostовских регулярок из-за того, что в .Net всё работает намного быстрее.
            0
            а замедление — портирование библиотек и рантайма подвело, или сам по себе язык при переносе страдал? ну вот вы сказали что бустовые регулярки медленнее дотнет-ных, а если какой-то счётный алгоритм? или там сортировка не библиотечная?
              0
              Несколько факторов. В случае с регулярками — да, дело было чисто в оптимизации библиотек. Это только один случай, в .Net много специфических оптимизаций, на которые нужно наступить, чтобы о них узнать.

              В случае с портированным кодом — проседания могут быть из-за разницы в механизмах работы с памятью. В C++ нет сжатия кучи, зато есть оверхед при копировании указателей (конструкторы-деструкторы, подсчёт ссылок) — из-за этого код, который часто создаёт много мелких объектов, будет работать медленнее после портирования. Насколько — полностью определяется соотношением рабочей нагрузки и операций по созданию объектов и работе с указателями. Счётные алгоритмы выглядят примерно одинаково на обоих языках, и с ними особой разницы мы не замечали — в основном всё упирается или в библиотеки, или в структуру языка.
          +4
          Для затравки могли бы какой-нибудь helloworld с C# на C++ перевести, дабы продемонстрировать возможности платформы. Без разбора, вы его в следующей части обещали :)

          Без реальных примеров преобразования кода эта статья смотрится несколько… пиарно, чтоль. Но спасибо, интересно таки. Ждем продолжения.
            +2
            Спасибо за замечание. Да, в следующей статье будут примеры, эта и так получилась довольно большая.
            0
            Задача «писать один раз — транспилировать на десяток языков» уже решена Haxe. Но «своё» решение всегда ближе и приятнее.
              0
              Это если изначально разработка на нём велась. У нас задача стояла перенести десятки миллионов уже готового кода на C#.
                0
                *десятки миллионов строк.
                  0
                  Да, но, вы уже сделали транспилятор в Java. Гораздо проще было бы сделать транспилятор в Haxe (т.к он более выразительный чем C#) и потом уже из Haxe во все остальное. И продолжить разработку на Haxe. И это было бы дешевле, чем продолжать портировать код на Х языков, даже имея тулинг для облегчения процесса.

                  Сразу контр-аргумент по поводу «у нас никто не пишет на Haxe». Он очень похож на C#, у всех кто пришел в Haxe из C# или Java не было проблем с пониманием, как оно работает или проблем с адаптацией.
                    +1
                    Спасибо за пояснение. Похоже, данный проект прошёл мимо нас уже потому, что поддержка C# и Java была добавлена в 2012 году, т. е. через 5 лет после выхода наших первых продуктов на Java (которые, в свою очередь, отставали на несколько лет от таковых для .Net). Нужно будет познакомиться с ним поподробнее.

                    С ходу я вижу следующие риски при использовании Haxe, которых нет в текущей нашей парадигме:

                    1. Сложности с сохранением структуры типов. Поскольку речь идёт о библиотеках, API после портирования C# > Haxe > C# должен оставаться тем же самым с точностью до всех библиотечных типов и интерфейсов. В противном случае это повлечёт необходимость изменения кода клиентов, чего, естественно, требуется избегать всеми возможными способами. Это относится не только к .Net, но и к API уже вышедших продуктов для других языков.
                    2. Миграция с C# на Haxe должна быть выполнена идеально с первого раза (сейчас проблемы с портированным кодом никак не влияют на релизы для .Net), и до её завершения возможности начинать покрытие других языков нет.
                    3. Утрата истории изменений кода, насчитывающего десятки миллионов строк и сотни человеколет разработки.
                    4. Все подсистемы должны работать в точности одинаково на всех языках и в точности как в исходном коде .Net. Например, наши юзкейзы требуют, чтобы генерация XML или графики в портированном коде была полностью аналогична таковой в .Net.
                    5. Непонятно, что будет при переезде с дотнетоспецифическими вещами вроде встраиваемых ресурсов, работы со сборками или кодом, завязанным на рефлексию.


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

                      1) Повторить простые контракты можно. А вот со спецификой C# типа in/out, явной/неявной реализацией интерфейсов, перегрузкой операторов итд уже будет сложнее. С другими языками страшнее, особенно чем дальше они от managed языков с GC. Возможно придётся писать «фасады» под таргет платформу.
                      3) Повторите структуру файлов/папок старого проекта. На худой конец заставьте git думать что произошел ренейм из .cs в .hx.
                      4) Часть модулей придётся «переизобрести» в любом случае. Но вы уже это делали, когда эмулировали .NET. Добавлять чисто нативный код (C++, Java итд) после транспиляции никто не запрещает.
                      5) Рефлексия есть. Чисто платформными фишками Haxe позволяет пользоваться. Плюс это всего лишь генерация кода из кода, сборка остается за вами, и что вы туда дольете дело ваше. Работу с эмбеддед ресурсами можно сделать через абстракцию.

                      В общем, только человек знакомый со всем вашим проектом может правильно оценить риски, и не за один присест, нуансов много.
                      Просто взять из коробки Haxe и перенести туда 100% кода не получится. Но перевести 95% кода на Haxe и 5% на каждую платформу ИМХО вполне реально.

                      П.с. С++ несут больше всего рисков по портированию.
                0
                Наибольший интерес вызывает следующий момент: Портер умеет работать со Span, Memory, MemoryMarshal? В системе net они занимают центральное положение при написании технических библиотек.
                  0
                  Нет. В наших библиотеках они не используются, а мы поддерживаем, в первую очередь, те аспекты .Net, которые нужны непосредственно для портирования кода продуктов.
                  0
                  А можно поинтересоваться, в общих терминах, что это за продукт, который востребован клиентами на разных языках и оправдывает такое неординарное техническое решение в своей разработке?
                    0
                    Набор библиотек для работы с различными форматами файлов (офисные документы, графика и т. п.). Библиотек, правильно читающих все поля и нюансы форматов MS Office, не так много, а Automation Tools стоят дорого.
                      0
                      Понял, спасибо за ответ. Крутой проект, вы молодцы!
                    +2
                    Очень похоже по описанию на Aspose.
                    products.aspose.com/cells/family
                      0

                      Хотел высказать своё никому не нужное мнение.


                      Мне кажется, изначально нужно было сделать вашу либу "С++ first"а к остальным языкам наделать биндингов.
                      Это проверенный и многократно оправдавший себя подход.
                      Требование "уметь забиндиться на функцию из плюсовой либы" есть практически у любого языка программирования, это базовая вешь вообще.


                      Например, клинтская либа для Apache Kafka написана на С++ (librdkafka), а поверх этой плюсовой либы уже работает, например, C# обёртка (kafka-dotnet).
                      Таких примеров очень много.


                      Опять же, мне кажется при транспиляции вы будете всё время заперты в клетке — ни тебе yield, ни тебе span, вы так не будете успевать за развитием языка C# и будет всё сложнее нанимать C# програмистов с существенными ограничениями на использование фишек языка C#.


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

                        0
                        Спасибо за комментарий. Ещё можно Skia/SkiaSharp вспомнить из этой серии.

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

                          Согласен, если туда-сюда надо гонять много объектов, то затраты по памяти на маршаллинг могут оказаться неприемлемыми.


                          Зря я начал что-то советовать не разобравшись до конца в проблеме.


                          А вот про "сложные обёртки" не соглашусь, транспилятор так-то тоже ни разу не простая получается обёртка.

                            0
                            Это да, сам транслятор тоже сложен, здесь соглашусь.
                        –1
                        Дичь какая-то… Выглядит как убиение килочеловеколет на невнятную хотелку ради хотелки…
                          0

                          Кажется в вашем случае проще было взять mono и выкинуть из него лишнее. А дальше сделать обертку на C++.

                            0
                            Добрый день. Возможно, Вы в чём-то правы, наши «порты» для Python действительно работают по похожей схеме. С другой стороны, при наличии большого числа мелких объектов (а это как раз наш случай) маршаллинг вызовов съедает очень много ресурсов, чего, конечно же, хотелось бы избежать.

                            Одной из наших целей было получить возможно более простое решение для пользователя, который может просто подключить плюсовую библиотеку к плюсовому приложению и работать. Мост между CLR (не важно, mono или нативным) — это усложнение данной схемы, хотя, возможно, и оправданное.

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

                          Самое читаемое