Пишем расширения для PHP 7 на C++

Автор: admin от 12-08-2017, 12:20, посмотрело: 33

Мне приходилось писать расширения для того, чтобы воспользоваться функциями C++ библиотек в коде PHP. Ещё, одно тяжёлое расширение портировал с 5й версии на 7ю.



Если загуглить документацию на тему написания расширений для PHP, то, в основном, это будут тексты до 2014 года, актуальные для версии 5. Сам сайт php.net предоставляет обрывчатые и устаревшие сведения, а то, что удаётся найти в их wiki, опять про 5ю версию. Максимум, что удалось найти на офф сайте, это скудный ман по миграции уже написанных расширений.

В итоге, единственным более или менее понятным маном по написанию расширений для меня оказался исходный код PHP, которым я и руководствовался при написании и миграции расширений.



В самом деле, API PHP так поменялся, что даже подробнейшие статьи, такие как Wrapping C++ Classes in a PHP Extension не особенно то помогают при написании расширений под PHP 7.



В данной статье рассматривается работа под Linux, у меня Kubuntu. Для винды нужно писать другие config файлы, а так как в проде винду не ставят и расширять PHP под виндой — дело не благодарное, я в этом не разбирался.



Что нужно



php-dev, gcc, исходные коды php. Комбо по установке всего, что нужно для сборки из исходных кодов, можно легко нагуглить.



Определяем лица на фотографиях



Для определения лиц используем библиотеку OpenCV, тестировалось на версиях >=2.3.1



Отправная точка для создания расширений — утилита ext_skel. Она позволяет создать болванку для нового расширения. Мы будем править код, который получился после выполнения этой команды.



Нужно перейти в папку /ext исходных кодов PHP и от туда выполнить

ext_skel с указанием имени нового расширения:



./ext_skel --extname=phpcv


После этого вновь созданную папку phpcv можно перенести куда-то в более удобное место. Нам от туда нужны папка tests и файлы config.m4, php_phpcv.h и phpcv.c. Файл phpcv.c сразу переименуем в phpcv.cpp.



config.m4



Это конфигурационный файл, используемый утилитой phpize для подготовки нашего расширения к компилированию.



Файл представляет собой эдакий bash скрипт с использованием специальных макросов. Макросы эти определены в файлах acinclude.m4 и aclocal.m4 в исходном коде php, и написаны на языке из скобочек и знаков препинания. На самом деле достаточно почитать коменты, которые начинаются со строк «dnl» и будет более или менее понятно, что эти макросы делают.



Удаляем лишнее, правим код под наши нужды.





PHP_ARG_ENABLE — настраиваем флаг, с помощью которого расширение можно включить или выключить в процессе сборки PHP из исходных кодов.

PHP_REQUIRE_CXX() — необходимо, если мы собираемся использовать C++

Далее, код на bash для поиска opencv.

AC_CHECK_HEADER — проверяем наличие необходимых нам заголовочных файлов

PHP_ADD_LIBRARY_WITH_PATH — подключаем shared библиотеки

PHP_SUBST — это необходимо для формирования make файла

PHP_NEW_EXTENSION — тут указано имя расширения, перечислены *.cpp файлы, которые участвуют в процессе сборки, указаны флаги компилятора.



php_phpcv.h





Тут стоит обратить внимание на конструкцию



extern "C" { ... }

Это нужно для совместимости нашего C++ кода с C кодом PHP.



phpcv.cpp



Вот тут, наконец-то, будет C++ код.

Для нахождения лиц будем использовать метод cv::CascadeClassifier::detectMultiScale().



Расширение будет предоставлять единственную функцию, вот её прототип:



/**
 * @see cv::CascadeClassifier::detectMultiScale()
 * @param string $imgPath
 * @param string $cascadePath
 * @param double $scaleFactor
 * @param int $minNeighbors
 *
 * @return array
 */
function cv_detect_multiscale($imgPath, $cascadePath, $scaleFactor, $minNeighbors) {
}




PHP_MINFO_FUNCTION — добавляет сведения о нашем расширении в вывод phpinfo()

PHP_FUNCTION(cv_detect_multiscale) — код нашей функции. В ней после получения входных параметров с помощью zend_parse_parameters идёт код С++. С помощью библиотеки opencv находим лица и формируем выходной массив с координатами найденных лиц.

zend_function_entry — тут перечисляются функции, которые предоставляются расширением.

zend_module_entry — стандартная конструкция, структура, описывающая наше расширение. Несколько NULL подряд — это вместо методов, которые выполняются при инициализации и shutdown расширения и запроса, нам просто нечего делать во время этих фаз.



В коде можно заметить две магических десятки. Я решил тут не заморачиваться с передачей параметров для минимального и максимального размера лица на фото, которые нужно находить.



tests



Тест расширения состоит из кода, который что-то выводит и проверки вывода. Файлы *.phpt с тестами помещаются в папку tests





Cборка и тестирование



Собрать:



phpize && ./configure && make


Протестировать:



make test


DateTime Pattern Generator



Теперь рассмотрим расширение, которое предоставляет класс, оборачивающий C++ класс.

Добавим недостающий функционал в набор классов расширения intl. Зачем это нужно: https://blog.ksimka.io/a-long-journey-to-formatting-a-date-without-a-year-internationally-with-php/#header. Если коротко, стандартное расширение intl не предоставляет возможности интернационально формировать дату без года, то есть «February 10» или «10 февраля». Это расширение фиксит эту проблему.



В системе должна быть установлена библиотека ICU. В Debian-like системах можно поставить пакет libicu-dev.



config.m4





На этот раз всё очень лаконично. Так как нам нужна библиотека ICU и расширение intl тоже требует её присутствия, то мы просто позаимствовали макрос PHP_SETUP_ICU из intl расширения.

В PHP_NEW_EXTENSION $ICU_INCS — специфичные для ICU флаги.



intl_dtpg.h





Тут мы определяем структуру IntlDateTimePatternGenerator_object. В ней хранится указатель на объект DateTimePatternGenerator из библиотеки ICU, переменная для хранения статуса и объект zend_object, который представляет собой PHP класс. Это такая обёртка для C++ класса. API PHP оперирует объектом zend_object, а мы завернули его в струкутуру и всегда имеем доступ к тому, что находится «по соседству» с zend_object.



Ниже мы видим определение inline функции для извлечения структуры IntlDateTimePatternGenerator_object имея объект zend_object. Под капотом мы делаем в памяти шаг назад от начала zend_object на размер нашей структуры минус размер zend_object. Таким образом мы оказываемся как раз в начале структуры, указатель на которую нам и возвращается. Такой хитрый способ подсмотрен в исходных кодах расширения intl.



intl_dtpg.cpp



Расширение будет предоставлять класс следующей структуры:



class IntlDateTimePatternGenerator
{
    /**
     * @param string $locale
     */
    public function __construct(string $locale) {}

    /**
     * Return the best pattern matching the input skeleton.
     * It is guaranteed to have all of the fields in the skeleton.
     *
     * @param string $skeleton The skeleton is a pattern containing only the variable fields.
     *           For example, "MMMdd" and "mmhh" are skeletons.
     * @return string The best pattern found from the given skeleton.
     */
    public function findBestPattern(string $skeleton) {}
}




Сначала мы определяем хэндлеры, которые будут выполнятся в определённые фазы жизни нашего PHP объекта.



IntlDateTimePatternGenerator_object_dtor — деструктор PHP объекта. Тут нечего делать, вызываем стандартный API.

IntlDateTimePatternGenerator_object_free — освобождение памяти. Важная фаза, нужно сделать всё аккуратно, чтобы не было утечек памяти. Получаем нашу структуру, вызываем деструктор для zend_object, сбрасываем статус и уничтожаем объект С++ класса.

IntlDateTimePatternGenerator_object_create — выделение памяти под новый объект. Код подсмотрен в исходных кодах расширения intl.

Далее определяем методы нашего класса.

PHP_METHOD(IntlDateTimePatternGenerator, __construct) — констуктор, создаётся новый объект DateTimePatternGenerator.

Тут, в zend_parse_parameters, мы получаем строку в виде zend_string объекта, при этом в zend_parse_parameters указывается большая «S». Это нововведение в PHP 7. Но и старый способ с указанием маленькой «s» и получением отдельно C-style строки и её длины тоже работает, как мы видели в предыдущем расширении.

PHP_METHOD(IntlDateTimePatternGenerator, findBestPattern) — тут вызывается метод класса из ICU библиотеки.

Далее следует набор макросов для определения параметров методов класса. По имени параметров этих макросов в файле zend_API.h можно понять, что они значат.

В структуре zend_function_entry указываются методы класса. В макрос PHP_ME передаются ранее определённые наборы параметров и флаги. Эта структура используется при регистрации класса ниже.

PHP_MINIT_FUNCTION(intl_dtpg) — вызывается в момент инициализации расширения. Тут регистрируется наш класс и указываются хендлеры для обслуживания его фаз жизни.

PHP_MSHUTDOWN_FUNCTION(intl_dtpg) — когда нечего делать в момент shutdown, можно просто вернуть SUCCESS, а можно и не указывать и ниже, в zend_module_entry, указать NULL.

PHP_MINFO_FUNCTION(intl_dtpg) — добавляет информацию о расширении в вывод phpinfo()

Снова zend_function_entry, но в этот раз пустая — мы не определяем никаких функций в этом расширении.

zend_module_entry — тут мы указываем методы для инициализации и shutdown нашего расширения, другие два NULL это про request, мы ничего не делаем в момент инициализации и shutdown запроса.



tests



Небольшой тест, убеждаемся что паттерны генерируются:





Сборка и тестирование




phpize && ./configure && make
make test




valgrind — проверяем расширения на утечки памяти



Проверим наше расширение intl_dtpg на утечки памяти. Для этого создадим в папке расширения тестовый ini файл:





и тестовый php файл:



Проверяем:



valgrind php -c test-php.ini test.php


==30326== HEAP SUMMARY:

==30326== in use at exit: 478,022 bytes in 2,151 blocks

==30326== total heap usage: 22,863 allocs, 20,712 frees, 4,718,469 bytes allocated

==30326==

==30326== LEAK SUMMARY:

==30326== definitely lost: 0 bytes in 0 blocks

==30326== indirectly lost: 0 bytes in 0 blocks

==30326== possibly lost: 1,076 bytes in 14 blocks

==30326== still reachable: 476,946 bytes in 2,137 blocks

==30326== of which reachable via heuristic:

==30326== newarray : 30,416 bytes in 74 blocks

==30326== suppressed: 0 bytes in 0 blocks





Вроде неплохо.



В целях экспиремента закоментируем уничтожение объекта



DateTimePatternGenerator в хэндлере IntlDateTimePatternGenerator_object_free



/* {{{ IntlDateTimePatternGenerator_objects_free */
void IntlDateTimePatternGenerator_object_free(zend_object *object)
{
    IntlDateTimePatternGenerator_object *dtpgo = php_intl_datetimepatterngenerator_fetch_object(object);

    zend_object_std_dtor(&dtpgo->zo);

    dtpgo->status = U_ZERO_ERROR;
//     if (dtpgo->dtpg) {
//         delete dtpgo->dtpg;
//         dtpgo->dtpg = nullptr;
//     }
}
/* }}} */




Проверяем:



valgrind php -c test-php.ini test.php


==411== HEAP SUMMARY:

==411== in use at exit: 770,710 bytes in 2,477 blocks

==411== total heap usage: 22,863 allocs, 20,386 frees, 4,718,469 bytes allocated

==411==

==411== LEAK SUMMARY:

==411== definitely lost: 5,232 bytes in 2 blocks

==411== indirectly lost: 287,456 bytes in 324 blocks

==411== possibly lost: 1,076 bytes in 14 blocks

==411== still reachable: 476,946 bytes in 2,137 blocks

==411== of which reachable via heuristic:

==411== newarray : 30,416 bytes in 74 blocks

==411== suppressed: 0 bytes in 0 blocks



Строки «definitely lost» и «indirectly lost» явно указывают на утечку.



Заключение



Хочу сказать, что документация и нейминг API оставляют желать лучшего. Если нужно написать что-то более сложное, чем «Hello, world», придётся изучать исходные коды PHP и встроенных расширений.

Источник: Хабрахабр

Теги: PHP, php, extension

Категория: Программирование

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *