Як зробити розширення на PHP7 складніше, ніж «hello, world», і не стати красноглазиком.

Я пишу цю статтю для того, щоб шлях, який у мене зайняв в загальній складності не менше року, читач зміг пройти за пару годин. Як показав мій особистий досвід, просто програмувати на Сі дещо легше, ніж змусити працювати серйозне розширення для 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 почав індексувати файли проекту. Я коротше способу не знайшов. Якщо хтось знає, напишіть в коментарях.

Читайте також  Клац, клац: історія компанії Cherry, яка прославилася перемикачами для клавіатур

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

Відкриваємо наш файл з тілом нашого розширення 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 request start at */
     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.

Читайте також  Побудова орбіт небесних тіл засобами Python

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *