Как стать автором
Обновить

Стек стеком погоняет, или преобразование байткода виртуальной машины Java в байткод машины Фантом ОС

Время на прочтение 5 мин
Количество просмотров 13K
ОС Фантом — экспериментальная операционная система, содержащая на прикладном уровне виртуальную байткод-машину в персистентной оперативной памяти.

Один из двух ключевых запланированных для ОС Фантом путей миграции существующего кода — преобразование байткода 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.
Теги:
Хабы:
+27
Комментарии 17
Комментарии Комментарии 17

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн