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


Краткое содержание первой части


В первой части я сделал болванку расширения, заставил ее правильно работать в IDE Clion, написал функцию-аналог my_array_fill() и проверил ее работоспособность в php.

Что теперь?


Теперь я запилю код библиотеки libtrie в наше расширение.

Немного расскажу как можно заставить работать старые php5 расширения в php7.
Дальше я сделаю несколько основных функций из этой библиотеки в php и проверю, что получилось.

Поехали


Получаем код libtrie в наше расширение


Перехожу в каталог расширения

cd ~/Documents/libtrie/

Клонирую репозиторий libtrie

git clone https://github.com/legale/libtrie



Открываю файл с кодом расширения php_libtrie.c и файл с кодом библиотеки libtrie/src/libtrie.c.



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

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

#include "libtrie/src/libtrie.h"

Функция yatrie_new


Делаю первую функцию, которая создаст префиксное дерево. В библиотеке она называется

trie_s *yatrie_new(uint32_t max_nodes, uint32_t max_refs, uint32_t max_deallocated_size) {...}

Как видно из кода, функция принимает на входе 3 числовых аргумента и возвращает указатель на структуру trie_s. Проще говоря, возвращает ссылку на созданное префиксное дерево.

Для того чтобы вытащить в PHP наше префиксное дерево в PHP предусмотрен специальный тип данных resource. Когда в PHP выполняется функция

fopen("filename.ext");

Говоря на языке Си, программа просит операционную систему открыть заданный файл, создает указатель на этот файл, который в виде ресурса и возвращается наружу в PHP.

Тоже самое мы будем делать и с нашим деревом.

Сделаем функцию в php_libtrie.c:

Код функции
PHP_FUNCTION (yatrie_new) {
    /* Это указатель на дерево */
    trie_s *trie;
    //это переменные для аргументов функции
    zend_long max_nodes; //максимальное кол-во узлов доступное в нашем дереве
    zend_long max_refs; /* максимальное кол-во ссылок в дереве. 
 * Потребность зависит от плотности записи слов в дерево. Кол-во узлов +25% должно хватать на любое дерево.
 * Например, словарь русского языка OpenCorpora ~3млн. слов укладывается в 5млн. узлов и 5млн. ссылок */
    zend_long max_deallocated_size; /* максимальный размер освобождаемых участков в блоке ссылок
 * Зависит от плотности записи. Всего у нас 96 бит в маске узла, 1 бит зарезервирован, остается 95. 
 * Значит для любого узла участок памяти в блоке ссылок не может быть больше 95, что значит, 
 * что макс. размер освобожденного участка ссылок не может быть больше 94. */

    //получаем аргументы из PHP
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "lll", &max_nodes, &max_refs, &max_deallocated_size) == FAILURE) {
        RETURN_FALSE;
    }

    //создаем дерево и записываем его адрес в памяти в созданный для этого указатель
    trie = yatrie_new((uint32_t)max_nodes, (uint32_t)max_refs, (uint32_t)max_deallocated_size);

    //Если не удалось - завершаем работу
    if (!trie) {
        RETURN_NULL();
    }
    //тут выполняется 2 действия
    /* функция zend_register_resource() регистрирует ресурс в недрах Zend,
     * пишет номер этого ресурса в глобальную переменную le_libtrie, а макрос ZVAL_RES()
     * сохраняет созданный ресурс в zval return_value */  
    ZVAL_RES(return_value, zend_register_resource(trie, le_libtrie));
}


Теперь нужно добавить созданную функцию в массив функций расширения, иначе функция не будет видна из PHP.

PHP_FE(yatrie_new, NULL)



Чтобы было совсем красиво, я добавлю декларацию функции в заголовочный файл. Этого делать не обязательно, поскольку наши PHP функции не взаимодействуют друг с другом, но я тем не менее предпочитаю декларировать все функции в заголовочном файле.

Просто добавляем строки:

PHP_FUNCTION(confirm_libtrie_compiled);
PHP_FUNCTION(my_array_fill);
PHP_FUNCTION(yatrie_new);
в файл php_libtrie.h. Куда нибудь между:
#ifndef PHP_LIBTRIE_H
#define PHP_LIBTRIE_H
и
#endif	/* PHP_LIBTRIE_H */



деструктор созданного ресурса PHP


Созданная функция yatrie_new() создает дерево, а также регистрирует ресурс PHP. Теперь нужна функция, которая будет закрывать созданный ресурс и освобождать память, занятую префиксным деревом.

/**
 * @brief деструктор ресурса, он принимает на входе указатель на ресурс и закрывает его
 * @param rsrc  : zend_resource * указатель
 * @return void
 */
static void php_libtrie_dtor(zend_resource *rsrc TSRMLS_DC) {
    //тут мы берем указатель на trie из ресурса
    trie_s *trie = (trie_s *) rsrc->ptr;
    //тут выполняется функция библиотеки, которая освобождает память,
    // выделенную для trie
    yatrie_free(trie);
}

Поскольку функция внутренняя в массив функций расширения она не включается. Добавлю ее декларацию в php_libtrie.h:



Теперь надо зарегистрировать созданную функцию-деструктор в PHP. Это делается через специальную функцию инициализации расширения. До этого эта функция просто сразу возвращала SUCCESS. Надо добавить туда регистрацию деструктора.

//Регистрируем в PHP функцию деструктор нашего ресурса trie
PHP_MINIT_FUNCTION (libtrie) {
    le_libtrie = zend_register_list_destructors_ex(
            php_libtrie_dtor,
            NULL, PHP_LIBTRIE_RES_NAME, module_number);
    return SUCCESS;
}



Функция удаления созданного дерева


Как у функции fopen() есть пара fclose(), так и моей функции создания дерева должна быть подруга, которая ее уравновесит.

Код
/**
 * @brief Удаляет дерево из памяти
 * @param trie  : resource
 * @return true/false : bool
 */
PHP_FUNCTION (yatrie_free) {
    zval *resource; //указатель для zval структуры с ресурсом

    //получаем аргумент типа ресурс
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &resource) == FAILURE) {
        RETURN_FALSE;
    }

    /* тут вызывается закрытие ресурса, на входе принимается zend_resource,
     * который сначала надо достать из zval. Это и делает макрос Z_RES_P()
     */
    if (zend_list_close(Z_RES_P(resource)) == SUCCESS) {
        //макрос пишет true в return_vale и делает return
        RETURN_TRUE;
    }
    //макрос пишет false в return_vale и делает return
    RETURN_FALSE;
}


Добавляю функцию в массив функций расширения:

PHP_FE(yatrie_free, NULL)

Добавляю декларацию функции в заголовочный файл:

PHP_FUNCTION(yatrie_free);



Как видно на скриншоте, я добавил в заголовочный файл внутреннее название ресурса в PHP, а также макросы PHP5, которые почему-то удалили из PHP7. Я их не использую, но если кто-то захочет с ними можно без труда собрать расширение PHP5 на PHP7.

#define PHP_LIBTRIE_VERSION "0.1.0" /* Replace with version number for your extension */
#define PHP_LIBTRIE_RES_NAME "libtrie data structure" /* PHP resource name */

//previously (php5) used MACROS
#define ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id, default_id, resource_type_name, resource_type)                (rsrc = (rsrc_type) zend_fetch_resource(Z_RES_P(*passed_id), resource_type_name, resource_type))
#define ZEND_REGISTER_RESOURCE(return_value, result, le_result)  ZVAL_RES(return_value,zend_register_resource(result, le_result))

Функция добавление слова в trie


Теперь сделаем функцию добавления слова в префиксное дерево.


  1. Код
    
    /**
     * @brief Добавляет в trie слово и возвращает node_id последней буквы добавленного слова
     * @param trie  : resource
     * @param word  : string
     * @return node_id : int
     */
    PHP_FUNCTION (yatrie_add) {
        trie_s *trie; //указатель для дерева
        zval *resource; //указатель для zval структуры с ресурсом
        unsigned char *word = NULL; //указатель для строки добавляемого слова
        size_t word_len; //длина слова word
        uint32_t node_id; //id последнего узла, добавленного слова
    
        //получаем аргументы
        if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs", &resource, &word, &word_len) == FAILURE) {
            RETURN_FALSE
        }
    
        /*получаем ресурс из недр PHP, функция принимает:
         * 1 аргументом сам ресурс PHP (не zval, а именно ресурс),
         * 2 аргументом имя ресурса
         * я установил его через константу в заголовочном файле
         * 3 аргументом числовой id ресурса, который был присвоен при регистрации ресурса
         * Функция возвращает указатель типа void *, поэтому надо его привести к правильному типу trie_s
         *
         * В PHP5 в этом месте использовался макрос ZEND_FETCH_RESOURCE(), который почему-то решили убрать в PHP7.
         */
        trie = (trie_s *) zend_fetch_resource(Z_RES_P(resource), PHP_LIBTRIE_RES_NAME, le_libtrie);
    
    
        /* добавим слово в trie
         * первый аргумент - указатель на строку добавляемого слова
         * второй аргумент - id узла с которого начать добавление, мы добавляем с корневого узла
         * третий аргумент - указатель на наше дерево.
         */
        node_id = yatrie_add(word, 0, trie);
    
        //возвращаем числовое значение
        RETURN_LONG(node_id);
    }
    


  2. Добавляю запись в массив функций:

    PHP_FE(yatrie_add, NULL)
    
  3. Добавляю декларацию в заголовочный файл:

    PHP_FUNCTION(yatrie_add)
    

Функция вывода всех слов из словаря


Теперь сделаем функцию, которая будет выбирать все слова из префиксного дерева и выводить их в PHP виде массива.


  1. Код
    /**
     * @brief обходит все ветви дерева, начиная с заданного узла, и выводит в массив все
     *         слова, встреченные на своем пути
     * @param trie                  : resource
     * @param node_id               : int
     * @param head (необязательный) : string строка, которая будет
     *                                добавлена в начало каждого найденного слова
     * @return array
     */
    PHP_FUNCTION (node_traverse) {
        trie_s *trie; //указатель для trie
        words_s *words; //структура для сохранения слов из trie
        zval * resource; //указатель для zval ресурса
        zend_long node_id; //начальный узел
        unsigned char *head = NULL; //указатель на строку префикс
        size_t head_len; //длина префикса
    
        //получаем аргументы из PHP
        if (zend_parse_parameters(ZEND_NUM_ARGS(), "rl|s", &resource, &node_id, &head, &head_len) == FAILURE) {
            RETURN_NULL(); //возвращаем null в случае неудачи
        }
    
        //получим наше дерево из ресурса
        trie = (trie_s *) zend_fetch_resource(Z_RES_P(resource), PHP_LIBTRIE_RES_NAME, le_libtrie);
    
        //для сохранения слов из trie функция node_traverse() использует специальную структура words_s
        //выделим память под нее
        words = (words_s *) calloc(1, sizeof(words_s));
        words->counter = 0; //установим счетчик слов на 0
    
        //нужна еще 1 структура для передачи префикса
        string_s *head_libtrie = calloc(1, sizeof(string_s));
    
        //если head задан
        if(head != NULL) {
            head_libtrie->length = (uint32_t)head_len; //присвоим длину
            memcpy(&head_libtrie->letters, head, head_len); //скопируем строку в head_libtrie
        }
        //теперь получим слова из trie
        node_traverse(words, (uint32_t) node_id, head_libtrie, trie);
        //теперь создадим PHP массив, размер возьмем из счетчика слов в words
        array_init_size(return_value, words->counter);
    
        //добавим слова в массив php
        while (words->counter--) {
            //поскольку в trie буквы хранятся в виде кодов, нужно декодировать их
            //это массив для декодированного слова
            uint8_t dst[256];
            //функция из библиотеки libtrie
            decode_string(dst, words->words[words->counter]);
    
            //эта функция Zend API, которая добавляет в массив элемент с типом php string из типа Си char *
            add_next_index_string(return_value, (const char *) dst);
        }
        //теперь надо освободить память выделенную под words и head_libtrie
        free(words);
        free(head_libtrie);
    }
    

  2. Добавляю запись в массив функций:

    PHP_FE(node_traverse, NULL)
    

  3. Добавляю декларацию в заголовочный файл:

    PHP_FUNCTION(node_traverse)
    


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


Поскольку в расширении теперь используются файлы сторонней библиотеки, эти файлы надо тоже скомпилировать. Открываю файл config.m4 и добавляю туда 2 исходных файла libtrie:

libtrie/src/libtrie.c
libtrie/src/single_list.c


Вот полное содержимое файла после изменений.

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(libtrie/src/)
  # ключевая строка
  PHP_NEW_EXTENSION(libtrie,   libtrie/src/libtrie.c   libtrie/src/single_list.c   php_libtrie.c   , $ext_shared)
  # PHP_NEW_EXTENSION(libtrie, php_libtrie.c libtrie/src/libtrie.c libtrie/src/single_list.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi




Теперь нужно заново сделать ./configure скрипт. Запускаю из корневого каталога расширения:

phpize && ./configure

Теперь собираю расширение:

make

Тестируем


Для теста лучше всего сделать php скрипт, чтобы много не писать в консоли. Я сделаю так:

nano yatrie_test.php

А это содержимое файла:

<?php
echo "Cоздаем дерево на 500 узлов и 500 ссылок\n\n";
$trie = yatrie_new(500, 500, 100);
echo "Готово!\n Добавляем слова, сохряняя id узлов в массив \$nodes\n";
$nodes[] = yatrie_add($trie, "ух");
$nodes[] = yatrie_add($trie, "ухо");
$nodes[] = yatrie_add($trie, "уха");
echo "Тут хорошо видно как работает дерево.\n
Первое слово из 2 букв, поэтому последний узел 2.\n
Второе слово из 3 букв, но 2 буквы совпадают с первым словом,\n
поэтому только 1 узел добавлен\n";
print_r($nodes);
print_r(node_traverse($trie, 0));
yatrie_free($trie);

Выполняем в консоли:

php -d extension=modules/libtrie.so yatrie_test.php

Вот что должно получиться:



Полученный исходный код расширения берем отсюда. Не стесняемся и ставим звездочки :-)




К сожалению, не доступен сервер mySQL