Можно ли написать свой модуль (расширение) к PHP без особых знаний, требующих большого времени изучения теории? Если умеешь программировать на самом PHP, то написать простейший код на С не составит особого труда, тем более, что PHP позволяет генерировать каркас под разрабатываемое расширение, в рамках которого потом пишешь код. Есть еще набирающий популярность зефир на хабре для этого вопроса. Данная публикация для тех, кто решил покопаться в исходниках PHP, немного посмотреть его внутренности, преследуя цель лишь поверхностного исследования. В данный момент я тот же самый исследовать без необходимых знаний. На собеседованиях по PHP часто просят написать код подсчета факториала. Вот такую функцию мы и напишем сейчас на С, которую потом можно вызывать из кода PHP. Я буду описывать действия, которые я сам делал и при этом ничего не знаю изначально по этой части. В интернете можно найти много статей по этому вопросу, большинство из них описывает информацию с использованием zval «старого» формата, но я не думаю, что будет хуже если и я еще добавлю от себя.
В PHP есть уже готовый инструмент ./ext_skel (находится в папке ext), который генерирует будущий шаблон (каркас) для расширения. Я не буду описывать все, что им генерируется и зачем (сам особо в этом ничего еще не понимаю и не знаю), а просто распишу минимальные правки, которую решат нашу задачу. Весь процесс происходит в CentOS 7.
Создаем каркас для будущего расширения mathstat, которое будет содержать функцию factorial().
[root@localhost ext]# ./ext_skel --extname=mathstat
Смотрим, что содержится в папке mathstat.
[root@localhost mathstat]# ls
config.m4 config.w32 CREDITS EXPERIMENTAL mathstat.c mathstat.php php_mathstat.h tests
После выполнения команды создания расширения, будет выдана следующая вспомогательная информация.
To use your new extension, you will have to execute the following steps:
1. $ cd ..
2. $ vi ext/mathstat/config.m4
3. $ ./buildconf
4. $ ./configure --[with|enable]-mathstat
5. $ make
6. $ ./sapi/cli/php -f ext/mathstat/mathstat.php
7. $ vi ext/mathstat/mathstat.c
8. $ make
В PHP7 файла buildconf после генерации у меня нет (наверное это остатки ранних версий PHP), но я знаю, что сейчас компиляция расширений начинается с команды phpize. Она “создает” кучу файлов, среди которых есть необходимый ./configure. Напомню, что пользовательский вариант компиляции расширения состоит в последовательном выполнении следующих команд.
Phpize -> ./configure -> make -> make test -> make install
Если сразу сделать эту последовательность команд, то make install по не ясным причинам будет ломаться и выдавать ошибку на копирование. Если кто в курсе, отпишите, в комментариях, почему так.
[root@localhost eugene]# make install
Installing shared extensions: /usr/local/lib/php/extensions/no-debug-non-zts-20151012/
cp: cannot stat 'modules/*': No such file or directory
make: *** [install-modules] Error 1
Phpize создает файлы на основе описания config.m4. Это, как я понял, своеобразный декларативный способ описания того, каким будет расширение, будет ли оно подтягивать внешние исходники или нет и т.д… Поэтому просмотрев другие расширения PHP в исходниках, я просто решил его максимально упростить, чтобы минимизировать ошибки компиляций с чистого листа. Действую по принципу — ничего не хочу, «все галочки снимаю».
Открываем этот файл (config.m4) и оставляем только этот текст. Опция “--enable-mathstat” говорит о том, что это просто расширение без внешних исходников (библиотек) и который можно либо включить, либо выключить. (dnl означает комментирование строки)
dnl $Id$
PHP_ARG_ENABLE(mathstat, whether to enable mathstat support,
[ --enable-mathstat Enable mathstat support])
if test "$PHP_MATHSTAT" != "no"; then
PHP_NEW_EXTENSION(mathstat, mathstat.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi
Перезапускаем команду phpize.
[root@localhost mathstat]# phpize
Configuring for:
PHP Api Version: 20151012
Zend Module Api No: 20151012
Zend Extension Api No: 320151012
[root@localhost mathstat]# ls
acinclude.m4 config.guess configure EXPERIMENTAL mathstat.c php_mathstat.h
aclocal.m4 config.h.in configure.in install-sh mathstat.php run-tests.php
autom4te.cache config.m4 config.w32 ltmain.sh missing tests
build config.sub CREDITS Makefile.global mkinstalldirs
Далее, делаем знакомые команды:
./configure && make
make test — запустит один изначально созданный тест. Про эти тесты PHP я как то писал уже вкратце.
[root@localhost mathstat]# make install
Installing shared extensions: /usr/local/lib/php/extensions/no-debug-non-zts-20151012/
В этот раз “make install” проходит, далее пробуем прописывать расширение в php.ini.
Определяем, где находится php.ini.
[root@localhost mathstat]# php --ini
Configuration File (php.ini) Path: /usr/local/lib
Loaded Configuration File: /usr/local/lib/php.ini
Scan for additional .ini files in: (none)
Additional .ini files parsed: (none)
viim /usr/local/lib/php.ini
extension=mathstat.so
;zend_extension = /usr/local/lib/php/extensions/no-debug-non-zts-20151012/xdebug.so
[root@localhost mathstat]# systemctl restart php-fpm
[root@localhost mathstat]# php -m | grep -i math
mathstat
Команда php -m (просматривает все установленные модули) говорит, что вроде бы все нормально, расширение mathstat подгрузилось.
Запускаем в текущей директории тестовый файл mathstat.php
[root@localhost mathstat]# php mathstat.php
Functions available in the test extension:
confirm_mathstat_compiled
Congratulations! You have successfully modified ext/mathstat/config.m4. Module mathstat is now compiled into PHP.
[root@localhost mathstat]#
Отлично, что — то уже работает.
2. Начинаем реализовывать функцию factorial().
Редактируем файл mathstat.c для добавления функции factorial().
Для этого нужно добавить функцию в “список” mathstat и сделать на неё заглушку, через макрос. Делаю все по аналогии как в других расширениях.
const zend_function_entry mathstat_functions[] = {
PHP_FE(confirm_mathstat_compiled, NULL) /* For testing, remove later. */
PHP_FE(factorial, NULL)
PHP_FE_END /* Must be the last line in mathstat_functions[] */
};
Реализация функции заглушки. Делается в обертке макроса. Как он работает в итоге, пока не ясно, оставляю изучение себе на будущее. Просто делаю в аналогичном формате.
PHP_FUNCTION(factorial)
{
RETURN_LONG(1000);
}
В данной случае под каждый тип возвращаемых данных, свой вариант RETURN_. Поиск в интернете покажет все возможные варианты. У нас просто целое значение. Тут вроде все просто.
Далее повторяем make clean && make && make install
[root@localhost mathstat]# make clean
find . -name *.gcno -o -name *.gcda | xargs rm -f
find . -name *.lo -o -name *.o | xargs rm -f
find . -name *.la -o -name *.a | xargs rm -f
find . -name *.so | xargs rm -f
find . -name .libs -a -type d|xargs rm -rf
rm -f libphp.la modules/* libs/*
Build complete.
Don't forget to run 'make test'.
[root@localhost mathstat]# make install
Installing shared extensions: /usr/local/lib/php/extensions/no-debug-non-zts-20151012/
[root@localhost mathstat]# systemctl restart php-fpm
[root@localhost mathstat]# systemctl status php-fpm
● php-fpm.service - The PHP FastCGI Process Manager
Loaded: loaded (/usr/lib/systemd/system/php-fpm.service; enabled; vendor preset: disabled)
Active: active (running) since Thu 2016-06-16 01:12:22 EDT; 5s ago
Main PID: 32625 (php-fpm)
CGroup: /system.slice/php-fpm.service
├─32625 php-fpm: master process (/usr/local/etc/php-fpm.conf)
├─32626 php-fpm: pool www
└─32627 php-fpm: pool www
Jun 16 01:12:22 localhost.localdomain systemd[1]: Started The PHP FastCGI Process Manager.
Jun 16 01:12:22 localhost.localdomain systemd[1]: Starting The PHP FastCGI Process Manager...
Перезапуск php-fpm не показал, что что-то сломали и поэтому идем дальше и тестим наличие функции в расширении. Делаю на всякий случай, даже если компиляция прошла.
[root@localhost mathstat]# php mathstat.php
Functions available in the test extension:
confirm_mathstat_compiled
factorial
Congratulations! You have successfully modified ext/mathstat/config.m4. Module mathstat is now compiled into PHP.
Наименование функции появилось и более того, теперь мы можем её уже вызывать из кода PHP.
[root@localhost mathstat]# php -a
Interactive mode enabled
php > echo factorial(1);
1000
php >
Видно, что функция вызвалась и вернула заранее указанное значение 1000.
Научим функцию принимать аргумент и его же отдавать, для этого необходимо сделать описание аргумента функции. Смотрим аналогии в других расширениях PHP (я смотрел bcmath). Куча макросов, но формат понятен, в принципе.
ZEND_BEGIN_ARG_INFO(arginfo_factorial, 0)
ZEND_ARG_INFO(0, number)
ZEND_END_ARG_INFO()
И добавляем его использование в функции. Если оставлять NULL, то умолчанию считается, что тип аргумента типа int.
/* {{{ mathstat_functions[]
*
* Every user visible function must have an entry in mathstat_functions[].
*/
const zend_function_entry mathstat_functions[] = {
PHP_FE(confirm_mathstat_compiled, NULL) /* For testing, remove later. */
PHP_FE(factorial, arginfo_factorial)
PHP_FE_END /* Must be the last line in mathstat_functions[] */
};
Немного исправляем тело функции.
PHP_FUNCTION(factorial)
{
int argc = ZEND_NUM_ARGS();
long number = 0;
if (zend_parse_parameters(argc, "l", &number) == FAILURE) {
RETURN_LONG(0);
}
RETURN_LONG(number);
}
Здесь используется zend_parse_parameters, который проверяет переданные аргументы на тип используя формат в кавычках (""), затем по адресу задает принятое значение. Детали можно легко найти в интернете. Для задачи реализации факториала больших знаний пока не нужно.
Проверяем после перекомпиляции (make clean && make && make install).
[root@localhost mathstat]# php -r "echo factorial('80');";
80[root@localhost mathstat]# php -r "echo factorial(80);";
80[root@localhost mathstat]#
Если передадим строку в аргументе, получим ошибку. Пока не ясно, как на самом деле все это работает до конца, но требуемая задача сделана.
[root@localhost mathstat]# php -r "echo factorial('aaaa');";
PHP Warning: factorial() expects parameter 1 to be integer, string given in Command line code on line 1
PHP Stack trace:
PHP 1. {main}() Command line code:0
PHP 2. factorial() Command line code:1
Warning: factorial() expects parameter 1 to be integer, string given in Command line code on line 1
Call Stack:
0.2040 349464 1. {main}() Command line code:0
0.2040 349464 2. factorial() Command line code:1
Так как тело функции вроде бы отрабатывает, реализуем теперь сам алгоритм расчета факториала. Как Вы знаете, алгоритм основан на рекурсивном вызове, сделаем тоже самое. Прописываем тело функции calculate() в этом же файле mathstat.c с последующим его вызовом.
static long calculate(long number)
{
if(number == 0) {
return 1;
} else {
return number * calculate(number - 1);
}
}
PHP_FUNCTION(factorial)
{
int argc = ZEND_NUM_ARGS();
long number = 0;
if (zend_parse_parameters(argc, "l", &number) == FAILURE) {
RETURN_LONG(0);
}
number = calculate(number);
RETURN_LONG(number);
}
Компилируем, перезапускаем, проверяем.
[root@localhost mathstat]# php -a
Interactive mode enabled
php > echo factorial(1);
1
php > echo factorial(2);
2
php > echo factorial(3);
6
php > echo factorial(4);
24
php > echo factorial(5);
120
Удивительно, но это работает. Получается, чтобы реализовать данную функцию без базовых знаний как там все устроенно в PHP, да и сам язык С/C++ не смотрелся с университета, мне понадобилось не более 3-4 часов. Весь процесс написания кода напоминает работу в каком то фреймворке для PHP. Все что нужно, это изучить архитектуру фреймворка и его API, а дальше работать в рамках его каркаса, тоже самое и здесь.
Автор: bizzonaru