Один из двух ключевых запланированных для ОС Фантом путей миграции существующего кода — преобразование байткода Java в байткод Фантом.
Надо сказать, что эти виртуальные машины изрядно, хотя и совершенно случайно, похожи. Виртуальная машина Фантома была спроектирована тогда, когда про Явскую я ещё ничего не знал, но, наверное, сходство целей привело к сходству принятых решений.
Обе машины — стековые. Обе оперируют двумя отдельными стеками — стеком для работы с объектами (на стеке лежат только ссылки), и бинарным стеком — для вычислений. Машина Фантома имеет также отдельные стеки для фреймов функций и ловушек исключений. Как эта часть устроена в JVM, я не знаю до сих пор, но полагаю, что вряд ли кардинально отличным образом.
Естественно, что и набор операций стековых машин местами схож как две капли.
Но, безусловно, есть и весьма существенные отличия.
Во-первых, виртуальная машина Фантома предназначена для работы прикладного кода в менее дружественной среде. Ява исходит из того, что каждая программа живёт в отдельном адресном пространстве, и всё, что вокруг — “наш” код. Фантом допускает прямые вызовы между приложениями разных пользователей и разных программ, что требует более жёсткого отношения к некоторым аспектам виртуальной машины, включая тот же вызов, да и интерфейс объекта вообще. Например, мы не можем полагаться на то, что вызванный метод ведёт себя “прилично” — нельзя давать ему доступ в свой стек, нельзя полагаться на наличие или отсутствие возвращаемого значения. Нельзя гарантировать различие между методом, функцией и статической функцией. То есть, мы можем предполагать, что именно мы вызываем, но что нам «подсунули» с той стороны — неизвестно.
В силу всего сказанного, вызов в Фантоме унифицирован абсолютно — это всегда вызов метода (есть this и есть класс), и всегда возвращается значение, которое для void метода равно null и явно уничтожается вызывающим кодом. Это гарантирует, что какая бы ошибка вызова не случилась, что бы не подвернулось в качестве предмета вызова, протокол вызова и возврата будет соблюдён.
Есть отличие и в работе с целыми. Ява выделяет их в отдельную категорию типов, отличную от объектных, “классовых” типов — java.lang.Integer и int — разные вещи в Яве. Компайлер иногда удачно скрывает этот факт, но внутри они различаются. Фантом и здесь идёт в сторону максимализма. Целое — честный объект. Его можно вытащить на целочисленный стек и там посчитать в “необъектной”, бинарной форме, но он вернётся в форму объекта будучи присвоен переменной или передан в параметре. Это, кстати, тоже вытекает из требования униформности протокола вызова метода — методы, возвращающие целое и объект по протоколу тождественны. (То же самое, очевидно, относится и к другим «интегральным» типам — long, float, double.)
Есть и другие отличия, например, протокол подключения того, что в Яве называется native методы. В Фантоме это «системные вызовы», и, опять же, на уровне вызова метода они ничем не отличимы от обычного “честного” метода. (Код такого метода содержит специальную инструкцию для “ухода” в ядро ОС, но “снаружи” метода это не видно. Это, в частности, позволяет наследовать и оверрайдить такие методы традиционным путём, через замену VMT.)
Представляется (по крайней мере, мне представлялось), что преобразование байткода одной стековой машины в байткод другой стековой машины — элементарная задача. В конце концов, там и там стеки, и 90% операций — просто идентичны. Ну нет никакой разницы между Фантомовским и Явским байткодом целочисленного сложения: поднять два целых со стека, сложить, положить на стек результат.
Первый подход к трансляции опирался именно на модель последовательного преобразования байткода Ява в фантомовский. Быстро выяснилось, что сделать это линейно нельзя. Совсем. Приходится “отрабатывать” при разборе Явского кода “работу” стека, и синтезировать промежуточное представление. Часть такого транслятора была написана и признана негодной — трудоёмкость превзошла все мыслимые границы. К примеру, локально, в точке вызова, совершенно невозможно выяснить, объектный это вызов (первый параметр — this), или нет. Яве всё равно, а нам важно. Выяснить это можно, но нужно приложить немало усилий. Это даже при условии, что писать приходилось только анализатор — бекенд компилятора, генерирующий вполне надёжный байткод Фантома, к тому времени стабильно работал (в силу того что был готов и стабильно использовался компилятор “собственного” языка).
В этом месте работа бы застопорилась, не попадись мне под руки фреймворк по имени Soot. Изначально предназначенный для статического анализа и инструментовки Ява байткода, он идеально подошёл для описанной задачи. Soot парсит класс-файл JVM и генерирует чрезвычайно вменяемое внутреннее представление — дерево операций с компактным (полтора десятка типов узлов) базисом, плюс информация о типах и другой метаинформации.
С этой точки конверсия производится катастрофически проще — фактически, нужно преобразовать дерево в дерево. На сдачу, кстати, получаем и поддержку Dalvik (Andrid VM bytecode).
Нельзя сказать, что теперь всё безоблачно. Хотя первые примитивные Ява-классы уже прошли компиляцию и начата работа по юнит-тестам компилятора. Есть ещё масса проблем.
Например: в фантоме наследование от классов с “внутренней” реализацией предполагалось запретить. В то же время, Ява “привыкла” видеть у строки тип java.lang.String, а не internal.String. Но это ещё ладно! Сложнее со сравнением объектов. В Яве == для целых и строк работает различно, сравнивает значения и ссылки, соответственно. Более консистентный Фантом чётко различает сравнение значений и ссылок, а значит простое на вид преобразование операторов == и != вызывает проблему — надо или разбираться с типом, или вводить в базис “явский” байткод, который ведёт себя как описано выше. Что “неаккуратненько”, зато чертовски просто.
Вообще изначально предполагалось, что систему типов Явы надо инкапсулировать, представив их в дереве типов виртуальной машины Фантом внутри ветки java. Фактически, сейчас я от этого отказался. Представляется, что это вызовет больше проблем, чем решит.
Смешная проблема была с доступом к публичным полям: в Фантоме их… нет. Вообще. Только методы. Обход проблемы потребовал автоматической генерации и использования геттеров и сеттеров. Что, наверное, тоже проблематично — сейчас им даются типовые “Явские” имена getVariable/setVariable, что может вызвать конфликт. Нужно, видимо, сделать имена “генерируемых” методов специальными и недоступными из обычного пространства имён методов, но делать так тоже несколько жалко — автогенерация публичных геттеров-сеттеров имеет прикладную ценность.
Следующей проблемой будут примитивы синхронизации. В Яве точкой синхронизации может быть любой объект. Держать для этого в каждом объекте Фантома специальные поля не хочется, но уметь как-то “достраивать” объекты надо. Причём не только синхронизация но и, например, механизм слабых ссылок требует “навешивать” на объект дополнительные сущности. В данный момент это предполагается делать через поле заголовка объекта, на которое можно, при необходимости, вешать объект или множество объектов для обслуживания специальных случаев. У большинства “линейных” объектов это поле будет пустовать, и заполняться только если с ним делают что-то особенное.
Уф. Наверное, для начала на этом поставим точку с запятой.
Ну и да, это всё — open source. Если интересно принять участие в работе над ОС, или в вашем проекте нужна готовая виртуальная машина, проект легко находится на гитхабе по ключу phantomuserland.
Комментарии (15)
Maccimo
01.03.2016 13:51+2>> Надо сказать, что эти виртуальные машины изрядно, хотя и совершенно случайно, похожи.
…
>> Как эта часть устроена в JVM, я не знаю до сих пор
Гм…
>> К примеру, локально, в точке вызова, совершенно невозможно выяснить, объектный это вызов (первый параметр — this), или нет.
А в чём проблема?
В байткоде это всегда invokestatic, а метод и так знает, что он статический.
>> Чёткой отсылки к номеру версии байткода в документации soot я не нашёл, но последний билд проекта — от 2012 года
Currently, Soot can process code from the following sources:
— Java (bytecode and source code up to Java 7), including other languages that compile to Java bytecode, e.g. Scala
…
© http://sable.github.io/soot/#what-input-formats-does-soot-provide
Последний коммит — две недели назад: https://github.com/Sable/soot/graphs/commit-activitydzavalishin
01.03.2016 14:20О. Спасибо. А я уж было слегка грустил и задумывался, не переехать ли на ASM.
Про invoke — откровенно говоря, не помню деталей. Давно делал ту версию. Вполне возможно, что мне тупо не пришло в голову опереться на тип invoke.
ababo
02.03.2016 07:00+1Рад, что вы решили написать статью на Хабре. Приятно иметь возможность пообщаться с таким интересным человеком, так сказать, воочию.
Теперь по делу. Не считаете ли вы, что персистентную ОС с единым адресным пространством сегодня стоило бы разрабатывать без ВМ, а на основе ЯП с гарантиями защиты памяти типа Rust?dzavalishin
02.03.2016 14:48Спасибо на добром слове.
Ограничивать ОС одним ЯП, да ещё и не самым популярным — затея спорная. Да и гарантии защиты памяти даёт не сам язык, а среда исполнения+генератор кода. В Фантоме виртуальная машина самодостаточна с точки зрения обеспечения надёжности, но это же всё равно промежуточный этап — дальше будет генерация бинарного кода из байткода (JIT), и ВМ как таковая исчезнет, останется только в виде промежуточного представления программы.ababo
02.03.2016 15:00+1Да, ограничиваться одним языком не стоит, тут вы правы. Просто смущает мысль о необходимости раздутой среды исполнения, похожей на JVM. Дело в том, что разработчики Rust продемонстрировали, что можно почти полностью отказаться от среды исполнения (как в C), но обеспечить безопасность памяти и параллелизма на основе статического анализа (при наличии соотв. системы типов, разумеется). Потому тут напрашивается какое-то подмножество LLVM-IR, над которым тоже можно было бы проводить подобные проверки. Т.е., по-сути, это та же ВМ, но с минимальной средой исполнения (только проверки границ массива, стека, ещё что-то, без чего нельзя обойтись).
dzavalishin
02.03.2016 21:00Оно и в Яве давно так, интерпретатор почти уже не используется. Можно её вообще скомпилировать в бинарник (gcj, part of gcc compilers collection). Как таковая среда — это библиотека. Если она не нужна, можно получить сишный размер программы.
igor_suhorukov
09.03.2016 12:31К сожалению, GCJ мертв. Из AOT компиляции для java байткода знаю только Excelsior JET
dzavalishin
09.03.2016 19:05Жаль. Хороший был проект. А почему, известно? Люди ушли, или проблемы?
igor_suhorukov
10.03.2016 11:19По моему мнению это связано с появлением проекта OpenJDK, куда устремились люди и средства корпораций. Посмотрите на даты, очень похоже на это.
guai
08.03.2016 23:17В дотнете, вроде как, на уровне вм есть особые фичи для поддержки овеществленных дженериков, а в яве, вроде как, нет такого… хотя чуваки из редхата при создании цейлона обошлись и запихали их в рантайм через аннотации…
А в фантоме с этим как?dzavalishin
09.03.2016 11:09А какой именно поддержки?
guai
09.03.2016 11:40Ну я конечно не спец в этом деле и могу сморозить глупость. Как я понял, в дотнете дженерик класс остаётся неким шаблоном до самого рантайма и в рантайме тоже, и по мере надобности из него делаются нормальные классы со своими статическими полями, например, этот конкретный класс можно по имени поискать, и т.п.
Ну вот такая фича, наверное, и должна на уровне вм поддерживаться, типа создавать конкретные классы из шаблончиков по мере надобности. Ну в случае если интересует не только запуск явы на фантоме.dzavalishin
09.03.2016 11:53На уровне виртуальной машины это, вроде бы, большого смысла не имеет, там только проверка типов специфична. Вот раскрыть шаблон при генерации JIT — это интересно. Но нет, сейчас такого нет, потому что, для начала, пока нет JIT.
lany
Какую версию Java-байткода вы поддерживаете? Java-6?
Как я понимаю, производительность вас сильно не волнует, если вы совсем от примитивных типов отказались?
Не очень понял, как транслируется вызов статического метода (invokestatic). Какой объект используется в качестве this?
Интерфейсы у вас есть в языке? Такие же, как в Java? Как у них устроена таблица виртуальных методов?
dzavalishin
4*спасибо за вопрос. :)
Чёткой отсылки к номеру версии байткода в документации soot я не нашёл, но последний билд проекта — от 2012 года, что наводит на мысль, что явно не 8. Скорее 6, да.
Производительности я считаю правильным достигать через JIT, сохраняя регулярную структуру формальной виртуальной машины. Внутри кода JIT ничто не мешает пускаться во все тяжкие, уходя от навязанных виртуальной машиной представлений. Собственно, даже в интерпретаторе любая формула из группы интегральных переменных будет отработана в бинарном виде, преобразование инта в объектную форму происходит только при вызовах и сохранениях переменных.
Вызов статики пока не реализован, но проблем тут нет вообще. Есть два простых варианта. первый — генерировать для статики специальный класс, второй — тупо делать статические методы так же как обычные, держать для них синглтон инстанс this, и при кодогенерации запрещать доступ к полям this. Спасибо ещё раз за вопрос, заодно я, кажется, определился с реализацией. :)
Интерфейсы запроектированы. Реализация реально не дошла. Есть два варианта реализации. Первый. Предпочтительный. Реально все пойнтера виртуальной машины таскают с собой пойнтер на интерфейс, по которому и происходит обращение к vmt. Это позволит реализовать cast с заменой в пойнтере интерфейса на любой нужный. Дальше вызов по vmt[ordinal интерфейса]. Второй вариант совсем тупой — invokedynamic. Но он пока без полиморфизма. Что, впрочем, тоже решается легко — передавать сигнатуру вместо имени метода.