Пишем расширение под PHP (7.0.7) без знаний о С-C++ и как это вообще работает

в 16:06, , рубрики: php

Можно ли написать свой модуль (расширение) к 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js