Мне приходилось писать расширения для того, чтобы воспользоваться функциями 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
Отправная точка для создания расширений — утилита
Нужно перейти в папку
После этого вновь созданную папку phpcv можно перенести куда-то в более удобное место. Нам от туда нужны папка
Это конфигурационный файл, используемый утилитой
Файл представляет собой эдакий bash скрипт с использованием специальных макросов. Макросы эти определены в файлах
Удаляем лишнее, правим код под наши нужды.
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 файлы, которые участвуют в процессе сборки, указаны флаги компилятора.
Тут стоит обратить внимание на конструкцию
Это нужно для совместимости нашего C++ кода с C кодом PHP.
Вот тут, наконец-то, будет C++ код.
Для нахождения лиц будем использовать метод cv::CascadeClassifier::detectMultiScale().
Расширение будет предоставлять единственную функцию, вот её прототип:
PHP_MINFO_FUNCTION — добавляет сведения о нашем расширении в вывод phpinfo()
PHP_FUNCTION(cv_detect_multiscale) — код нашей функции. В ней после получения входных параметров с помощью
zend_function_entry — тут перечисляются функции, которые предоставляются расширением.
zend_module_entry — стандартная конструкция, структура, описывающая наше расширение. Несколько
В коде можно заметить две магических десятки. Я решил тут не заморачиваться с передачей параметров для минимального и максимального размера лица на фото, которые нужно находить.
Тест расширения состоит из кода, который что-то выводит и проверки вывода. Файлы *.phpt с тестами помещаются в папку
Собрать:
Протестировать:
Теперь рассмотрим расширение, которое предоставляет класс, оборачивающий 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.
На этот раз всё очень лаконично. Так как нам нужна библиотека ICU и расширение intl тоже требует её присутствия, то мы просто позаимствовали макрос PHP_SETUP_ICU из intl расширения.
В PHP_NEW_EXTENSION
Тут мы определяем структуру
Ниже мы видим определение
Расширение будет предоставлять класс следующей структуры:
Сначала мы определяем хэндлеры, которые будут выполнятся в определённые фазы жизни нашего PHP объекта.
IntlDateTimePatternGenerator_object_dtor — деструктор PHP объекта. Тут нечего делать, вызываем стандартный API.
IntlDateTimePatternGenerator_object_free — освобождение памяти. Важная фаза, нужно сделать всё аккуратно, чтобы не было утечек памяти. Получаем нашу структуру, вызываем деструктор для
IntlDateTimePatternGenerator_object_create — выделение памяти под новый объект. Код подсмотрен в исходных кодах расширения intl.
Далее определяем методы нашего класса.
PHP_METHOD(IntlDateTimePatternGenerator, __construct) — констуктор, создаётся новый объект
Тут, в zend_parse_parameters, мы получаем строку в виде
PHP_METHOD(IntlDateTimePatternGenerator, findBestPattern) — тут вызывается метод класса из ICU библиотеки.
Далее следует набор макросов для определения параметров методов класса. По имени параметров этих макросов в файле
В структуре zend_function_entry указываются методы класса. В макрос PHP_ME передаются ранее определённые наборы параметров и флаги. Эта структура используется при регистрации класса ниже.
PHP_MINIT_FUNCTION(intl_dtpg) — вызывается в момент инициализации расширения. Тут регистрируется наш класс и указываются хендлеры для обслуживания его фаз жизни.
PHP_MSHUTDOWN_FUNCTION(intl_dtpg) — когда нечего делать в момент shutdown, можно просто вернуть
PHP_MINFO_FUNCTION(intl_dtpg) — добавляет информацию о расширении в вывод phpinfo()
Снова zend_function_entry, но в этот раз пустая — мы не определяем никаких функций в этом расширении.
zend_module_entry — тут мы указываем методы для инициализации и shutdown нашего расширения, другие два
Небольшой тест, убеждаемся что паттерны генерируются:
Проверим наше расширение intl_dtpg на утечки памяти. Для этого создадим в папке расширения тестовый ini файл:
и тестовый php файл:
Проверяем:
Вроде неплохо.
В целях экспиремента закоментируем уничтожение объекта
Проверяем:
Строки «definitely lost» и «indirectly lost» явно указывают на утечку.
Хочу сказать, что документация и нейминг API оставляют желать лучшего. Если нужно написать что-то более сложное, чем «Hello, world», придётся изучать исходные коды PHP и встроенных расширений.
Если загуглить документацию на тему написания расширений для 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» и будет более или менее понятно, что эти макросы делают.Удаляем лишнее, правим код под наши нужды.
config.m4
PHP_ARG_ENABLE(phpcv, whether to enable phpcv support, [ --enable-phpcv Enable phpcv support]) if test "$PHP_PHPCV" != "no"; then PHP_REQUIRE_CXX() SEARCH_PATH="/usr/local /usr /opt/local" SEARCH_FOR="/include/opencv2/opencv.hpp" if test -r $PHP_PHPCV/$SEARCH_FOR; then CV_DIR=$PHP_PHPCV else AC_MSG_CHECKING([for opencv in default path]) for i in $SEARCH_PATH ; do if test -r $i/$SEARCH_FOR; then CV_DIR=$i AC_MSG_RESULT(found in $i) break fi done fi if test -z "$CV_DIR"; then AC_MSG_RESULT([not found]) AC_MSG_ERROR([Please reinstall the OpenCV distribution]) fi AC_CHECK_HEADER([$CV_DIR/include/opencv2/objdetect/objdetect.hpp], [], AC_MSG_ERROR('opencv2/objdetect/objdetect.hpp' header not found)) AC_CHECK_HEADER([$CV_DIR/include/opencv2/highgui/highgui.hpp], [], AC_MSG_ERROR('opencv2/highgui/highgui.hpp' header not found)) PHP_ADD_LIBRARY_WITH_PATH(opencv_objdetect, $CV_DIR/lib, PHPCV_SHARED_LIBADD) PHP_ADD_LIBRARY_WITH_PATH(opencv_highgui, $CV_DIR/lib, PHPCV_SHARED_LIBADD) PHP_ADD_LIBRARY_WITH_PATH(opencv_imgproc, $CV_DIR/lib, PHPCV_SHARED_LIBADD) PHP_SUBST(PHPCV_SHARED_LIBADD) PHP_NEW_EXTENSION(phpcv, phpcv.cpp, $ext_shared,, -std=c++0x -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1) fi
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
php_phpcv.h
#ifndef PHP_PHPCV_H #define PHP_PHPCV_H #define PHP_PHPCV_EXTNAME "phpcv" #define PHP_PHPCV_VERSION "0.2.0" #ifdef HAVE_CONFIG_H #include "config.h" #endif extern "C" { #include "php.h" #include "ext/standard/info.h" } #ifdef ZTS #include "TSRM.h" #endif extern zend_module_entry phpcv_module_entry; #define phpext_phpcv_ptr &phpcv_module_entry #if defined(ZTS) && defined(COMPILE_DL_PHPCV) ZEND_TSRMLS_CACHE_EXTERN(); #endif #endif /* 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) { }
phpcv.cpp
#include "php_phpcv.h" #include <opencv2/objdetect/objdetect.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> PHP_MINFO_FUNCTION(phpcv) { php_info_print_table_start(); php_info_print_table_header(2, "phpcv support", "enabled"); php_info_print_table_end(); } PHP_FUNCTION(cv_detect_multiscale) { char *imgPath = NULL, *cascadePath = NULL; long imgPathLen, cascadePathLen, minNeighbors; double scaleFactor, minWidth, minHeight; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ssdl", &imgPath, &imgPathLen, &cascadePath, &cascadePathLen, &scaleFactor, &minNeighbors) == FAILURE) { RETURN_FALSE; } // Read Image cv::Mat image; image = cv::imread(imgPath, CV_LOAD_IMAGE_GRAYSCALE); if (image.empty()) { RETURN_FALSE; } equalizeHist(image, image); //min size for detected object, discarding objects smaller than this minWidth = image.size().width / 10; minHeight = image.size().height / 10; // Load Face cascade (.xml file) cv::CascadeClassifier faceCascade; if (!faceCascade.load(cascadePath)) { RETURN_FALSE; } // Detect faces std::vector<cv::Rect> faces; faceCascade.detectMultiScale(image, faces, scaleFactor, minNeighbors, 0, cv::Size(minWidth, minHeight)); array_init(return_value); // Build array to return for ( int i = 0; i < faces.size(); i++ ) { // Now we have: faces[i].x faces[i].y faces[i].width faces[i].height zval face; array_init(&face); add_assoc_long(&face, "x", faces[i].x); add_assoc_long(&face, "y", faces[i].y); add_assoc_long(&face, "w", faces[i].width); add_assoc_long(&face, "h", faces[i].height); add_next_index_zval(return_value, &face); } } const zend_function_entry phpcv_functions[] = { PHP_FE(cv_detect_multiscale, NULL) PHP_FE_END }; zend_module_entry phpcv_module_entry = { STANDARD_MODULE_HEADER, PHP_PHPCV_EXTNAME, phpcv_functions, NULL, NULL, NULL, NULL, PHP_MINFO(phpcv), PHP_PHPCV_VERSION, STANDARD_MODULE_PROPERTIES }; #ifdef COMPILE_DL_PHPCV #ifdef ZTS ZEND_TSRMLS_CACHE_DEFINE(); #endif ZEND_GET_MODULE(phpcv) #endif
PHP_MINFO_FUNCTION — добавляет сведения о нашем расширении в вывод phpinfo()
PHP_FUNCTION(cv_detect_multiscale) — код нашей функции. В ней после получения входных параметров с помощью
zend_parse_parameters идёт код С++. С помощью библиотеки opencv находим лица и формируем выходной массив с координатами найденных лиц.zend_function_entry — тут перечисляются функции, которые предоставляются расширением.
zend_module_entry — стандартная конструкция, структура, описывающая наше расширение. Несколько
NULL подряд — это вместо методов, которые выполняются при инициализации и shutdown расширения и запроса, нам просто нечего делать во время этих фаз.В коде можно заметить две магических десятки. Я решил тут не заморачиваться с передачей параметров для минимального и максимального размера лица на фото, которые нужно находить.
tests
Тест расширения состоит из кода, который что-то выводит и проверки вывода. Файлы *.phpt с тестами помещаются в папку
tests002.phpt
--TEST-- Test face detection --SKIPIF-- <?php if (!extension_loaded("phpcv")) print "skip"; ?> --FILE-- <?php $cascade = '/usr/share/opencv/haarcascades/haarcascade_frontalface_alt2.xml'; $img = __DIR__ . '/img.jpg'; $faces = cv_detect_multiscale($img, $cascade, 1.1, 5); if (is_array($faces) && count($faces) > 0) { echo 'face detection works'; } ?> --EXPECT-- face detection works
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
config.m4
PHP_ARG_ENABLE(intl-dtpg, whether to enable intl-dtpg support, [ --enable-intl-dtpg Enable intl-dtpg support]) if test "$PHP_INTL_DTPG" != "no"; then PHP_SETUP_ICU(INTL_DTPG_SHARED_LIBADD) PHP_SUBST(INTL_DTPG_SHARED_LIBADD) PHP_REQUIRE_CXX() PHP_NEW_EXTENSION(intl_dtpg, intl_dtpg.cpp, $ext_shared,,-std=c++0x $ICU_INCS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1) fi
На этот раз всё очень лаконично. Так как нам нужна библиотека ICU и расширение intl тоже требует её присутствия, то мы просто позаимствовали макрос PHP_SETUP_ICU из intl расширения.
В PHP_NEW_EXTENSION
$ICU_INCS — специфичные для ICU флаги.intl_dtpg.h
intl_dtpg.h
#ifndef INTL_DTPG_H #define INTL_DTPG_H #include <zend_modules.h> #include <zend_types.h> #include <unicode/dtptngen.h> extern "C" { #include "php.h" #include "ext/standard/info.h" } extern zend_module_entry intl_dtpg_module_entry; #define phpext_intl_dtpg_ptr &intl_dtpg_module_entry #define INTL_DTPG_VERSION "1.0.0" #ifdef ZTS #include "TSRM.h" #endif typedef struct { DateTimePatternGenerator *dtpg; UErrorCode status; zend_object zo; } IntlDateTimePatternGenerator_object; static inline IntlDateTimePatternGenerator_object *php_intl_datetimepatterngenerator_fetch_object(zend_object *obj) { return (IntlDateTimePatternGenerator_object *)((char*)(obj) - XtOffsetOf(IntlDateTimePatternGenerator_object, zo)); } #if defined(ZTS) && defined(COMPILE_DL_INTL_DTPG) ZEND_TSRMLS_CACHE_EXTERN() #endif #endif /* 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) {} }
intl_dtpg.cpp
#ifdef HAVE_CONFIG_H #include "config.h" #endif #include "intl_dtpg.h" #include <unicode/ustdio.h> #include <unicode/smpdtfmt.h> zend_class_entry *IntlDateTimePatternGenerator_ce; zend_object_handlers IntlDateTimePatternGenerator_object_handlers; /* {{{ IntlDateTimePatternGenerator_objects_dtor */ static void IntlDateTimePatternGenerator_object_dtor(zend_object *object) { zend_objects_destroy_object(object); } /* }}} */ /* {{{ 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; } } /* }}} */ /* {{{ IntlDateTimePatternGenerator_object_create */ zend_object *IntlDateTimePatternGenerator_object_create(zend_class_entry *ce) { IntlDateTimePatternGenerator_object* intern; intern = (IntlDateTimePatternGenerator_object*)ecalloc(1, sizeof(IntlDateTimePatternGenerator_object) + zend_object_properties_size(ce)); zend_object_std_init(&intern->zo, ce); object_properties_init(&intern->zo, ce); intern->dtpg = nullptr; intern->status = U_ZERO_ERROR; intern->zo.handlers = &IntlDateTimePatternGenerator_object_handlers; return &intern->zo; } /* }}} */ /* {{{ proto void IntlDateTimePatternGenerator::__construct(string $locale) * IntlDateTimePatternGenerator object constructor. */ PHP_METHOD(IntlDateTimePatternGenerator, __construct) { zend_string *locale; zval *object; IntlDateTimePatternGenerator_object* dtpg = nullptr; if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &locale) == FAILURE) { return; } object = getThis(); dtpg = php_intl_datetimepatterngenerator_fetch_object(Z_OBJ_P(object)); dtpg->status = U_ZERO_ERROR; dtpg->dtpg = DateTimePatternGenerator::createInstance(Locale(ZSTR_VAL(locale)), dtpg->status); } /* }}} */ /* {{{ proto string IntlDateTimePatternGenerator::findBestPattern(string $skeleton) * Return the best pattern matching the input skeleton. */ PHP_METHOD(IntlDateTimePatternGenerator, findBestPattern) { zend_string *skeleton; zval *object; IntlDateTimePatternGenerator_object* dtpg = nullptr; if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &skeleton) == FAILURE) { return; } object = getThis(); dtpg = php_intl_datetimepatterngenerator_fetch_object(Z_OBJ_P(object)); UnicodeString pattern = dtpg->dtpg->getBestPattern(UnicodeString(ZSTR_VAL(skeleton)), dtpg->status); std::string s; pattern.toUTF8String(s); RETURN_STRING(s.c_str()); } /* }}} */ ZEND_BEGIN_ARG_INFO_EX(arginfo_findBestPattern, 0, 0, 1) ZEND_ARG_INFO(0, skeleton) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo___construct, 0, 0, 1) ZEND_ARG_INFO(0, locale) ZEND_END_ARG_INFO() const zend_function_entry IntlDateTimePatternGenerator_functions[] = { PHP_ME(IntlDateTimePatternGenerator, __construct, arginfo___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) PHP_ME(IntlDateTimePatternGenerator, findBestPattern, arginfo_findBestPattern, ZEND_ACC_PUBLIC) PHP_FE_END }; /* {{{ PHP_MINIT_FUNCTION */ PHP_MINIT_FUNCTION(intl_dtpg) { zend_class_entry ce; INIT_CLASS_ENTRY(ce, "IntlDateTimePatternGenerator", IntlDateTimePatternGenerator_functions); ce.create_object = IntlDateTimePatternGenerator_object_create; IntlDateTimePatternGenerator_ce = zend_register_internal_class(&ce); memcpy(&IntlDateTimePatternGenerator_object_handlers, zend_get_std_object_handlers(), sizeof IntlDateTimePatternGenerator_object_handlers); IntlDateTimePatternGenerator_object_handlers.offset = XtOffsetOf(IntlDateTimePatternGenerator_object, zo); IntlDateTimePatternGenerator_object_handlers.clone_obj = NULL; //no clone support IntlDateTimePatternGenerator_object_handlers.dtor_obj = IntlDateTimePatternGenerator_object_dtor; IntlDateTimePatternGenerator_object_handlers.free_obj = IntlDateTimePatternGenerator_object_free; if(!IntlDateTimePatternGenerator_ce) { zend_error(E_ERROR, "Failed to register IntlDateTimePatternGenerator class"); return FAILURE; } return SUCCESS; } /* }}} */ /* {{{ PHP_MSHUTDOWN_FUNCTION */ PHP_MSHUTDOWN_FUNCTION(intl_dtpg) { return SUCCESS; } /* }}} */ /* {{{ PHP_MINFO_FUNCTION */ PHP_MINFO_FUNCTION(intl_dtpg) { php_info_print_table_start(); php_info_print_table_header(2, "intl_dtpg support", "enabled"); php_info_print_table_header(2, "intl_dtpg version", INTL_DTPG_VERSION); php_info_print_table_end(); } /* }}} */ /* {{{ intl_dtpg_functions[] */ const zend_function_entry intl_dtpg_functions[] = { PHP_FE_END }; /* }}} */ /* {{{ intl_dtpg_module_entry */ zend_module_entry intl_dtpg_module_entry = { STANDARD_MODULE_HEADER, "intl_dtpg", intl_dtpg_functions, PHP_MINIT(intl_dtpg), PHP_MSHUTDOWN(intl_dtpg), NULL, NULL, PHP_MINFO(intl_dtpg), INTL_DTPG_VERSION, STANDARD_MODULE_PROPERTIES }; /* }}} */ #ifdef COMPILE_DL_INTL_DTPG #ifdef ZTS ZEND_TSRMLS_CACHE_DEFINE() #endif ZEND_GET_MODULE(intl_dtpg) #endif
Сначала мы определяем хэндлеры, которые будут выполнятся в определённые фазы жизни нашего 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
Небольшой тест, убеждаемся что паттерны генерируются:
001.phpt
--TEST-- Check for intl_dtpg presence --SKIPIF-- <?php if (!extension_loaded("intl_dtpg")) print "skip"; ?> --FILE-- <?php $dtpg = new IntlDateTimePatternGenerator('ru_RU'); $ruPattern = $dtpg->findBestPattern('MMMMd'); $dtpg = new IntlDateTimePatternGenerator('en_US'); $enPattern = $dtpg->findBestPattern('MMMMd'); echo $ruPattern . ';' . $enPattern; ?> --EXPECT-- d MMMM;MMMM d
Сборка и тестирование
phpize && ./configure && make make test
valgrind — проверяем расширения на утечки памяти
Проверим наше расширение intl_dtpg на утечки памяти. Для этого создадим в папке расширения тестовый ini файл:
test-php.ini
extension=modules/intl_dtpg.so
и тестовый php файл:
test.php
<?php $dtpg = new IntlDateTimePatternGenerator('ru_RU'); $ruPattern = $dtpg->findBestPattern('MMMMd'); $dtpg = new IntlDateTimePatternGenerator('en_US'); $enPattern = $dtpg->findBestPattern('MMMMd'); echo $ruPattern . ';' . $enPattern . "\n";
Проверяем:
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 и встроенных расширений.