Создать расширение язык программирования

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

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

Установка ПО

PHP

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

sudo apt install php7.2-dev
 cd /tmp && wget http://it2.php.net/get/php-7.2.11.tar.gz/from/this/mirror -O php7.tar.gz 
 sudo tar -xvf php7.tar.gz -C /usr/local/src 

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

Обычно я использую 2 редактора кода. Простой и быстрый geany и довольно тормозной, но очень продвинутый clion фирмы JetBrains. 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, но первый раз нужно запустить руками.

Читайте также:  Общая характеристика языков программирования кратко

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

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

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

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

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

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

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

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

 echo my_array_fill \( int start_index , int num , mixed value \) >> libtrie.proto 
 ./ext_skel --extname=libtrie --proto=./libtrie.proto 

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

 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

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

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

 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, вместе с которой установится еще куча зависимых пакетов. Установим все это командой:

Настройка 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($) include_directories($TSRM/) include_directories($main/) include_directories($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 запись:

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

#ifndef PHP_LIBTRIE_H #define PHP_LIBTRIE_H #ifdef HAVE_CONFIG_H #include "config.h" #endif #include //тут для макроса va_start() #include //тут стандартные числовые типы //нужные константы #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 вещи:

    Нужно инициализировать специальную структуру 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 //не знаю что это >; 

Тут через специальный макрос-обертку 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"); //очередной макрос для вывода ошибки 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 файл.

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

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

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

 # Это заставит наш php, подключить наше скомпилированное расширение из каталога # modules. Ключ -a заставит php работь в режиме командной строки php -d extension=modules/libtrie.so -a 
print_r(my_array_fill(50, 2, "hello, baby!")); 

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

Источник

Оцените статью