Как сделать расширение на PHP7 сложнее, чем «hello, world», и не стать красноглазиком. Часть 1

  • Tutorial

Зачем?


Я пишу эту статью для того, чтобы путь, который у меня занял в общей сложности не меньше года, читатель смог пройти за пару часов. Как показал мой личный опыт, просто программировать на Си несколько легче, чем заставить работать серьезное расширение для PHP. Здесь я максимально подробно расскажу вам о том, как сделать расширение на примере библиотеки libtrie, реализующей префиксное дерево, более известное как trie. Я буду писать и параллельно выполнять описываемые действия на свежеустановленной системе Lubuntu 18.04.

Начнем.

Установка ПО


PHP


  1. Сначала ставим пакет php7.2-dev, в нем нужный для сборки расширения скрипт phpize. Кроме того, нам понадобится рабочая версия php, на котором мы будем проверять наше расширение. Установка этого пакета подтянет некоторое количество зависимых пакетов, ставим все, что предлагается.

    sudo apt install php7.2-dev

  2. Идем на сайт php.net, заходим в раздел downloads и вытаскиваем ссылку на архив с самой свежей стабильной версией php, сейчас это версия 7.2.11.
    Качаем архив исходников php:

    
    cd /tmp && wget http://it2.php.net/get/php-7.2.11.tar.gz/from/this/mirror -O php7.tar.gz
    

  3. Теперь распакуем архив себе:

    
    sudo tar -xvf php7.tar.gz -C /usr/local/src
    


Редакторы кода


Обычно я использую 2 редактора кода. Простой и быстрый geany и довольно тормозной, но очень продвинутый clion фирмы JetBrains. Geany установим из стандартной репы Убунту.


sudo apt install geany

Clion скачаем с официального сайта JetBrains:


cd ~/Downloads && wget https://download.jetbrains.com/cpp/CLion-2018.2.5.tar.gz -O clion.tar.gz


sudo tar -xvf clion.tar.gz -C /usr/share

Сделаем линк, чтобы было удобно запускать clion из консоли.


sudo ln -s /usr/share/clion-2018.2.5/bin/clion.sh /usr/bin/clion

После первого запуска clion сам создаст ярлыки для себя из меню оболочки LXpanel, но первый раз нужно запустить руками.


#запустим
clion

Создание расширения


Тут у нас есть как минимум 3 варианта:

  1. Взять сырую стандартную болванку из исходников php, которые мы скачали.
  2. Немного подпилить стандартную болванку специальным скриптом ext_skel
  3. Взять хорошую минималистскую болванку вот отсюда.

Третий вариант мне нравится больше всего, но я буду использовать второй, чтобы в случае неудачи минимизировать число мест, где я мог ошибиться. Хотя ковырять болванку разработчиков — то еще удовольствие :-)

  1. Перейдем в каталог со стандартными расширениями php.

    
    cd /usr/local/src/php-7.2.11/ext
    

    Скрипту кроме названия можно через файл proto задать некоторые параметры расширения. Все это можно не делать. Я буду все делать руками, но как работает proto покажу. Мы делаем trie, поэтому назовем наше расширение libtrie. Чтобы работать в каталоге /usr/local/src нужны привилегии администратора, чтобы без конца не писать sudo, я включу bash с повышенными правами.

    
    sudo bash
    

  2. Тут я задам параметры только 1 функции, которую будет реализовывать создаваемое расширение. Это просто демонстрационная функция, чтобы показать как это делается.

    Будем делать полный аналог стандартной функции

    array array_fill ( int $start_index , int $num , mixed $value )
    

    Синтаксис в proto файле очень простой, нужно просто указать название функции. Я напишу немного побольше, чтобы выглядело информативнее.

    
    echo my_array_fill \( int start_index , int num , mixed value \) >> libtrie.proto
    

  3. Теперь запустим скрипт ext_skel, задав ему название расширения и созданный нами proto файл.

    
    ./ext_skel --extname=libtrie --proto=./libtrie.proto
    

  4. У нас создался каталог с нашим расширением. Перейдем в него.

    
    cd libtrie
    


Структура файлов и принцип сборки


Структура файлов

config.m4  - тут хранится начальная конфигурация расшения на основании которой специальная программа phpize готовит скрипт ./configure, который создает конфигурацию для сборки расширения makefile. 

CREDITS - пустой файл, в котором пишут кто автор, кого он благодарит
libtrie.c    - тут основной код нашего расширения
php_libtrie.h - тут заголовочный файл расширения
config.w32 - тут начальная конфигурация для сборки расширения под windows
EXPERIMENTAL - пустой файл. Так и не разобрал, что в нем пишут.
libtrie.php - сгенерированный php файл для базовой проверки работоспособности расширения.
tests - тесты расширения


Для успешной сборки расширения необходимо всего 3 файла. В минималистской болванке расширения, которую я упомянул выше, есть только 3 файла.


config.m4 	
php_libtrie.h
libtrie.c

Стандартное именование принятое в php мне не нравится, я люблю, чтобы заголовочные файлы и файлы с телом программы назывались одинаково. Поэтому переименуем
libtrie.c
в
php_libtrie.c


mv libtrie.c php_libtrie.c

Редактирование config.m4


Создаваемый по умолчанию файл config.m4 буквально напичкан контентом, обилие которого сбивает с толку и запутывает. Как я сказал, этот файл нужен для формирования configure скрипта. Подробнее об этом написано здесь.


geany config.m4 &

Оставляем только это:


PHP_ARG_ENABLE(libtrie, whether to enable libtrie support,
[  --enable-libtrie           Enable libtrie support])

if test "$PHP_LIBTRIE" != "no"; then
  # если понадобится включить какие-то дополнительные заголовочные файлы
  # PHP_ADD_INCLUDE()
  # ключевая строка
  PHP_NEW_EXTENSION(libtrie, php_libtrie.c, $ext_shared)
  # PHP_NEW_EXTENSION(libtrie, php_libtrie.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi



Первый макрос создает возможность включать и отключать наше расширение при запуске создаваемого скрипта configure.

Второй блок — самый важный, он определяет какие файлы будут компилироваться в составе нашего расширения, будет ли расширение динамически подключаемым через .so файл, или расширение будет статичным и будет интегрировано при сборке php. Наше будет динамическим.
Сохраняем файл.

Скопируем файл в пользовательский каталог, чтобы не работать в режиме root.


#выход из рутового баша
exit

Копируем:


cp /usr/local/src/php-7.2.11/ext/libtrie ~/Documents/ -r

Демонстрационная функция


Напомню, что будем делать полный аналог array_fill(). Я буду работать через clion, но по этому руководству можно сделать в любом редакторе. Clion хорош тем, что позволяет автоматически делать базовую проверку синтаксиса, а также поддерживает быстрый переход по файлам или функциям через ctrl + click. Чтобы такие переходы работали в нашем расширении, придется настроить файл CMakeLists.txt

Для правильной работы clion потребуется установка системы сборки cmake, вместе с которой установится еще куча зависимых пакетов. Установим все это командой:


sudo apt install cmake

Настройка cmake


Открываем наш каталог с раширением в clion. Создаем через контекстное меню по клику на названии корневого каталога в верхней части экрана файл CMakeLists.txt со следующим содержимым:


cmake_minimum_required(VERSION 3.12)
project(php-ext-libtrie C)

set(CMAKE_C_STANDARD 11)
# задаем переменную phproot, чтобы удобнее прописывать пути к файлам php
set(phproot /usr/local/src/php-7.2.11/)
# тут указываются каталоги, которые нужно включить в проект
# мы делаем это чтобы заставить clion понимать внутренние функции и макросы самого php
include_directories(${phproot})
include_directories(${phproot}TSRM/)
include_directories(${phproot}main/)
include_directories(${phproot}Zend/)
# без этой строки clion не сможет прочитать файл и не будет ничего индексировать
add_executable(php-ext-libtrie php_libtrie.c)



Может кто-то знает, как можно сделать этот файл короче, чтобы clion начал индексировать файлы проекта. Я короче способа не нашел. Если кто-то знает, напишите в комментариях.

Код демонстрационной функции


Открываем наш файл с телом нашего расширения php_libtrie.c и
удаляем все комментарии, чтобы они нас не путали.





Clion проверяет были ли объявлены все использованные в коде функции и макросы и вышибает ошибку, если это не так. Очевидно, что разработчики PHP не пользуются clion, а то наверняка бы что-то сделали с этим. Чтобы эти ошибки не выпадали в нашем расширении, включим недостающие заголовочные файлы к нам.

Чтобы все упорядочить, я делаю так:
все include с заголовками из php_libtrie.c файла переношу в php_libtrie.h, в первом файле остается только 1 запись:

#include "php_libtrie.h" 



В файле php_libtrie.h будут все остальные необходимые включения.



Содержимое моего заголовочного файла
#ifndef PHP_LIBTRIE_H
#define PHP_LIBTRIE_H

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdarg.h> //тут для макроса va_start()
#include <inttypes.h> //тут стандартные числовые типы

//нужные константы
#if defined(__GNUC__) && __GNUC__ >= 4
# define ZEND_API __attribute__ ((visibility("default")))
# define ZEND_DLEXPORT __attribute__ ((visibility("default")))
#else
# define ZEND_API
# define ZEND_DLEXPORT
#endif
# define SIZEOF_SIZE_T 8 //нужна для макроса ZVAL_COPY_VALUE()

#ifndef ZEND_DEBUG
#define ZEND_DEBUG 0
#endif

//тут декларации того, что используется в нашем расширении
#include "php.h"
#include "php_ini.h"
#include "zend.h"
#include "zend_types.h" //ZVAL_COPY_VALUE
#include "ext/standard/info.h"

#include "zend_API.h"
#include "zend_modules.h"
#include "zend_string.h"
#include "spprintf.h"

extern zend_module_entry libtrie_module_entry;
...


Если все сделано правильно, то проверяльщик clion покажет в правом верхнем углу желтый или зеленый квадрат, который означает, что критических ошибок нет.



Небольшое теоретическое отступление


Для нормальной работы расширения необходимо 2 вещи:

  1. Нужно инициализировать специальную структуру zend_module_entry, в которой содержится следующее:

    zend_module_entry libtrie_module_entry = {
    	STANDARD_MODULE_HEADER, //стандартный заголовок
    	"libtrie", //название расширения
    	libtrie_functions, //название массива с функциями расширения
    	PHP_MINIT(libtrie), //функция, запускаемая при включении расширения
    	PHP_MSHUTDOWN(libtrie), //функция при выключении
    	PHP_RINIT(libtrie),		/* Replace with NULL if there's nothing to do at request start */
    	PHP_RSHUTDOWN(libtrie),	/* Replace with NULL if there's nothing to do at request end */
    	PHP_MINFO(libtrie), //видимо то, что будет показывать php в phpinfo()
    	PHP_LIBTRIE_VERSION, //версия расширения, установлена в заголовочном файле
    	STANDARD_MODULE_PROPERTIES //не знаю что это
    };
    

  2. Инициализировать тот самый массив, который содержит все функции нашего расширения.

    Тут через специальный макрос-обертку PHP_FE() задаются названия всех функций в нашем расширении. Вообще в PHP очень активно используются макросы, очень много таких макросов, которые просто ссылаются на другие макросы, а те в свою очередь дальше. К этому надо привыкнуть. Можно разобраться, если переходить по макросам через ctrl + click. Тут как раз clion незаменим.

    Помните proto файл? Мы задали там 1 функцию my_array_fill(). Поэтому теперь у нас тут 3 элемента:

    const zend_function_entry libtrie_functions[] = {
    	PHP_FE(confirm_libtrie_compiled,	NULL)		/* For testing, remove later. */
    	PHP_FE(my_array_fill,	NULL)
    	PHP_FE_END	/* Must be the last line in libtrie_functions[] */
    };
    

    Первая строка — тестовая функция, которая будет выдавать, что расширение работает, вторая — наша функция, третья — специальный макрос, который должен быть последним в этом массиве.

Находим нашу функцию:

PHP_FUNCTION(my_array_fill)

Как видно она тоже инициализируется через макрос. Все дело в том, что все функции php ничего не возвращают (если быть точным возвращают void) внутри Си, а их аргументы нельзя изменить. Где-то это даже удобно.

Если посмотреть в структуре файла (часть окна слева), тут перечислены все функции файла, но уже в том виде, в котором они будут после прекомпиляции макросов. На скриншоте видно, что наша функция my_array_fill на самом деле будет zif_my_array_fill.



Аргументы из недр php в нашу Си функцию мы получаем макросом. Подробнее об этом макросе можно посмотреть в файле:


/usr/local/src/php-7.2.11/README.PARAMETER_PARSING_API

Ниже приведен код нашей функции-аналога с подробными пояснениями.

Код
PHP_FUNCTION(my_array_fill)
{
	//сначала объявляем все переменные, которые нам тут понадобятся
	//в любую функцию передаются 2 аргумента:
	//указатели zend_execute_data *execute_data, zval *return_value
	//через первый указатель функция получает аргументы, а через второй отдает данные

	//zend_long это int64_t на x64 системах и int32_t на x86 системах

	//число передается в функцию с типом zend_long
	zend_long start_index; //1 аргумент число,
	zend_long num; //2 тоже число
	zval *value; //поскольку у нас mixed тип, то берем zval, которые может хранить любой тип

	//получаем аргументы в объявленные переменные, тут сразу проходит проверка на количество и тип аргументов
	if (zend_parse_parameters(ZEND_NUM_ARGS(), "llz",
							  &start_index, &num, &value) == FAILURE) {
		/*наши функции ничего не выводят
		 * поэтому все макросы RETURN_ просто пишут в
		 * return_value результат и прерывают функцию */
		RETURN_FALSE;
	}

	//проводим проверку второго аргумента, где задается кол-во выводимых элементов массива
	if (num <= 0) {
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "argument 2 must be > 0"); //очередной макрос для вывода ошибки
		RETURN_FALSE;
	}

	//zval *return_value уже есть, поэтому сразу в нем и инициализируем массив
	//этот макрос принимает на входе указатель на zval, в котором надо сделать массив, и кол-во элементов
	//приводим тип из zend_long в unsigned int32.
	// Размер ключей массива первое значение + кол-во. Т.е. если первое 1, а надо всего 3, то массив будет из 4 элементов
	array_init_size(return_value, (uint32_t)(start_index + num));

	//добавляем через цикл, начиная с начального, заканчивая последним
	for(zend_long i = start_index, last = start_index + num; i < last; ++i) {
		//копируем указатель нашего zval со входа в каждый элемент массива
		add_index_zval(return_value, i, value);
	}
	//функция ничего не возвращает, а массив уже записан в return_value
	return;
}




Сборка и тестирование расширения


Сначала запускаем phpize, который сделает нам configure файл.


phpize

Теперь запускаем ./configure, который сделает makefile.


./configure

Наконец запускаем make, который соберет нам наше расширение.


make

Проверим, что у нас получилось.


# Это заставит наш php, подключить наше скомпилированное расширение из каталога
# modules. Ключ -a заставит php работь в режиме командной строки
php -d extension=modules/libtrie.so -a

Вводим в консоли php:

print_r(my_array_fill(50, 2, "hello, baby!"));

Наслаждаемся результатом.



Кто-то спросит, а где же тут trie? О функциях, реализующих работу trie, я напишу во второй части.

Stay tuned.
Поделиться публикацией

Похожие публикации

Комментарии 11
    0
    Я недавно познакомился с Arduino, в которой редактор с подсветкой называет себя IDE и тоже сразу вспомнил, что есть CLion. Осталось научиться писать код под контроллеры вчистую, без прослойки от Arduino.
      0
      Редактор там правда ужасный. Вроде бы видел где-то инструкцию по тому, чтобы писать под ардуино в вижуал студио. Если появится такая же под CLion, будет круто.

      По теме — спасибо. Нативные расширения и библиотеки иногда творят чудеса. Под PHP не собирал, но под node.is был опыт.
      –8
      Если твои задачи требуют написания расширения для PHP, возможно тебе стоит сменить язык (С)

        0
        Когда-то ковырял Kerio Connect — прекрасная замена Outlook с полной совместимостью с ним по протоколу — от чехов, когда вам надоела политика Маленьких Мягеньких. Сделан на открытых компонентах, и клиент и админка, с исходниками на PHP, но! самое вкусное завернуто в расширение PHP и АПИ к нему обфусцирован.
          0

          Не понял. А чем плохи расширения? С какого на какой язык стоит сменить?

            0

            Ужасное месиво из макросов?

            0
            Вы это серьёзно что ли? Одни языки постоянно вызываются из других. В Си полно ассемблерных вставок, Фортран жив только в качестве библиотек к другим языкам, Луа вообще придуман только для того, чтобы быть вызванным откуда-то ещё.

            Ну и естественно полным-полно модулей на компилируемых языках к языкам высокого уровня.
            0

            Можно ли тоже самое сделать на PHP FFi?

              0
              Ни разу ничего не делал с помощью этого расширения. Посмотрел сейчас пример на ffi на github.com/dstogov/php-ffi Кажется, что такую одноразовую функцию как array_fill() можно. Но эта статья только пролог к написанию расширения, использующему ресурсы. Сделать с ресурсом через ffi мне кажется не выйдет.
                0
                Ух ты! Оно ещё живо!
                  0
                  ffi планируют добавить в ядро php8

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

            Самое читаемое