Задача
Вот бы, разрабатывая программу на одном языке, сразу получать исходники на других языках программирования… Я пишу на C# .NET, но в последнее время всё больше требуется интегрироваться с Java. Одно из решений — оформление web-сервисов для взаимодействия, но не то это, не то. Вроде и существуют конвертеры C# в Java, но эксперимент показал, что для реального проекта они (те, что удалось попробовать) не работают, хотя на «hello world» отрабатывают отлично. Переписать с нуля на Java весь проект нереально — он активно разрабатывается более 6 лет (Pullenti — обработка естественного языка), да и на C# он нужен. Пришлось мобилизоваться и в прошлом году написать этот конвертер, а в этом году и конвертер C# в Python.
Опыт создания конвертера C# в Java
Итак, на входе проекты (solution) Visual Studio, нужно получить классы на Java. Разумеется, не все типы проектов возможно поддержать, но тип «Class Library» и «Console Application» — вполне. Проекты не должны использовать никакие графические библиотеки типа System.Drawing, никакие Windows-возможности или DllImport, а для используемых библиотек должны существовать аналоги в Java. К счастью, для используемых мной возможностей такие аналоги нашлись (работа с XML, архивы GZip и др.). От некоторых несущественных деталей пришлось отказаться, немного подправив алгоритмы. На уровне языка C# и Java близки, но Java чуть беднее — вот что в нём отсутствует:
- property { get; set; } — приходится работать с функциями;
- struct — структуры отсутствуют, поэтому сначала я их моделировал просто классами Java, но затем переписал алгоритм, отказавшись от них полностью;
- delegate — отсутствуют, но моделируются интерфейсом с одной функцией;
- event — моделируется массивом ArrayList из элементов класса, реализующего интерфейс соответствующего delegate;
- out и ref аргументы у методов — моделируется классами с одним полем Value;
- internal — этот модификатор вообще не имеет смысла в Java, так как отсутствует понятие сборки, поэтому просто заменяется на public;
- new Class() { присваивания } — приходится создавать статическую функцию, в которую выносить эти присваивания;
- enum — если в C# возможны побитовые операции с его элементами, то в Java это запрещено, и конвертеру приходится enum преобразовывать в классы Java;
Но не это оказалось самое сложное — проблема с огромным количеством системных функций, для которых нужно было искать аналоги. Для некоторых аналогов не нашлось, и пришлось писать их на Java, создавая свою библиотечку, которая добавляется при генерации к результирующим исходникам Java. Например, в C# для потоковых операций ввода-вывода есть базовый класс Stream, а в Java два отдельных класса InputStream и OutputStream, и поэтому нужна некоторая «обёртка» над ними.
Если нужно было исключить из кода C# некоторый фрагмент или оформить его немного по-другому для Java, то использовались директивы препроцессора #if JAVA… #else… #endif.
Вот так, постепенно дополняя конвертер новыми функциями и подправляя исходный код на C#, удалось прийти к тому, что генерируемые на Java более чем 1500 юнит-тестов стали отрабатывать правильно, как и их прототипы на C#.
Сравнение скорости работы .NET и Java
Теперь самое интересное. Есть трудоёмкий алгоритм (в основном работа со строками), и абсолютно идентичные его реализации на C# и Java. Как «апологет» .NET, я всегда сомневался в эффективности Java. Но оказался неправ — C# всего на 15-20% быстрее Java (запускал из Eclipse под Windows), а не в несколько раз, как ожидалось. А какова ситуация с Python? Читайте дальше.
Опыт создания конвертера C# в Python
В Python оказалось конвертироваться даже проще, чем в Java. Основа была готова, осталось только подправить генератор на новый язык и найти (написать) аналоги используемых функций.
Что в Python отсутствует или неудобно:
- for(init, cond, incr) — в полноценном виде отсутствует, есть некоторые частные случаи с range, но в основном приходится моделировать через while;
- switch — отсутствует, моделируется if-then-else-ами;
- interface — отсутствует, но это просто класс с пустыми методами;
Очень неудобная система импорта других классов, приводящая в ряде случаев к циклической зависимости и невозможности импорта вообще. Приходилось отслеживать эти зависимости и выносить некоторый импорт на уровень методов, а не всего файла с классом. Несмотря на эти и другие сложности, удалось добиться правильного отрабатывания тех 1500 юнит-тестов.
Сравнение скорости работы .NET и Python
Многие жалуются на низкую скорость работы Python, но насколько она низка? И низка ли? Пришло время сравнить на том же алгоритме, что и для Java (точнее, на тех же юнит-тестах). Готовы? Итак, Python работает в 30 раз медленнее, чем .NET (запускал в Eclipse под PyDev).
Причина, думаю, в следующем. В Python отсутствуют простые типы как ValueType (насколько я понял), и даже обычные числа — это полноценные объекты (фактически как boxing в .NET). И элемент строки — это не символ, а тоже строка из одного символа! То есть string[i] — это не char, а как бы string.Substring(i, 1) в .NET.
Технология кросс-языковой разработки
На основе личного опыта скажу, что возможно разрабатывать на одном языке и платформе и автоматически переносить на другие языки и платформы. Свой базовый проект разрабатываю на .NET Framework 4.0, и в любой момент имею функционально эквивалентные:
- исходники на Java (конвертер);
- исходники на Python (конвертер);
- проекты на .NET Core (отдельные csproj-файлы, но исходники те же);
- проекты на .NET Framework 2.0 (конвертер, и такое нужно для одного заказчика);
Поделюсь одной полезнейшей возможностью Visual Studio, которой почему-то мало пользуются даже профессиональные разработчики C#, к моему глубокому удивлению. И которая отсутствует в средствах разработки, скажем, для Java. А именно — возможность изменять код и перемещать текущую позицию прямо в ходе выполнения, причём состояние всех переменных сохраняется и можно продолжить выполнение с учётом внесённых изменений. Это кардинальным образом облегчает отладку, особенно когда для воспроизведения нужной ситуации требуется некоторое время. Например, я включаю режим, при котором Visual Studio приостанавливает программу в месте возникновения Exception (вернее, я этот режим никогда не отключаю), запускаю длительную обработку, а через сколько-то минут или часов возникает эта ситуация, чаще всего NullReference. Я исправляю код, и вместо того, чтобы перезапускать программу, перемещаю точку выполнения на нужный оператор назад и запускаю выполнение дальше. Да и вообще частенько я прямо в ходе выполнения реализую сложные алгоритмы в нужных местах. Помнится, когда делал систему биржевой торговли, то это на порядок облегчало жизнь в условиях трудоёмкой процедуры инициализации и выхода на нужную алгоритмическую ветвь. В Visual Studio эта возможность достигается за счёт тесной интеграции IDE и компилятора — они могут себе такое позволить. Но вернёмся к технологии.
Ограничения очевидны — только библиотеки и консольные приложения. Почти нереальны конечные приложения с пользовательским GUI или Web-приложения. Да и при разработке библиотек приходится придерживаться ограничений, накладываемых требованием универсальности. От чего-то придётся отказаться, что-то переписать, но результат стоит затраченных усилий!
Комментарии (60)
linuxover
05.05.2018 21:54> Python работает в 30 раз медленнее, чем .NET.
что-то очень странное.
вообще существенные различия всех трех систем могут быть в области GC, а в остальном одинаковый алгоритм должен работать примерно с одинаковыми бенчами.
а вообще интересно на код конвертера бы посмотреть :)SirEdvin
05.05.2018 22:24Ничего удивительного, если судить по доменной зоне и рассказам о том, что там были структуры и всякая другая магия, можно предположить, что там много сложной математики и алгоритмики именно с числами и прочим.
А тут нужны cython + numpy и сверху еще numba было бы неплохо. А чистый python сильно проигрывает :(
Ну или pypy, как предлагают ниже, но numba должна быть круче.
Antervis
06.05.2018 13:35основные реализации python — не полноценные вм, как у java/c#, а обычные не оптимизирующие интерпретаторы. python хорош для использования библиотек (особенно написанных на c/c++), но не для их написания. И уж тем более не для арифметических задач.
Trixon
06.05.2018 15:02www.researchgate.net/figure/Speed-comparison-of-the-Neighbor-Joining-program-Speed-comparison-of-the_fig2_5600272
Вы даже не представляете, как ошибаетесь.
Python везде выдаёт самый худший результат по производительности.
KonstantinSmith Автор
06.05.2018 17:02Да что на него смотреть — код как код. А вот сгенерированные коды на Java и Python можно посмотреть на сайте (www.pullenti.ru/DownloadPage.aspx). А первая задача была — это чтобы сам генератор, который на .NET, перевёл сам себя в Java и также работал.
Northborn
05.05.2018 22:04Попробуйте использовать PyPy вместо CPython, он с JIT компиляцией. На данный момент поддерживает Python до 3.5 включительно. (Легко) можно получить прирост в скорости до x10. Код переписывать не надо.
KonstantinSmith Автор
06.05.2018 17:27Извиняюсь, забыл указать в статье, что Python запускал в Eclipse под PyDev (подправил статью). Может, в какой другой среде это будет побыстрее.
Antervis
05.05.2018 23:23а не логичнее ли наладить interoperability компонентов, написанных на разных языках?
Но оказался неправ — C# всего на 15-20% быстрее Java (запускал из Eclipse под Windows), а не в несколько раз, как ожидалось
вы всерьез сравниваете производительность оригинального C# кода и автогенерированного, использующего оболочки и эмулирующего C#-концепции Java-кода?mspain
06.05.2018 11:07-4Не проще просто писать на java, которая в отличие от кое-чего запускается на нормальной серверной ОС? Виндовс-сервер это оксюморон. В тепличных условиях пусть будет +15% (лениво проверять), а через полгода шындовш уже пучит и таращит, через год минус 50% производительности
perfectdaemon
06.05.2018 11:16+7Стыдно в 2018 поливать грязью .net-платформу и не знать про .net core
mspain
06.05.2018 11:38-4Стыдно должно быть сравнивать зрелые платформы SE и EE с этим. Когда от например RedHat появится решение и саппорт, тогда и поговорим о core
perfectdaemon
06.05.2018 12:26+4Как определяется зрелость платформ? Год, пять или десять лет?
На RHEL есть поддержка .net core: пруф.mspain
06.05.2018 12:57-4Если мерять годами, то язык brainfuck (создан в начале 90х) всегда будет более зрелым решением, чем net core. Шах и мат, микрософт. :) не вижу смысла продолжать тему сравнения зрелости net и java
vsapronov
06.05.2018 18:14+4perfectdaemon mspain
Попробую примирить. mspain — вы неправы в 2018 году, dotnet core очень годится для продакшена на RedHat. Сам пишу на нем и на Scala. Кроме того, в 2018 году Java — убогий язык и писать на нем изначально, плохая мысль.
perfectdaemon с товарищем надо полегче. Дело в том, что некомпетентный майкрософт много лет игнорировал линукс, заявлял что open source — это рак, при этом не имея вообще ничего на сервере. Хуже того, армия майкрософтовских фанбоев поддерживала всю эту галимотью. Тот факт, что теперь у мс и его сообщества есть хейтеры — это нормальная реакция. Теперь надо делать добро и адекватные шаги ровно столько же времени, сколько они подсиради опен сорсу вообще и линуксу в частности, т.е. Ещё лет 10. Репутация — такая штука…mspain
07.05.2018 10:21По вашему если что-то сделать опенсорсным, оно мгновенно и обязательно превращается в конфету? У МС есть что-то подобное java EE? Если нет, то им еще лет 10 надо сидеть молча и пилить, пилить.
mspain
07.05.2018 10:27У меня, кстати, к МС претензии ко всем продуктам от кривого гипервизора до отвратительного Word-а (жизнь заставляет иногда в нем верстать). Так что опенсорсность core ничего не меняет
mspain
06.05.2018 11:27Ещё очевидно, что программы не берут данные из воздуха и не складывают в воздух. А известно как в винде работают субд. Какая тормозная ntfs и тд и тп. В итоге скорость ПРОГРАММНОГО КОМПЛЕКСА под net обгоняет java только при очень непрямых руках и в get the facts
alexs0ff
06.05.2018 12:55+1винде работают субд
Причем тут винда и субд. Например, сейчас есть в продакшене система на .netFramework, которая ходит на Oracle, который в свою очередь установлен на линуксе.
Плюс майкрософт сделала порт своего SQL Server на линукс. поэтому ограничений в .net, почти нет (ну кроме нормальных кроcплатформенных ГУИ, но я бы тут взял или QT или Electron)mspain
06.05.2018 15:37Клиент на dotnet, бизнес-логика на могучем plsqlчике, третий уровень не завезли? Я тоже видел тонны подобного крапа от подрядчиков нашей конторы. К обсуждаемой теме performance оно каким боком? :)
KonstantinSmith Автор
06.05.2018 17:08вы всерьез сравниваете производительность оригинального C# кода и автогенерированного, использующего оболочки и эмулирующего C#-концепции Java-кода
Ну да, тем более, что обёрток там не так уж много, и для несущественных для производительности объектов. А так всё идентично — классы, методы, циклы, ветвления, массивы и пр. стандартные конструкции. Хотя, может, каких-либо «блох» и можно отловить. В принципе, генерируемый код Java выложен на pullenti.ru/DownloadPage.aspx, если Вы там обнаружите навскидку какие неэффективности — буду признателен!
RolexStrider
05.05.2018 23:46Еше во времена .NET 1.0 сами Microsoft такое пробовали (конвертер из Java в C#, ЕМНИП было в VS.NET 2003). Не взлетело, и уже в VS 2005 убрали. А наоборот — полагаю что даже сложнее будет.
SanSYS
06.05.2018 17:39Наверное имеется в виду J#
https://ru.m.wikipedia.org/wiki/Visual_J_Sharp
"Заявленной целью разработки Visual J# было облегчение перехода разработчиков с платформы Java на платформу .NET Framework. Однако эта цель достигнута не была" =)
a-tk
05.05.2018 23:52Редактирование кода на лету имеет ряд ограничений: нельзя добавлять/удалять классы/внутренности классов, нельзя править генераторы и прочие методы, которые разворачиваются в машины состояний, нельзя править методы, содержащие лямбды, и многое другое. Остаётся процентов 20-30 тривиальных случаев.
KonstantinSmith Автор
06.05.2018 16:58Зависит от проекта. У меня таких случаев как раз 80-90% (обработка естественного языка), и весьма нетривиальных.
a-tk
05.05.2018 23:53Код на производительность тестировался как? Сконвертированный сравнивался с исходником, или нормально переписывался с учётом особенностей целевого языка? В первом случае любой код будет медленнее исходного языка.
KonstantinSmith Автор
06.05.2018 18:48Нормально переписать нет никакой возможности — проекту около 7 лет, там сложные алгоритмы лингвистической обработки, морфологический анализ, выделение именованных сущностей (NER). Повторю, что использовались только «стандартные» языковые конструкции, эквивалентные в .NET и Java (от экзотики я избавился в ходе работы над конвертером). Не думаю, что можно как-то существенно оптимизировать код Java (см. результат генерации pullenti.ru/DownloadPage.aspx?file=PullentiJava.zip). Может, есть какие тонкие настройки оптимизации в самой среде Eclipse. Например, когда я начинал запускать в NetBeans, то там скорость почему-то была медленнее раза в 2. Скажем, в Visual Studio в режиме Debug скорость меньше на такие же 15-20%, чем в Release. Хорошо бы ещё проверить под Unix, но нет под рукой. Но в целом можно утверждать, что скорость примерно одинакова.
a-tk
06.05.2018 22:29То есть в сущности это не сравнение производительности, а просто проверка того, насколько просела производительность после автоматической миграции.
kemsky
06.05.2018 00:33Так как есть JPython, то может быть даже достаточно перегонять только в питон, для джавы написать отдельно интерфейс.
TimReset
06.05.2018 00:53В Java тоже есть редактирование кода на лету, правда работает оно не из коробки — нужно запускать на пропатченном jvm и с java agent'ом. (Для dcevm актуально). Это всё не сложно сделать. Хотя перенос, как вы выразились, «текущей позиции», всё же не поддерживается, насколько я знаю.
dcevm.github.io бесплатная
zeroturnaround.com/software/jrebel платная, очень платная
Я пользовался обеими и для большинства случаев первого хватит. Насколько я помню, основное отличие в поддерживаемых framework'ах — т.к. при изменении кода иногда нужно учитывать работающие в приложении framework'и и дёрнуть их методы (например, добавили поле которое испольуется в DI и его нужно инициализировать, вот dcevm и jrebel посмотрять, что у вас за DI framework используется и применят его)
Но по опыту — если такая фигня начинает быть важна, значит что то не то в архитектуре приложения — поидее всю логику работы нужно уметь востанавливать по логам и быстро воспроизводить в тестах. По крайне мере мне эти штуки пригодились только на таких проектах — тестов нет, запускается очень долго, и поэтому приходиться в run time код писать :-)
P.S. давно статью по dcevm писал, может пригодиться, habr.com/post/236075
impwx
06.05.2018 12:53-1В Python отсутствуют простые типы как ValueType (насколько я понял), и даже обычные числа — это полноценные объекты (фактически как boxing в .NET).
Это ерунда. В дотнете значимые и примитивные типы — это не одно и то же. Например,string
— примитивный, но ссылочный, аDecimal
— значимый, но не примитивный.
Каким образом вы определяли, что обычные числа используются как объекты? Вы смотрели ассемблерный код, генерируемый JIT? Я не знаток питона, но практически уверен, что проблема производительности не в нем, а в том, что ваш генератор выдает неэффективный код.alexs0ff
06.05.2018 13:15+1Например, string — примитивный
Да, ладно!
var isPrimitive = "Hello World!".GetType().IsPrimitive; Console.WriteLine(isPrimitive); >>out -> false
impwx
06.05.2018 18:16С точки зрения CLR тип
string
вполне себе примитивный: для него есть отдельный опкод, его можно использовать в аргументах атрибутов и в константах. А свойство просто проверяет текущий тип на вхождение в некий список. Если пытаться сформулировать правило, по которому здесь определяется примитивность типа, то оно получается длинным и неуклюжим. Поэтому я подозреваю, что на самом деле причина историческая: так реализовали в первой версии, а потом не стали менять для сохранения обратной совместимости.alexs0ff
06.05.2018 21:40Если пытаться сформулировать правило, по которому здесь определяется примитивность типа, то оно получается длинным и неуклюжим
Зачем формулировать правило, которого нет в спецификации языка C#?
Даже Джон Скит, не называет string примитивным типом и полностью доверяет type.Isprimitive
I wouldn't personally call dynamic, decimal, object or string primitive types. I'd use Type.IsPrimitive for the canonical source there.
Источник
stackoverflow.com/a/47589777impwx
06.05.2018 22:51Во-первых, в спецификации языка C# определения примитивности и не может быть, потому что это более низкоуровневая вещь, относящаяся ко всему CLR.
Во-вторых, именно отсутствие нормального определения побуждает к попытке сформулировать свое. В ECMA-335 я такого не нашел, всё так или иначе упирается в это самое свойствоIsPrimitive
, а рекурсивное определение, даже подтвержденное личным мнением Джона Скита, на мой взгляд бесполезно.
Для себя я определяю «примитивный» тип как нечто базовое, что CLR особым образом использует и на что полагается. Под такое определение строка подходит — в формате метаданных CLI есть даже отдельная секция для хранения строк.
Какое определение больше нравится — решать вам. Но если вы знаете ситуацию, в котором текущая реализацияIsPrimitive
имела бы осмысленную практическую пользу — пожалуйста, поделитесь.alexs0ff
07.05.2018 08:21в спецификации языка C# определения примитивности и не может быть
Ага, а вот в спецификации VB.NET есть секция 7.3
The primitive types are identified through keywords, which are aliases for predefined types in the System namespace.
Там же, кстати, к примитивным типам относят и Decimal, Date
Для себя я определяю «примитивный» тип как нечто базовое,
Так и писали бы в первоначальном сообщении ИМХО — вопросов бы не было.
Restorer
06.05.2018 13:07+2Вот бы, разрабатывая программу на одном языке, сразу получать исходники на других языках программирования
Haxe (https://haxe.org/), не?KonstantinSmith Автор
06.05.2018 17:39Да, наверное, неточно выразился. Правильнее было бы так:
Вот бы, разрабатывая программу наодноммоём любимом языке C#, сразу получать исходники надругихраспространённых языках программирования Java и Python
AdAbsurdum
06.05.2018 17:10А что будет с NuGet-пакетами?
KonstantinSmith Автор
06.05.2018 17:14Если нет аналога в Java — то никак не конвертируется. Или использовать исходники пакета, если есть, для конвертации и их в Java. Увы, есть ограничения… Например, связанные с использованием GUI или некоторых Windows-возможностей (какой-нибудь реестр). Но в рамках, скажем, функционала .NET Core этих ограничений будет меньше.
devpreview
06.05.2018 17:11Для таких задач можно попробовать GraalVM.
C# нет, но есть Truffle и возможность имплементить свой язык.
Dreamer_other
06.05.2018 17:15А как именно вы делали конвертацию? Этого в статье не хватает.
KonstantinSmith Автор
06.05.2018 17:22Сначала попробовал конвертеры C# в Java из Интернета (парочку), но они выдавали некомпилируемый результат с кучей ошибок. Может, есть какие платные получше, но денег разбираться нет. Писал сам, на это ушло около 3-х месяцев, зато теперь конвертирует то, что нужно, и как нужно сразу рабочий код. Задача была добиться того, чтобы результат вообще не нужно было бы вручную как-либо корректировать. Поскольку такие генерации идут периодически при доработках исходного проекта на C#. А в этом году за месяц доделал конвертер для получения Python. Но это так — экзотика, так как реальных проектов у меня для Python нет.
Konstontin
06.05.2018 17:40+2Может быть язык Haxe — это то, что вам нужно
KonstantinSmith Автор
06.05.2018 17:41Да, наверное, неточно выразился в начале статьи. Правильнее было бы так:
Вот бы, разрабатывая программу наодноммоём любимом языке C#, сразу получать исходники надругихраспространённых языках программирования Java и Python
SanSYS
06.05.2018 17:46Месье знает толк в извращениях...
Если верно понял, Пайтон был по фану
А не хотите во что-нибудь поэкзотичнее конвертнуть?
Например Go или Rust))KonstantinSmith Автор
06.05.2018 18:26«Штирлиц был извращенец, поэтому во Вращенцах ему был установлен памятник.» Могу и в Go, если там будет не памятник, а хотя бы небольшой бюстик…
erzi
06.05.2018 18:33«new Class() { присваивания }»
в Java — new Class() {{ присваивания }}KonstantinSmith Автор
06.05.2018 18:36Надо же, не знал! Спасибо, оказывается, так можно — подправлю конвертер.
turbanoff
06.05.2018 18:50Это не то же самое. Этот вариант создаёт анонимный класс.
Не стоит править — просядет производительность.
ExplosiveZ
06.05.2018 18:57Так делать нельзя. Это уже будет не `Class`, а некий другой, анонимный `Class`.
// Я буду обновлять комментарии перед отправкой своего.
Dj_Art
06.05.2018 18:36+1Похвально. Но, есть такая штука как IKVM.NET. Разработка относительно недавно прекратилась, однако, позволяет запускать почти любой JVM байткод на .NET, и при том умеет не только интерпретировать, но и компилировать. Так, кстати говоря, можно из Java в C# перегонять код, при помощи декомпилятора. И даже Interop с Java есть, можно создать jar-обертку для.NET кода и использовать ее при разработке на Java, с одним ограничением — код должен исполняться в IKVM.
AnarchyMob
06.05.2018 18:36Лучше конечно конвертировать на уровне байт кода, а не исходного кода. Я встречал на хабре несколько таких проектов, например CIL2Java, был еще dot42 и множество других.
ruslanfedoseenko
07.05.2018 10:52По поводу Java<->.NET есть такой «диназавр» IKVM.NET. Умеет бинарники в jar и наоборот. Но я так понял вам важно иметь именно исходники.
Terranz
Хорошая статья, спасибо.
А по поводу изменения кода в памяти и перемещения указателя: можно просто поставить брейкпоинт и в нём уже запустить Evaluation окно с отдельным кодом внутри, который подцепит все переменные и так далее.