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

  • Tutorial
Можно ли написать свой модуль (расширение) к 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, а дальше работать в рамках его каркаса, тоже самое и здесь.

Особо большого кода по описанному варианту нет, но оставлю ссылку на github
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 16
  • 0

    Если у всс это заняло 3 часа, вам лучше бы посмотреть на SWIG.

  • 0
    cp: cannot stat 'modules/*': No such file or directory
    Вам действительно неясно что означает эта ошибка?..
    • 0
      Да, не ясно. Так как это директория есть и она видна. Поэтому я максимально упростил config.m4. Configure генерируется с логической ошибкой, более того, там внутри него есть используемые флаги (-qversion, -V) которые мой локальный gcc не понимает.

      gcc version 4.8.5 20150623 (Red Hat 4.8.5-4) (GCC)
      configure:2688: $? = 0
      configure:2677: cc -V >&5
      cc: error: unrecognized command line option '-V'
      cc: fatal error: no input files
      compilation terminated.
      configure:2688: $? = 4
      configure:2677: cc -qversion >&5
      cc: error: unrecognized command line option '-qversion'
      
      
      • 0

        Кроме того что эта директория должна существовать (кстати, в приведённом вами выводе ls её нет, она создаётся в цепочке ./configure -> make -> make test?), в ней ещё и должно что-нибудь лежать. Ну а эта ошибка скорее выглядит не как ошибка, а как перебор флагов для получения версии компилятора — они явно перебираются в цикле, на что недвусмысленно намекает одинаковый номер строк. Или на этой ошибке останавливается весь процесс?

    • –2
      Спасибо, весьма полезная статья.

      PHP7 рулит!
      • +2
        Что в статье особенного? Ничего нового по сравнению с другими версиями пыхи. А без C++ вы не напишите ничего, кроме факториала и пары других простых алгоритмов. Расширения пишутся для конкретных задач, у которых нет быстрых решений на PHP.
        • 0
          Статья открывает для многих его возможности. Многие пишут на данном языке, но не знают что критически важные места можно сильно оптимизировать. Не видел кто бы этим реально воспользовался, но раз существует такая возможность значит она кому то потребовалась. Спасибо. Очень интересная статья. Добавил в закладки.
      • 0

        Писал года 3-4 назад в своем личном бложике нубостатьи про разработку модулей для php, и почему-то не рискнул опубликовать их на хабре…
        Не думаю что это кому-то окажется полезным, но немного про разработку модулей под PHP <7: https://popsul.name/blog/php%20extension/
        Писал статьи на отъе… Поэтому это не реклама и не самый качественный туториал, а просто, материал который кому-то может быть поможет.

        • 0
          DNS умерли, да?(
          • 0

            Переехал на .ru :) https://popsul.ru/blog/php%20extension/

            • 0
              О, другое дело, спасибо) Кстати, пользуясь случаем, хотел спросить, чем это вызвано? Вроде бы сейчас с руками отрывают всякие такие зоны)
              • 0

                Да для меня не важно какая зона, name, ru или какой-нибудь blog. ru просто дешевле чем name :D

                • 0
                  :DDD Действительно)))

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