Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

JPHP — Как оно работает. История создания

Anna | 29.05.2014 | нет комментариев

В этой статье я расскажу больше детально об истории плана JPHP и каким образом он был разработан с технической стороны. Текст будет увлекателен как простым разработчикам PHP, так и любителям компиляторов. Я постарался описать все на простом языке.

image

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. Это виртуальные способы. Этих способов довольно много, но для этого есть повод — разные оптимизации. Дабы осознать как это работает, приведу псевдо-код:

Пример псевдо-кода

$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. Сознаюсь, это была рутина и попахивало говнокодом. Я переделал компилятор и он стал понимать типы и что с ними делать. Компилятор обучился подставлять различные типы и разs_permark!$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), один из примеров такого класса:

phplangSystem класс

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, Дабы не загрязнять глобальное пространство имен. Среди них следующие:

  1. phplangModule — Механизм загрузки исходников без include и require. Разрешает загрузить файл (аналогично include), но не исполняя его сразу, а только по желанию программиста. К тому же класс предоставляет вероятность получить информацию о том, какие классы и функции находятся внутри. Он сумеет загружать исходники из всяких Stream объектов.
    С возникновением механизма автозагрузки классов, я не вижу смысла применять include и require, помимо как в обработчике загрузчика классов.
  2. phpioStream — стрим объекты взамен fopen, fwrite, и т.п. Это концептуально больше верно, дозволено применять typehinting для таких классов, дозволено сделать свой новейший класс Stream либо применять присутствующий для чтения файлов, источников, памяти и т.д.
  3. phplangThread, phplangThreadGroup — классы для работы с мультипоточностью. В языке из коробки обязаны идти средства для работы с потоками, и на данный момент они не ограничиваются 2 классами.
  4. phpioFile — класс для работы с файлом, заменяет кучу несогласованный функций для работы с файлами, менее зависим от платформы и примерно копия класса File из Java, со своими особенностями
  5. Классы для работы с Java кодом, с рефлексией, Дабы была вероятность через рефлексию вызывать способы, читать свойства, создавать объекты java классов без создания оберток на Java
  6. phplangEnvironment изолированные окружения для выполнения кода, поддерживают опции: 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

Источник: programmingmaster.ru

Оставить комментарий
БАЗА ЗНАНИЙ
СЛУЧАЙНАЯ СТАТЬЯ
СЛУЧАЙНЫЙ БЛОГ
СЛУЧАЙНЫЙ МОД
СЛУЧАЙНЫЙ СКИН
НОВЫЕ МОДЫ
НОВЫЕ СКИНЫ
НАКОПЛЕННЫЙ ОПЫТ
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB