- Пишем PHP extension
- Компиляция под Win32
- Компиляция под *nix
- Обработка аргументов и возвращаемые значения
- Производительность
- Выводы
- Материалы
- Два типа расширений PHP. Zend extension VS PHP module
- С точки зрения конечного пользователя.
- С точки зрения решаемых задач
- С точки зрения разработчика, который раньше не писал расширений для PHP и вдруг сподобился
- С точки зрения жизненного цикла
- Бонус. Гибридные расширения
Пишем PHP extension
А давайте сегодня взглянем на PHP немного с другой точки зрения, и напишем к нему расширение. Так как на эту тему уже были публикации на Хабре (здесь и здесь), то не будем углубляться в причины того, для чего это может оказаться полезным и для чего может быть использовано на практике. Эта статья расскажет, как собирать простые расширения под Windows с использованием Visual C++ и под Debian с использованием GCC. Также я постараюсь немного осветить работу с PHP-массивами внутри расширений и провести сравнение производительности алгоритма, написанного на native PHP и использующего код, написанный на C.
Компиляция под Win32
Итак, начнем с Windows. Как известно, разработчики PHP используют Visual C++ 9 или Visual Studio 2008 для компиляции своего детища под Windows. Поэтому мы будем использовать Visual Studio 2008, бесплатная Express версия тоже подойдет, как впрочем, наверное, и более поздние и ранние версии студии.
- Скомпилированные бинарники PHP 5.3, которые можно взять здесь,
- Исходники PHP 5.3, которые можно скачать с сайта или вытянуть из общедоступного SVN,
- Желание поэкспериментировать и немного терпения.
php-5.3.6 php-5.3.6\main php-5.3.6\TSRM php-5.3.6\Zend
PHP_WIN32 ZEND_WIN32 ZTS=1 ZEND_DEBUG=0
#ifndef STDAFX #define STDAFX #define PHP_COMPILER_ID "VC9" // эту опцию мы указываем для совместимости с PHP, скомпилированным Visual C++ 9.0 #include "zend_config.w32.h" #include "php.h" #endif
Если вы попытаетесь скомпилировать проект на данном этапе, вы получите ошибку, говорящую о том, что отсутствует main\config.w32.h. Его можно получить либо запустив скрипт main\configure.bat, либо можно выдернуть его из исходников, например версии PHP 5.2. При этом не забываем отредактировать в этом файле все пути и раскомментировать директиву «#define HAVE_SOCKLEN_T». Теперь проект должен скомпилироваться без ошибок.
Теперь давайте напишем hello world, добавим в наш cpp файл следующее:
PHP_FUNCTION(test); const zend_function_entry test_functions[] = < PHP_FE(test, NULL) >; zend_module_entry test_module_entry = < STANDARD_MODULE_HEADER, // #if ZEND_MODULE_API_NO >= 20010901 "test", // название модуля test_functions, // указываем экспортируемые функции NULL, // PHP_MINIT(test), Module Initialization NULL, // PHP_MSHUTDOWN(test), Module Shutdown NULL, // PHP_RINIT(test), Request Initialization NULL, // PHP_RSHUTDOWN(test), Request Shutdown NULL, // PHP_MINFO(test), Module Info (для phpinfo()) "0.1", // версия нашего модуля STANDARD_MODULE_PROPERTIES >; ZEND_GET_MODULE(test) PHP_FUNCTION(test) < RETURN_STRING("hello habr", 1); // возвращаем PHP-строку, второй параметр указывает, нужно ли копировать строку в памяти или нет >
На что мы должны получить ответ «hello habr».
Компиляция под *nix
- Иметь установленный PHP на машине,
- Иметь установленный PHP-dev. Для этого нужно выполнить всего одну команду:
Первый нужен для магической компиляции расширения, а во втором будет его исходный код. В config.m4 напишем следующее:
PHP_ARG_ENABLE(test, Enable test support) if test "$PHP_TEST" = "yes"; then AC_DEFINE(HAVE_TEST, 1, [You have test extension]) PHP_NEW_EXTENSION(test, test.c, $ext_shared) fi
# phpize // команда сгенерирует необходимые файлы для следующего шага # ./configure // сгенерируется makefile # make // компилируем # make install // устанавливаем .so в директорию с PHP расширениями
Обработка аргументов и возвращаемые значения
Для начала посмотрим, как можно принимать аргументы:
char* text; int text_length; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &text, &text_lenght) == FAILURE)
Третий параметр указывает ожидаемый тип (здесь можно просмотреть все варианты), в данном случае это char* или int. Также по ссылке можно найти варианты комбинирования типов и указания количества аргументов. Все следующие параметры являются переменными, в которые будут записаны переданные значения. При передаче строки передается сама строка и ее длина.
Если количество аргументов, переданных в вашу функцию, не совпадает, будет выброшен E_WARNING, при этом вы можете возвратить какое-либо значение, например, сообщение об ошибке.
Возвращать можно как простые типы, так и сложные. Давайте познакомимся с формированием возвращаемого массива. Для указания того, что будет возвращен массив, его нужно проинициализировать:
Для добавления значений в массив необходимо использовать функции, зависящие от того, какой индекс и значение добавляется в массив. Например:
add_next_index_long(result, 42); // $result[] = 42; add_assoc_bool(result, "foo", 1); // $result['foo'] = true; add_next_index_null(result); // $result[] = NULL;
Полный список функций можно найти здесь
Если кого-то заинтересует, я могу в следующей статье рассмотреть пример работы с объектами (классический пример расширения на объектах — mysqli). Тут есть очень хорошая статья на эту тему.
Производительность
Для проверки производительности я выбрал несколько синтетический пример: подсчет вхождения каждого символа в строку. Другими словами, мы должны получить функцию, которая принимает строку в качестве параметра, и отдает массив, в котором указано количество употреблений каждого символа в данной строке. Этот пример продемонстрирует работу с большими строками.
У меня получилась такая реализация, сильно не пинайте за код, я все-таки больше пишу на PHP, чем на C:
PHP_FUNCTION(calculate_chars) < char* text; int text_length; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &text, &text_length) == FAILURE) < return; >array_init(return_array); int table[256] = < 0 >; for (int i = 0; i < text_length; i++) < table[((unsigned char*)text)[i]]++; >char str[2]; str[1] = '\0'; for (int i = 0; i < 256; i++) < if (table[i]) < str[0] = (char)i; add_assoc_long(return_array, str, table[i]); >> >
user> php -r "print_r( calculate_chars('example') );" Array ( [a] => 1 [e] => 2 [l] => 1 [m] => 1 [p] => 1 [x] => 1 >
А теперь давайте сравним скорость выполнения этого кода и аналогичного на native PHP:
$map = array(); for ($i = 0; $i < $length; $i++) < $char = $text[$i]; if (isset($map[$char])) < $map[$char]++; >else < $map[$char] = 1; >>
Сравнивать я буду время выполнения обоих решений с помощью функции microtime. Возьмем строку в 100 символов, строку в 5000 символов, и строку в 69000 символов (я взял книгу A Message from the Sea, написанную Чарльзом Диккенсом, надеюсь, что он мне это простит), и для каждого варианта прогоним оба решения по несколько тысяч раз. Результаты приведены в таблице ниже. Тестирование проводилось на моем не самом сильном домашнем ноутбуке и VDS с Debian на борту, и да, я отчетливо понимаю, что результаты могут зависеть от конфигурации, от версии операционной системы, PHP, атмосферного давления и направления ветра, но я хотел показать лишь примерные цифры.
Полный код тестового скрипта можно скачать здесь. Исходники и бинарники самих расширений можно скачать здесь (win) и здесь (nix).
Кол-во итераций | PHP code / Win32 | PHP code / Debian | PHP extension / Win32 | PHP extension / Debian | Win32 выигрыш | Debian выигрыш | |
1. Строка 100 символов | 1000000 | 84.7566 сек | 72.5617 сек | 8.4750 сек | 4.4175 сек | в 10 раз | в 16.43 раз |
2. Строка 5000 символов | 10000 | 39.1012 сек | 31.7541 сек | 0.5001 сек | 0.134 сек | в 78.19 раз | в 236.98 раз |
3. Строка 69000 символов | 1000 | 52.3378 сек | 44.0647 сек | 0.4875 сек | 0.0763 сек | в 107.36 раз | в 577.51 раз |
Выводы
Если судить о производительности модуля по сравнению с интерпретируемым кодом, то мы видим, что ощутимые результаты можно получить на больших объемах данных и на малых количествах итераций. То есть, для часто использующихся, однако, не очень ресурсоемких алгоритмов не имеет смысла вынесение их в компилируемый код. Но для алгоритмов, работающих с большими объемами данных, это может иметь практический смысл. Также, опираясь на мои измерения, можно заметить, что результаты работы PHP-кода сравнимы на разных системах (напомню, что это были две разные машины), а вот результаты работы расширения очень сильно отличаются. Из этого лично я делаю вывод, что существуют какие-то особенности компиляции, которые мне не известны. Впрочем, я сильно сомневаюсь, что кто-то использует Windows-сервера для PHP-проектов. Хотя я также очень сомневаюсь, что кто-то прямо сейчас побежит переписывать что-то на С, эта статья все-таки больше just for fun, чем руководство к действию. Просто я хотел показать, что написать PHP extension очень просто, и иногда может быть очень полезно.
UPD1. Сравнение с count_chars
В комментах задали интересный вопрос: что если сравнить с производительностью функции count_chars?
Я увеличил количество итераций в сто раз, и прогнал тот же самый тест, но уже с использованием этой функции. Можно увидеть, что на Debian результаты почти сравнялись, а под Windows наблюдается интересная ситуация: чем больше объем данных, тем больше мой модуль сливает в производительности. Напомню, что идея теста была не в том, чтобы написать велосипед, а в том, чтобы взять алгоритм для работы с большими объемами данных.
Кол-во итераций | count_chars / Win32 | count_chars / Debian | extension / Win32 | extension / Debian | Win32 выигрыш | Debian выигрыш | |
1. Строка 100 символов | 10000000 | 67.5245 сек | 47.8104 сек | 81.8185 сек | 43.8091 сек | в 0.83 раз | в 1.09 раз |
2. Строка 5000 символов | 1000000 | 22.4693 сек | 12.8959 сек | 47.2514 сек | 12.9577 сек | в 0.48 раз | в 0.99 раз |
3. Строка 69000 символов | 100000 | 15.0681 сек | 7.661 сек | 46.9598 сек | 7.7387 сек | в 0.32 раз | в 0.99 раз |
Материалы
- PHP at the Core: A Hacker’s Guide to the Zend Engine, php.net
- Compiling shared PECL extensions with phpize, php.net
- Creating a PHP Extension for Windows using Microsoft Visual C++ 2008, talkphp.com
- Extension Writing Part I: Introduction to PHP and Zend, devzone.zend.com
- Extension Writing Part II: Parameters, Arrays, and ZVALs, devzone.zend.com
- Wrapping C++ Classes in a PHP Extension, devzone.zend.com
Два типа расширений PHP. Zend extension VS PHP module
PHP module – оно же обычное расширение PHP
К этому типу относится подавляющее число расширений в PHP. Все то, что подключается в php.ini с помощью инструкции extension=some_library.so — это они и есть.
Zend extension
Расширений такого типа крайне мало, однако они ничуть не менее востребованы.
В статье я обзорно, совсем по верхам, расскажу, чем же эти два типа расширений отличаются.
С точки зрения конечного пользователя.
Отличаются только способом подключения.
Обычные расширения подключаются через php.ini с помощью инструкции:
extension=some_extension.so
Расширения zend с помощью:
zend_extension=some_extension.so .
Если хочется подключить через аргумент командной строки, то, для обычных:
php -d extension=/path/extension.so
А для расширений zend:
php -z /path/zend_extension.so
Однако под капотом они очень разные.
Тут очень подходит аналогия с бензиновым и дизельным двигателем. Для пользователя вся разница заключается только в типе топлива, которое он заливает в бак, но по факту это две совершенно разных конструкции, с разными принципами работы и со своими плюсами и минусами.
С точки зрения решаемых задач
Стандартные расширения, в подавляющем числе случаев, используются для расширения функциональных возможностей языка, таких как добавления новых классов, функций, констант и т.д. Крайне редко используются для решения других задач. Например, PECL расширение Vulcan Logic Disassembler(vld) позволяет посмотреть сгенерированный opcode для скрипта.
Расширения zend используются в случаях, когда нужно максимально глубоко залезть внутрь виртуальной машины. Например для отладки или профилирования скрипта, либо для изменения логики работы PHP.
С точки зрения разработчика, который раньше не писал расширений для PHP и вдруг сподобился
Написание обычных расширений хорошо документировано и описано во множестве статей. Для них даже есть инструмент генерации скелета проекта, включенный в исходные коды PHP.
В случае с Zend extension ничего этого нет. Хороших статей практически нет. Плохих тоже. Будьте готовы к длительному и вдумчивому изучению исходных кодов как самого PHP, так и немногих существующих расширений данного типа.
С точки зрения жизненного цикла
К сожалению, тут не обойтись без кода на С, поскольку жизненный цикл расширения целиком и полностью является отражением определяющей его структуры. (Структуры привожу в сокращенном виде. Только то, что необходимо в рамках статьи)
Стандартное расширение задается структурой _zend_module_entry (описывается в zend_module.h )
struct _zend_module_entry < /* skipped */ int (*module_startup_func)(INIT_FUNC_ARGS); /* MINIT() */ int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); /* MSHUTDOWN() */ int (*request_startup_func)(INIT_FUNC_ARGS); /* RINIT() */ int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); /* RSHUTDOWN() */ void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); /* PHPINFO() */ /* skipped */ void (*globals_ctor)(void *global); /* GINIT() */ void (*globals_dtor)(void *global); /* GSHUTDOWN */ int (*post_deactivate_func)(void); /* PRSHUTDOWN() */ /* skipped */ >;
Расширение Zend задается структурой _zend_extension (описывается в zend_extensions.h )
А вот теперь уже можно показывать картинку с жизненным циклом.
Бонус. Гибридные расширения
Да. Такая возможность есть.
Зачем оно может понадобиться?
- Вам нужен полный контроль, предоставляемый расширением zend и, помимо этого, хочется зарегистрировать новые функции.
- Вам, зачем-то, понадобилось использовать вообще все возможные хуки.
- Вам необходимо управлять порядком загрузки своего расширения. К примеру надо загрузиться не раньше загрузки OPCache .