В этой статье я расскажу более подробно об истории проекта JPHP и каким образом он был разработан с технической стороны. Текст будет интересен как простым разработчикам PHP, так и любителям компиляторов. Я постарался описать все на простом языке.
А перед началом небольшие новости.
В скором времени у проекта появится отдельный сайт и попробовать JPHP будет очень просто, будут выкладываться различные скомпилированные версии движка. Итак, перейдем к теме…
Проект начался спонтанно. До этого я искал похожие проекты, т.е. реализацию php для JVM. Есть такой проект как Quercus из Resin, это транслятор в код Java, написанный на Java. Такое положение вещей меня не устраивало, тем более авторы проекта заявляли, что их реализация работает с такой же скоростью как и Zend PHP + APC. До определенного времени существовали и другие реализации PHP для JVM (p8 и projectzero например), но они все умерли и закрылись.
Главной мотивацией начать проект была производительность и JIT. Я уже довольно давно общаюсь с Java, высокая производительность JVM, печеньки, огромное сообщество и качественные библиотеки — это то, что меня в ней привлекает. И немного пораскинув мыслями у меня зародилась идея — взять лучшее из Java и реализовать на ней движок PHP. Собрав раскинутые мысли, я приступил к написанию тестовой версии, порешив на том, что если jphp будет хотя бы раза в 2 быстрее оригинального Zend PHP я продолжу разработку.
Первым делом я прошелся по репозиториям всех известных JVM языков (groovy, jruby, scala) и узнал — какой набор библиотек они используют для генерации байткода JVM. Как оказалась, существует известная сторонняя библиотека — ASM. Она довольно активно развивается, имеет достаточную документацию в PDF, и вроде как поддерживает даже Dalvik (Android) байткод (об этом ниже).
Виртуальная машина Java (JVM) довольно мощный инструмент. О том как устроен байткод JVM можно узнать из документации к библиотеке ASM. Кратко описать все возможности VM можно в следующих пунктах:
1. Виртуальная машина стековая
2. Есть возможность хранить локальные переменные по индексам (что-то вроде регистров)
3. GC (сборщик мусора) реализован на уровне VM
4. Объекты и классы реализованы на уровне VM
5. Большое количество стандартных операций — POP, PUSH, DUP, INVOKE, JMP и т.п.
6. Для Try Catch есть инструкции байткода, для finally — частично
7. Для VM есть несколько типов значений: int32, int64, float, double, объекты, массивы скаляров, массивы объектов, для bool, short, byte, char используется int32.
Таким образом я понял, что мне не придется реализовывать самому GC и систему классов объектов с нуля.
Прежде чем начать разработку, я хорошенько осмыслил главные преимущества PHP, не только как языка, но и как платформы. Самыми очевидными для меня оказались следующие вещи:
Эти преимущества являются также и недостатками. При каждом запросе заново грузятся все классы и это не очень-то хорошо. Проблему частично решают кэшеры байткода, но не до конца. Я подумал, что смогу решить эту проблему, сохранив те преимущества, что я перечислил выше. Что я получил в итоге:
На уровне Java VM нет никакой динамической типизации, а в PHP она нужна. Многим это покажется большой проблемой, но это не так.
Для хранения значений я реализовал абстрактный класс Memory. Значения JPHP хранит как объекты Memory. Далее я реализовал классы StringMemory, NullMemory, DoubleMemory, LongMemory, TrueMemory, FalseMemory, ArrayMemory и ObjectMemory. Как вы наверно поняли по названиям классов, каждый из них отвечает за определенный тип — числа, строки и т.п. Все они были унаследованы от Memory.
Memory состоит из абстрактных методов, которые необходимы для реализации операций над значением, например для операторов плюс и минус есть методы
Объект Memory по потреблению памяти не превосходит объекты zval из Zend PHP, и частично поэтому JPHP и Zend PHP примерно эквивалентны по расходу памяти. Для многих значений (например false, true, null, небольшие числа) объекты кэшируются в памяти и не дублируются. Есть ли смысл при каждом
Как я описал выше, все значения это объекты класса Memory. В самом начале я реализовал autoboxing и для простых константных значений, т.е. например если:
Как вы видите — «2» превращалось в объект. Это было удобно с точки зрения программирования, но с точки зрения производительности сущий кошмар. Такая реализация у меня работала не быстрее чем Zend PHP.
Я решил идти до конца. Чтобы избежать инициирования такого большого количества объектов для простых значений (а представьте этот код в цикле?), я решил реализовать метод plus() и другие аналогичные для базовых типов Java — double, long, boolean и т.п., а не только для Memory. Признаюсь, это была рутина и попахивало говнокодом. Я переделал компилятор и он стал понимать типы и что с ними делать. Компилятор научился подставлять разные типы и разные методы для операций в зависимости от типа элементов в стеке. Да стек просчитывается во время компиляции. Хотя можно было бы сохранить эти константные значения и в таблице констант, но все равно был бы overhead, хотя и не такой большой.
Как оказалось все это я затеял не зря и производительность на синтетических тестах начала обгонять Zend PHP уже в более 2-3-4-10 раз, например на циклах, переменных и т.п.
PHP поистине магический язык, я точно не уверен, но в каком еще языке во время выполнения можно обращаться к переменной по названию из строки? Простой пример:
Чтобы реализовать такую магию, необходимо использовать именную таблицу переменных — хеш таблицу. JVM предоставляет возможность хранения переменных по индексам, естественно, превращать имена переменных в индексы во время компиляции это более логичный шаг, он обеспечивает очень быстрый доступ к переменным, более быстрый чем в хеш таблице. Сравните, что будет быстрее — поиск по хеш таблице или обращение по индексу к массиву?
Я сначала забыл про нее и реализовал переменные на индексах. Производительность обращения к переменным была на высоте. Когда я вспомнил про такую магию, пришлось переделать на хеш таблицу и… все было плохо. Производительность работы с переменными упала буквально в 2-3 раза.
Не долго думая, я решил реализовать в компиляторе 2 режима компиляции переменных — в индексы и с хеш-таблицей. Анализатор помогает выявить код, в котором необходимо обращение к переменным по строке. Это происходит по следующим признакам:
Почему в коде, содержащем require и include нужно сохранять имена переменных в хеш-таблице? Да всё просто, PHP должен передавать переменные внутрь подключаемых скриптов, то же самое и с eval. А остальные функции работают с именами переменных.
Очень часто вам и не нужна такая магия переменных, а значит ваш код будет работать быстрее в JPHP. Существует также глобальная область видимости переменных. В этой области автоматически используется механизм хранения переменных в хеш-таблице, т.к. предполагается, что к глобальным переменных можно обращаться через массив
Супер — глобальные переменные
В PHP массивы копируются, а не передаются по ссылке. Однако, копирование массива происходит не в момент присваивания =, т.к. это бы создавало большой overhead. Внутри движка JPHP, да и в Zend PHP, массивы копируются по ссылке, но в момент изменения массива он копируется (в том случае если количество ссылок на массив > 1).
JPHP не использует подсчет ссылок, он использует стандартный GC от Java, который умеет удалять и циклические ссылки. Это создает проблемы при реализации таких массивов. Поэтому я реализовал специальный механизм превращения любого значения Memory в immutable значение. Покажу сначала псевдо-код:
В JPHP есть еще один вид Memory объектов — ReferenceMemory, эти объекты просто хранят ссылку на другой объект Memory. Однако не всегда переменные хранятся как Reference объекты, в некоторых случаях локальные переменные могут обходится без таких ссылок и использовать напрямую байткод для записи нового значения в ячейку, это работает естественно быстрее чем обычный метод
Reference объекты возвращают из метода
Классы уже существуют на уровне JVM. Если говорить в общем, то Java, Scala, Groovy, JRuby генерируют одинаковые классы с разной сигнатурой в рамках JVM. JPHP при компиляции php классов использует JVM классы с особенной сигнатурой:
В каждый метод и функцию передается объект Environment, этот объект позволяет узнать очень многое об окружении, в котором выполняется метод. Классы и функции компилируются одинаково для всех окружений. Memory[] это массив переданных аргументов в метод. Метод должен всегда возвращать что-то, а не void, потому что в PHP даже если функция ничего не возвращает, она возвращает null, вот такая тавтология.
Функции php также компилируются в классы, т.к. в JVM нет такого понятия как функции. На выходе мы получаем класс с одним статическим методом, который по сути и является нашей функцией. Почему функции не компилируются в методы одного класса? Это хороший вопрос, и скорее всего необходимо это переделать, чтобы не плодить лишние классы, но пока так удобнее.
JVM умеет легко загружать классы из памяти во время выполнения, достаточно написать свой Java загрузчик классов. Поэтому JPHP компилирует во время выполнения и может во время выполнения загружать классы и не все сразу.
В движке была также реализована возможность для написания классов на самой Java. Однако, не все так просто. Все методы таких классов должны иметь нужную сигнатуру и помечаться некоторыми вспомогательными аннотациями (например для type hinting), один из примеров такого класса:
По мере роста числа новых нативных классов для JPHP я заметил, что время затрачиваемое на регистрацию классов увеличивалось. Чем больше было расширений и классов, тем больше становилась задержка перед запуском движка. Это меня беспокоило. И пришла идея — как это исправить.
PHP как язык обладает механизмом ленивой загрузки классов, все про это знают. Я просто воспользовался механизмом ленивой загрузки классов для регистрации нативных классов. Когда регистрируется нативный java класс, он просто прописывается в таблицу имен, а де-факто регистрация происходит в момент первого использования этого класса. Реализовав этот механизм, я получил хорошие результаты, время инициализации движка уменьшилось в 2-3 раза, а время прохождения тестов уменьшилось с 24 секунд, до 13 секунд. Благодаря этому количество нативных классов практически не будет влиять на скорость инициализации движка.
Скорость старта движка особенно важна в GUI приложениях.
1. Именование JVM классов. JVM следит за соблюдением стандарта именования классов. Если вы пишите файловый путь к классу в байткоде, то JVM проверяет соответствие именования этого класса как в языке Java. Это чем-то напоминает стандарт PSR-0. Однако, если класс размещать в global пакете jvm, такой проверки не происходит. PHP может хранить в одном файле сколько угодно классов и функций, и они могут иметь любые причудливые названия. Поэтому пришлось отвязать привязку имен php классов к именам JVM классов внутри байткода. Но это не единственная причина такого выбора…
2. Уникальные имена классов. JPHP должен уметь сохранять полученный байткод в файл и загружать его в любое окружение, поэтому все классы на уровне jvm должны иметь уникальные имена, чтобы не было конфликтов. Во время загрузки jvm байткода изменить имя класса нельзя, по крайней мере я еще не пытался. Пока как временное решение я генерирую случайное имя класса для JVM на основе UUID + некоторые вещи. Думаю это не очень элегантное решение, в будущем надеюсь найдется получше. Использовать имя файла, в котором находится класс нельзя, т.к. код может находится и вовсе не в классе, а файл байткода может переноситься с компьютера на компьютер и его имя может меняться.
3. Ограничения рефлексии. Через рефлексию Java невозможно вызвать метод в контексте класса родителя, т.е. что-то вроде
Я решил эту проблему не совсем элегантно, мне пришлось отказаться от стандартного механизма переопределения методов jvm классов, и поэтому имена наследуемых методов на уровне jvm классов разные — по алгоритму
Трейты это механизм множественного наследования, который ввели в PHP начиная с 5.4 версии. По сути он работает как copy-paste. В реализации JPHP происходит также, правда происходит не копирование AST дерева, а копирование скомпилированного байткода. В JVM нет конечно же трейтов, но JPHP компилирует трейты в обычные JVM классы и он сам контролирует ограничения трейтов, например не дает создавать объекты от трейтов или наследоваться от трейтов.
Таким образом, можно легко использовать скомпилированный в JVM байткод трейт повторно, не имея оригинальных исходников. В копировании на уровне JVM нет ничего сложного, с этим делом легко справляется библиотека ASM. Единственное, приходится в некоторых местах генерировать немного другой байткод, нежели в обычных классах. Например так происходит с константой
JPHP заменяет в обычных ситуациях константу
Здесь под библиотеками я имею ввиду расширения, написанные на си с применением zend api, в том числе и стандартные функции PHP. Где-то месяц я реализовывал их — функции для строк, массивов и т.п. как в php. Больше всего раздражает то, что даже прочитав описание некоторых функций я не мог с 1-2-3 раза въехать, что будет при передачи различных вариантов аргументов, какой ждать результат. В php функции слишком универсальны, в одну функцию впихивается огромное количество функционала и это сильно затрудняет реализацию таких функций.
На каком-то этапе я понял, что не реализую эти функции на таком уровне, чтобы на JPHP можно было запустить wordpress или symfony к примеру. И отношение к проекту со стороны было бы примерно таким:
Я понял, что отказ от Zend Runtime это очень хорошая идея. PHP часто ругают за кривой рантайм, кривые и не согласованные функции. И было принято решение писать свой рантайм, я думаю активные разработчики, которые любят пробовать что-то новое и экспериментировать не отвернутся от проекта.
Я решил выделить все core классы и функции, которые обязательно будут идти из коробки в JPHP, в отдельный namespace
Этот список еще не полон, не было времени над ним подумать как следует, поэтому классов так мало.
Я думаю многие удивляются, как можно делать такой сложный проект в одиночку и чтобы ничего не ломалось по мере разработки. Эту проблему практически на 100% решают юнит тесты. Тестировать движок языка программирования очень просто, сразу видно что и как надо тестировать.
Первое время я писал собственные простые тесты, на тот момент я не мог задействовать сложные zend тесты языка. Но по мере развития JPHP я начал постепенно внедрять zend тесты, которые можно найти в исходниках самого php. Они тоже не идеальны, иногда приходилось их править, из-за того, что в тестах использовались сторонние функции. Вы поймете, вот пример: тест для тестирования
С помощью Zend тестов я смог исправить огромное количество багов и несогласованностей с языком PHP, особенно в ООП (а там поверьте, очень много нетривиального поведения). Реализация трейтов также происходила с помощью внедрения zend тестов и когда я пишу, что такая-то фича была реализована, это означает что она проходит jphp и zend юнит-тесты.
Dalvik это виртуальная машина совершенно другого типа нежели JVM, она выполняет другой формат байткода и сама по себе является регистровой, а не стековой. JPHP это компилятор под JVM машину и естественно он компилирует байткод несовместимый с Dalvik. Однако, Google любезно предоставил разработчикам интересную утилиту для конвертации JVM байткода в Dalvik байткод. Есть проект Ruboto от JRuby, который тоже может помочь с ориентирами — куда двигаться.
Андроид является безусловно перспективным направлением, но пока не пришло время портирования JPHP под эту платформу. Только когда проект дойдет до 1.0 версии, когда станет стабильным, только тогда я думаю будет в этом смысл.
WEB
Да возможно. JPHP позволяет писать свои web-сервера полностью на php, подобно тому как это пишется в Node.js, Ruby и в других языках. При этом он из коробки будет обеспечивать гибкий и настраиваемый режим hot-reloading для горячей замены кода.
JPHP позволит писать очень производительные сервера, давая механизмы доступа к общей памяти между запросами. Это позволит писать на php фреймворки совершенно другого плана. Если вы слышали о Phalcon, то это что-то похожее, только пишется на Си. JPHP предоставит вам возможность написать такой фреймворк, со сложной логикой, с высокой производительностью на языке php, а не в виде сложного расширения на си или с++. В крайнем случае вы сможете написать Java расширение для узких мест, что намного легче чем писать расширение на си или с++.
GUI
Да многие списывают десктопы, т.к. все уходит в веб. Но эта сфера по прежнему актуальна и пользуется спросом. JPHP позволит писать gui приложения, уже позволяет, есть расширение для Swing, в будущем возможно появится и для JavaFX, который поддерживает HTML5 и CSS3.
Android
Конечно это пока отдаленная перспектива, но она есть. К тому же JPHP может уже запускаться на ARM устройствах где есть Oracle VM, например на Raspberry Pi.
PHP оказался достаточно капризным пациентом, но в итоге операция прошла успешно =). Я понимаю, что многие сторонние разработчики не любят этот язык, что его очень часто ругают, но стоит посмотреть на язык и с другой стороны. Сам я использую PHP когда мне нужно сделать быстро какой-нибудь прототип, фронтэнд для чего-нибудь и часто использую для написания функциональных тестов.
Спасибо за внимание.
JPHP это компилятор языка PHP для Java VM. Две недели назад я писал статью о проекте. Похожие проекты — JRuby для ruby, Jython для python. После публикации первой статьи о JPHP, проект за два дня набрал 500 звёзд на гитхабе и успел засветиться не только в РУнете, но и на зарубежных ресурсах, успел побывать на первом месте в рейтинге гитхаба.
А перед началом небольшие новости.
Последние новости проекта
- У проекта появился свой новостной twitter — https://twitter.com/jphpcompiler
- Ура! Были реализованы трейты из PHP 5.4 как я и обещал
- Был реализован goto из PHP 5.3 и execute кавычки
``
- Добрый человек popsul реализовал бинарные числа
0b
- Реализованные функции от zend runtime были отделены от ядра в отдельный модуль jphp-zend-ext (все кроме Reflection, spl autoloading, итераторов)
- Проведен существенный рефакторинг компилятора
- Еще один добрый человек VISTALL, хорошо знакомый с системой плагинов IDEA, дал много дельных советов и сейчас исследует возможность интеграции отладчика JVM для JPHP в эту известную среду, успехи уже есть
В скором времени у проекта появится отдельный сайт и попробовать JPHP будет очень просто, будут выкладываться различные скомпилированные версии движка. Итак, перейдем к теме…
Начало проекта
Проект начался спонтанно. До этого я искал похожие проекты, т.е. реализацию php для JVM. Есть такой проект как Quercus из Resin, это транслятор в код Java, написанный на Java. Такое положение вещей меня не устраивало, тем более авторы проекта заявляли, что их реализация работает с такой же скоростью как и Zend PHP + APC. До определенного времени существовали и другие реализации PHP для JVM (p8 и projectzero например), но они все умерли и закрылись.
Главной мотивацией начать проект была производительность и JIT. Я уже довольно давно общаюсь с Java, высокая производительность JVM, печеньки, огромное сообщество и качественные библиотеки — это то, что меня в ней привлекает. И немного пораскинув мыслями у меня зародилась идея — взять лучшее из Java и реализовать на ней движок PHP. Собрав раскинутые мысли, я приступил к написанию тестовой версии, порешив на том, что если jphp будет хотя бы раза в 2 быстрее оригинального Zend PHP я продолжу разработку.
Первым делом я прошелся по репозиториям всех известных JVM языков (groovy, jruby, scala) и узнал — какой набор библиотек они используют для генерации байткода JVM. Как оказалась, существует известная сторонняя библиотека — ASM. Она довольно активно развивается, имеет достаточную документацию в PDF, и вроде как поддерживает даже Dalvik (Android) байткод (об этом ниже).
Знакомство с Java VM
Виртуальная машина Java (JVM) довольно мощный инструмент. О том как устроен байткод JVM можно узнать из документации к библиотеке ASM. Кратко описать все возможности VM можно в следующих пунктах:
1. Виртуальная машина стековая
2. Есть возможность хранить локальные переменные по индексам (что-то вроде регистров)
3. GC (сборщик мусора) реализован на уровне VM
4. Объекты и классы реализованы на уровне VM
5. Большое количество стандартных операций — POP, PUSH, DUP, INVOKE, JMP и т.п.
6. Для Try Catch есть инструкции байткода, для finally — частично
7. Для VM есть несколько типов значений: int32, int64, float, double, объекты, массивы скаляров, массивы объектов, для bool, short, byte, char используется int32.
Таким образом я понял, что мне не придется реализовывать самому GC и систему классов объектов с нуля.
Выбор целей и приоритетов
Прежде чем начать разработку, я хорошенько осмыслил главные преимущества PHP, не только как языка, но и как платформы. Самыми очевидными для меня оказались следующие вещи:
- Изолированные окружения — это уже практически аксиома для php, когда выполнение скриптов происходит в отдельном окружении на каждый запрос. Такая схема работы позволяет не задумываться о разделении ресурсов между запросами.
- HOT Reloading — это традиционная схема работы php, когда при смене исходников, заново открывая страницу, мы видим новый результат
Эти преимущества являются также и недостатками. При каждом запросе заново грузятся все классы и это не очень-то хорошо. Проблему частично решают кэшеры байткода, но не до конца. Я подумал, что смогу решить эту проблему, сохранив те преимущества, что я перечислил выше. Что я получил в итоге:
- Environment объекты — изолированные окружения для работы движка, каждое такое окружение имеет свой набор функций, классов, глобальных переменных и настроек
- CompileScope объекты — они хранят скомпилированные классы и функции, реализуют кэширующий механизм для классов, функций и модулей. Environment объекты используют scope для поиска классов и функций, если они уже были скомпилированы для CompileScope, то они моментально загружаются в Environment
Динамическая типизация
На уровне Java VM нет никакой динамической типизации, а в PHP она нужна. Многим это покажется большой проблемой, но это не так.
Для хранения значений я реализовал абстрактный класс Memory. Значения JPHP хранит как объекты Memory. Далее я реализовал классы StringMemory, NullMemory, DoubleMemory, LongMemory, TrueMemory, FalseMemory, ArrayMemory и ObjectMemory. Как вы наверно поняли по названиям классов, каждый из них отвечает за определенный тип — числа, строки и т.п. Все они были унаследованы от Memory.
Memory состоит из абстрактных методов, которые необходимы для реализации операций над значением, например для операторов плюс и минус есть методы
plus()
и minus()
, которые необходимо реализовать в каждом классе Memory. Это виртуальные методы. Этих методов достаточно много, но для этого есть причина — различные оптимизации. Чтобы понять как это работает, приведу псевдо-код:Пример псевдо-кода
Естественно это псевдо-код, а не реальный php-код. Это все происходит под капотом, в байткоде.
$var + 20; // имеется такое простое выражение
// представьте что $var хранит объект класса Memory
$var->plus(20);
// а метод plus возвращает тоже новый объект типа Memory
$x + 20 - $y /* превращается в */ $x->plus(20)->minus($y);
Естественно это псевдо-код, а не реальный php-код. Это все происходит под капотом, в байткоде.
Объект Memory по потреблению памяти не превосходит объекты zval из Zend PHP, и частично поэтому JPHP и Zend PHP примерно эквивалентны по расходу памяти. Для многих значений (например false, true, null, небольшие числа) объекты кэшируются в памяти и не дублируются. Есть ли смысл при каждом
true
создавать новый объект TrueMemory? Конечно же нет.Неудача с динамической типизацией и ее исправление
Как я описал выше, все значения это объекты класса Memory. В самом начале я реализовал autoboxing и для простых константных значений, т.е. например если:
// если вы пишите
$y = $x + 2;
// это примерно превращалось в (на уровне байткода)
$y->assign( $x->plus( new LongMemory(2) ) );
Как вы видите — «2» превращалось в объект. Это было удобно с точки зрения программирования, но с точки зрения производительности сущий кошмар. Такая реализация у меня работала не быстрее чем Zend PHP.
Я решил идти до конца. Чтобы избежать инициирования такого большого количества объектов для простых значений (а представьте этот код в цикле?), я решил реализовать метод plus() и другие аналогичные для базовых типов Java — double, long, boolean и т.п., а не только для Memory. Признаюсь, это была рутина и попахивало говнокодом. Я переделал компилятор и он стал понимать типы и что с ними делать. Компилятор научился подставлять разные типы и разные методы для операций в зависимости от типа элементов в стеке. Да стек просчитывается во время компиляции. Хотя можно было бы сохранить эти константные значения и в таблице констант, но все равно был бы overhead, хотя и не такой большой.
Как оказалось все это я затеял не зря и производительность на синтетических тестах начала обгонять Zend PHP уже в более 2-3-4-10 раз, например на циклах, переменных и т.п.
Магия переменных
PHP поистине магический язык, я точно не уверен, но в каком еще языке во время выполнения можно обращаться к переменной по названию из строки? Простой пример:
$var = "foobar";
${'var'} = 100500;
$name = 'var';
echo $$name;
Чтобы реализовать такую магию, необходимо использовать именную таблицу переменных — хеш таблицу. JVM предоставляет возможность хранения переменных по индексам, естественно, превращать имена переменных в индексы во время компиляции это более логичный шаг, он обеспечивает очень быстрый доступ к переменным, более быстрый чем в хеш таблице. Сравните, что будет быстрее — поиск по хеш таблице или обращение по индексу к массиву?
Я сначала забыл про нее и реализовал переменные на индексах. Производительность обращения к переменным была на высоте. Когда я вспомнил про такую магию, пришлось переделать на хеш таблицу и… все было плохо. Производительность работы с переменными упала буквально в 2-3 раза.
Не долго думая, я решил реализовать в компиляторе 2 режима компиляции переменных — в индексы и с хеш-таблицей. Анализатор помогает выявить код, в котором необходимо обращение к переменным по строке. Это происходит по следующим признакам:
- Если в коде есть выражения:
$$var
,${...}
- Имеются функции eval, include, require, get_defined_vars, extract, compact
- Глобальная область видимости
Почему в коде, содержащем require и include нужно сохранять имена переменных в хеш-таблице? Да всё просто, PHP должен передавать переменные внутрь подключаемых скриптов, то же самое и с eval. А остальные функции работают с именами переменных.
Очень часто вам и не нужна такая магия переменных, а значит ваш код будет работать быстрее в JPHP. Существует также глобальная область видимости переменных. В этой области автоматически используется механизм хранения переменных в хеш-таблице, т.к. предполагается, что к глобальным переменных можно обращаться через массив
$GLOBALS
по имени и когда угодно.Супер — глобальные переменные
$GLOBALS, $_SERVER, $_ENV и т.п.
, для таких переменных прописывать ключевое слово global не нужно. Их реализация довольно проста, компилятор заранее знает имена супер-глобальных переменных и если такие встречаются, то для доступа к такой переменной подставляет другой код, пример псевдо-кода:Пример супер-глобальной переменной
function test() {
$a = $GLOBALS['a'];
...
$GLOBALS['x'] = 33;
}
// превращается примерно в следуещее на уровне байткода
function test() {
$GLOBALS =& getGlobalVar('GLOBALS'); // ключевой момент
$a->assign($GLOBALS['a']);
...
$GLOBALS['x']->assign(33);
}
Массивы, ссылки и Immutable значения, GC
В PHP массивы копируются, а не передаются по ссылке. Однако, копирование массива происходит не в момент присваивания =, т.к. это бы создавало большой overhead. Внутри движка JPHP, да и в Zend PHP, массивы копируются по ссылке, но в момент изменения массива он копируется (в том случае если количество ссылок на массив > 1).
JPHP не использует подсчет ссылок, он использует стандартный GC от Java, который умеет удалять и циклические ссылки. Это создает проблемы при реализации таких массивов. Поэтому я реализовал специальный механизм превращения любого значения Memory в immutable значение. Покажу сначала псевдо-код:
$x = array(1, 2, 3);
$y = $x;
$y[0] = 100; // здесь массив в $y копируется, чтобы не изменять массив $x
// в байткоде это выглядит примерно так (псевдо-код):
$x->assign( array(1,2,3) );
$y->assign($x->toImmutable()); // достать неизменяемое значение переменной $x
$y[0]->assign(100);
// если вы напишите так:
$y =& $x;
// то метод ->toImmutable будет просто опущен
$y->assign($x);
В JPHP есть еще один вид Memory объектов — ReferenceMemory, эти объекты просто хранят ссылку на другой объект Memory. Однако не всегда переменные хранятся как Reference объекты, в некоторых случаях локальные переменные могут обходится без таких ссылок и использовать напрямую байткод для записи нового значения в ячейку, это работает естественно быстрее чем обычный метод
->assign().
Reference объекты возвращают из метода
toImmutable
свое реальное значение, а массивы возвращают в свою очередь специальную ссылочную копию массива, которая копируется при первом же изменении. Поэтому, чтобы присвоить переменной ссылку на другую переменную компилятору достаточно не использовать метод toImmutable
.Реализация классов и функций
Классы уже существуют на уровне JVM. Если говорить в общем, то Java, Scala, Groovy, JRuby генерируют одинаковые классы с разной сигнатурой в рамках JVM. JPHP при компиляции php классов использует JVM классы с особенной сигнатурой:
Memory methodName(Environment env, Memory[] args)
В каждый метод и функцию передается объект Environment, этот объект позволяет узнать очень многое об окружении, в котором выполняется метод. Классы и функции компилируются одинаково для всех окружений. Memory[] это массив переданных аргументов в метод. Метод должен всегда возвращать что-то, а не void, потому что в PHP даже если функция ничего не возвращает, она возвращает null, вот такая тавтология.
Функции php также компилируются в классы, т.к. в JVM нет такого понятия как функции. На выходе мы получаем класс с одним статическим методом, который по сути и является нашей функцией. Почему функции не компилируются в методы одного класса? Это хороший вопрос, и скорее всего необходимо это переделать, чтобы не плодить лишние классы, но пока так удобнее.
JVM умеет легко загружать классы из памяти во время выполнения, достаточно написать свой Java загрузчик классов. Поэтому JPHP компилирует во время выполнения и может во время выполнения загружать классы и не все сразу.
Долгий старт и решение проблемы
В движке была также реализована возможность для написания классов на самой Java. Однако, не все так просто. Все методы таких классов должны иметь нужную сигнатуру и помечаться некоторыми вспомогательными аннотациями (например для type hinting), один из примеров такого класса:
php\lang\System класс
import php.runtime.Memory;
import php.runtime.env.Environment;
import php.runtime.lang.BaseObject;
import php.runtime.reflection.ClassEntity;
import static php.runtime.annotation.Reflection.*;
@Name("php\\lang\\System")
final public class WrapSystem extends BaseObject {
public WrapSystem(Environment env, ClassEntity clazz) {
super(env, clazz);
}
@Signature
private Memory __construct(Environment env, Memory... args) {
return Memory.NULL;
}
@Signature(@Arg("status"))
public static Memory halt(Environment evn, Memory... args) {
System.exit(args[0].toInteger());
return Memory.NULL;
}
}
По мере роста числа новых нативных классов для JPHP я заметил, что время затрачиваемое на регистрацию классов увеличивалось. Чем больше было расширений и классов, тем больше становилась задержка перед запуском движка. Это меня беспокоило. И пришла идея — как это исправить.
PHP как язык обладает механизмом ленивой загрузки классов, все про это знают. Я просто воспользовался механизмом ленивой загрузки классов для регистрации нативных классов. Когда регистрируется нативный java класс, он просто прописывается в таблицу имен, а де-факто регистрация происходит в момент первого использования этого класса. Реализовав этот механизм, я получил хорошие результаты, время инициализации движка уменьшилось в 2-3 раза, а время прохождения тестов уменьшилось с 24 секунд, до 13 секунд. Благодаря этому количество нативных классов практически не будет влиять на скорость инициализации движка.
Скорость старта движка особенно важна в GUI приложениях.
Проблемы возникшие с JVM
1. Именование JVM классов. JVM следит за соблюдением стандарта именования классов. Если вы пишите файловый путь к классу в байткоде, то JVM проверяет соответствие именования этого класса как в языке Java. Это чем-то напоминает стандарт PSR-0. Однако, если класс размещать в global пакете jvm, такой проверки не происходит. PHP может хранить в одном файле сколько угодно классов и функций, и они могут иметь любые причудливые названия. Поэтому пришлось отвязать привязку имен php классов к именам JVM классов внутри байткода. Но это не единственная причина такого выбора…
2. Уникальные имена классов. JPHP должен уметь сохранять полученный байткод в файл и загружать его в любое окружение, поэтому все классы на уровне jvm должны иметь уникальные имена, чтобы не было конфликтов. Во время загрузки jvm байткода изменить имя класса нельзя, по крайней мере я еще не пытался. Пока как временное решение я генерирую случайное имя класса для JVM на основе UUID + некоторые вещи. Думаю это не очень элегантное решение, в будущем надеюсь найдется получше. Использовать имя файла, в котором находится класс нельзя, т.к. код может находится и вовсе не в классе, а файл байткода может переноситься с компьютера на компьютер и его имя может меняться.
3. Ограничения рефлексии. Через рефлексию Java невозможно вызвать метод в контексте класса родителя, т.е. что-то вроде
super.call()
, а в php parent::
. Конечно в Java 7 ввели invokeDynamic, который позволяет такое провернуть, но он работает на удивление медленнее чем рефлексия. Отказ от invokeDynamic был в первую очередь по причине низкой производительности в Java 7, хотя в Java 8 эту проблему решили и теперь по скорости они одинаковы (может быть я его не правильно готовлю?). К тому же хотелось поддержки Java 6 и более легкой адаптации под Android, в котором я подозреваю никакого invokeDynamic нету, а рефлексия есть.Я решил эту проблему не совсем элегантно, мне пришлось отказаться от стандартного механизма переопределения методов jvm классов, и поэтому имена наследуемых методов на уровне jvm классов разные — по алгоритму
method_name + $ + index
. Такое решение не создало и не создает никаких проблем, зато решает вышеописанную проблему.Как были реализованы Трейты
Трейты это механизм множественного наследования, который ввели в PHP начиная с 5.4 версии. По сути он работает как copy-paste. В реализации JPHP происходит также, правда происходит не копирование AST дерева, а копирование скомпилированного байткода. В JVM нет конечно же трейтов, но JPHP компилирует трейты в обычные JVM классы и он сам контролирует ограничения трейтов, например не дает создавать объекты от трейтов или наследоваться от трейтов.
Таким образом, можно легко использовать скомпилированный в JVM байткод трейт повторно, не имея оригинальных исходников. В копировании на уровне JVM нет ничего сложного, с этим делом легко справляется библиотека ASM. Единственное, приходится в некоторых местах генерировать немного другой байткод, нежели в обычных классах. Например так происходит с константой
__CLASS__
в трейтах.trait Foobar {
function test(){
echo __CLASS__;
}
}
class A {
use Foobar;
}
$a = new A();
$a->test();
JPHP заменяет в обычных ситуациях константу
__CLASS__
во время компиляции на строку с именем класса, в котором находится код. С трейтами так делать нельзя и приходится вычислять значение этой константы динамически во время выполнения, если она встречается в трейтах. Зато __TRAIT__
константа в трейтах работает также как __CLASS__
в классах. Также приходится обходится и с выражениями self
и self::class
. Копирование свойств происходит достаточно просто, поэтому описывать это нет смысла.Отказ от Zend runtime библиотек
Здесь под библиотеками я имею ввиду расширения, написанные на си с применением zend api, в том числе и стандартные функции PHP. Где-то месяц я реализовывал их — функции для строк, массивов и т.п. как в php. Больше всего раздражает то, что даже прочитав описание некоторых функций я не мог с 1-2-3 раза въехать, что будет при передачи различных вариантов аргументов, какой ждать результат. В php функции слишком универсальны, в одну функцию впихивается огромное количество функционала и это сильно затрудняет реализацию таких функций.
На каком-то этапе я понял, что не реализую эти функции на таком уровне, чтобы на JPHP можно было запустить wordpress или symfony к примеру. И отношение к проекту со стороны было бы примерно таким:
Консервативный разработчик: «Зачем мне JPHP если на нем нельзя запустить wordpress, symfony, yii или другой известный проект, вот когда реализуете все zend библиотеки, тогда я подумаю. А пока я лучше посмотрю на HHVM».
Прогрессивный разработчик: «Вы реализовали JPHP и повторили весь php-ный кривой и уродливый рантайм, все несогласованные функции и классы, и зачем надо было тратить на это время?».
Я понял, что отказ от Zend Runtime это очень хорошая идея. PHP часто ругают за кривой рантайм, кривые и не согласованные функции. И было принято решение писать свой рантайм, я думаю активные разработчики, которые любят пробовать что-то новое и экспериментировать не отвернутся от проекта.
Новые функции и классы для замены Zend Runtime
Я решил выделить все core классы и функции, которые обязательно будут идти из коробки в JPHP, в отдельный namespace
php
, чтобы не засорять глобальное пространство имен. Среди них следующие:php\lang\Module
— Механизм загрузки исходников без include и require. Позволяет загрузить файл (подобно include), но не выполняя его сразу, а только по желанию программиста. К тому же класс предоставляет возможность получить информацию о том, какие классы и функции находятся внутри. Он сможет загружать исходники из любых Stream объектов.
С появлением механизма автозагрузки классов, я не вижу смысла использовать include и require, кроме как в обработчике загрузчика классов.php\io\Stream
— стрим объекты вместо fopen, fwrite, и т.п. Это концептуально более правильно, можно использовать typehinting для таких классов, можно создать свой новый класс Stream или использовать существующий для чтения файлов, ресурсов, памяти и т.д.php\lang\Thread, php\lang\ThreadGroup
— классы для работы с мультипоточностью. В языке из коробки должны идти средства для работы с потоками, и на данный момент они не ограничиваются 2 классами.php\io\File
— класс для работы с файлом, заменяет кучу несогласованный функций для работы с файлами, менее зависим от платформы и почти копия класса File из Java, со своими особенностями- Классы для работы с Java кодом, с рефлексией, чтобы была возможность через рефлексию вызывать методы, читать свойства, создавать объекты java классов без создания оберток на Java
php\lang\Environment
изолированные окружения для выполнения кода, поддерживают опции:HOT_RELOAD
для горячей замены кода иCONCURRENT
для использования одного и того же окружения в нескольких потоках.
Этот список еще не полон, не было времени над ним подумать как следует, поэтому классов так мало.
Тестирование JPHP
Я думаю многие удивляются, как можно делать такой сложный проект в одиночку и чтобы ничего не ломалось по мере разработки. Эту проблему практически на 100% решают юнит тесты. Тестировать движок языка программирования очень просто, сразу видно что и как надо тестировать.
Первое время я писал собственные простые тесты, на тот момент я не мог задействовать сложные zend тесты языка. Но по мере развития JPHP я начал постепенно внедрять zend тесты, которые можно найти в исходниках самого php. Они тоже не идеальны, иногда приходилось их править, из-за того, что в тестах использовались сторонние функции. Вы поймете, вот пример: тест для тестирования
set_error_handler
, внутри теста используется функция fopen
. На мой взгляд это очень неправильно, почему функция расширения должна принимать участие в тесте на одну из базовых частей языка? Типичный юнит тест от zend состоит из нескольких секций, пример:Unit тест для трейтов
Как вы заметили, здесь несколько секций:
--TEST--
Use instead to solve a conflict.
--FILE--
<?php
error_reporting(E_ALL);
trait Hello {
public function saySomething() {
echo 'Hello';
}
}
trait World {
public function saySomething() {
echo 'World';
}
}
class MyHelloWorld {
use Hello, World {
Hello::saySomething insteadof World;
}
}
$o = new MyHelloWorld();
$o->saySomething();
?>
--EXPECTF--
Hello
Как вы заметили, здесь несколько секций:
--TEST--
, --FILE--
, --EXPECTF--
— описание теста, выполняемый код и что ожидается в результате.С помощью Zend тестов я смог исправить огромное количество багов и несогласованностей с языком PHP, особенно в ООП (а там поверьте, очень много нетривиального поведения). Реализация трейтов также происходила с помощью внедрения zend тестов и когда я пишу, что такая-то фича была реализована, это означает что она проходит jphp и zend юнит-тесты.
Андроид и Dalvik
Dalvik это виртуальная машина совершенно другого типа нежели JVM, она выполняет другой формат байткода и сама по себе является регистровой, а не стековой. JPHP это компилятор под JVM машину и естественно он компилирует байткод несовместимый с Dalvik. Однако, Google любезно предоставил разработчикам интересную утилиту для конвертации JVM байткода в Dalvik байткод. Есть проект Ruboto от JRuby, который тоже может помочь с ориентирами — куда двигаться.
Андроид является безусловно перспективным направлением, но пока не пришло время портирования JPHP под эту платформу. Только когда проект дойдет до 1.0 версии, когда станет стабильным, только тогда я думаю будет в этом смысл.
Куда двигаться дальше?
WEB
Да возможно. JPHP позволяет писать свои web-сервера полностью на php, подобно тому как это пишется в Node.js, Ruby и в других языках. При этом он из коробки будет обеспечивать гибкий и настраиваемый режим hot-reloading для горячей замены кода.
JPHP позволит писать очень производительные сервера, давая механизмы доступа к общей памяти между запросами. Это позволит писать на php фреймворки совершенно другого плана. Если вы слышали о Phalcon, то это что-то похожее, только пишется на Си. JPHP предоставит вам возможность написать такой фреймворк, со сложной логикой, с высокой производительностью на языке php, а не в виде сложного расширения на си или с++. В крайнем случае вы сможете написать Java расширение для узких мест, что намного легче чем писать расширение на си или с++.
GUI
Да многие списывают десктопы, т.к. все уходит в веб. Но эта сфера по прежнему актуальна и пользуется спросом. JPHP позволит писать gui приложения, уже позволяет, есть расширение для Swing, в будущем возможно появится и для JavaFX, который поддерживает HTML5 и CSS3.
Android
Конечно это пока отдаленная перспектива, но она есть. К тому же JPHP может уже запускаться на ARM устройствах где есть Oracle VM, например на Raspberry Pi.
Заключение
PHP оказался достаточно капризным пациентом, но в итоге операция прошла успешно =). Я понимаю, что многие сторонние разработчики не любят этот язык, что его очень часто ругают, но стоит посмотреть на язык и с другой стороны. Сам я использую PHP когда мне нужно сделать быстро какой-нибудь прототип, фронтэнд для чего-нибудь и часто использую для написания функциональных тестов.
Спасибо за внимание.