PHP extension. Пишем простой массив с Traversable и ArrayAccess

В этой статье предлагаю на примере простого массива рассмотреть как именно работают внутренние интерфейсы Traversable и ArrayAccess.

Сразу приведу список ресурсов, на которые далее по тексту будет множество ссылок.


И про платформу: я писал код по ubuntu, так что для других linux дистрибутивов (да и OS X) понадобится минимум изменений (поменять apt-get). Если хотите писать под Windows, то придется поискать информацию в других интернетах (все равно никто не пишет код по windows).

Собираем PHP


Для начала соберем debug-версию PHP. Можно, конечно, писать расширение и с обычной версией, но после пары debug-флагов PHP становится намного разговорчивей.

Открываем консоль, идем в директорию, в которую собираемся стянуть исходники PHP (например ~/dev/c/) и берем код php из git-репозитория.

git clone http://git.php.net/repository/php-src.git
cd php-src


Переключаемся на свежую ветку.

git checkout PHP-5.6


Устанавливаем необходимые для сборки PHP программы (скорей всего они у вас уже есть).

sudo apt-get install build-essential autoconf automake libtool


Теперь осталось установить bison. В ubuntu с 14 версии идет bison идет версии 3 и выше, которую PHP не переваривает. Нам нужна версия 2.7.

wget http://launchpadlibrarian.net/140087283/libbison-dev_2.7.1.dfsg-1_amd64.deb
wget http://launchpadlibrarian.net/140087282/bison_2.7.1.dfsg-1_amd64.deb

sudo dpkg -i libbison-dev_2.7.1.dfsg-1_amd64.deb
sudo dpkg -i bison_2.7.1.dfsg-1_amd64.deb

Так как мы будем собирать версию без расширений по умолчанию, нам libxml2 не понадобится. Иначе нужно будет установить libxml2-dev.

sudo apt-get install libxml2-dev


Configure указываем, что нам нужна debug-версия, без расширений. В параметре --prefix указываем директорию, в которую будет установлен PHP.

./buildconf
./configure --disable-all --enable-debug --prefix=$HOME/dev/bin/php
make && make install

Оке, PHP готов. Запустим свежесобранный php c флагом -v и убедимся, что собрали мы то что нужно и куда нужно (а то мало ли).

~/dev/bin/php/bin/php -v

Собираем расширение


«Скелет» расширения можно быстро сгенерировать используя ext_skel, который лежит в директории с исходниками PHP. Мы от ext_skel откажемся, потому что кроме полезного .gitignore он напихает нам сотни ненужных комментарив в файлы. А .gitignore можно взять тут.

Если все же очень хочется ext_skel, то запускать его нужно со следующими параметрами: в --extname указывается название расширения, а в --skel путь до папки skeleton.

~/dev/c/php-src/ext/ext_skel --extname=jco --skel=$HOME/dev/c/php-src/ext/skeleton/

Так или иначе, должна получиться директория со следующими файлами.

jco/
    .gitignore
    config.m4
    config.w32
    jco.c
    php_jco.h

Открываем config.m4 и пишем:

if test "$PHP_JCO" = "yes"; then
  AC_DEFINE(HAVE_JCO, 1, [Whether you have Jco])
  PHP_NEW_EXTENSION(jco, jco.c, $ext_shared)
fi


Все, дальше в config.m4 будем трогать только строчку с PHP_NEW_EXTENSION, добавляя туда новые файлы.

Теперь напишем основной заголовочный файл нашего расширения: php_jco.h. Называться он должен обязательно php_%название расширения%.h

#ifndef PHP_JCO_H
#define PHP_JCO_H 1

extern zend_module_entry jco_module_entry;
#define phpext_jco_ptr &jco_module_entry

//Если будем компилировать потоко-безопасную версию, то подключим нужный заголовчный файл.
#ifdef ZTS
#include "TSRM.h"
#endif

#endif


В этом файле мы объявляем переменную типа zend_module_entry с информацией о нашем расширении. Название переменной должно быть вида %название расширения%_module_entry.

Открываем jco.c и пишем в него следущее.

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

#include "php.h"
#include "php_jco.h"

// Определим тестовую функцию 
PHP_FUNCTION(hello_from_jco)
{
    // Второй параметр указывает, что мы хотим скопировать строку в памяти.
    RETURN_STRING("JCO ENABLED! YEY!", 1);
}

// Дадим PHP знать о нашей функции, указав ее в таблице функций модуля.
const zend_function_entry jco_functions[] = {
    PHP_FE(hello_from_jco,  NULL)
    PHP_FE_END
};

// Определим функцию, которую php будет вызывать при подключении нашего расширения
PHP_MINIT_FUNCTION(jco_init)
{
    return SUCCESS;
}


zend_module_entry jco_module_entry = {
    STANDARD_MODULE_HEADER,
    "jco", // название асширения
    jco_functions,
    PHP_MINIT(jco_init),
    NULL, // MSHUTDOWN
    NULL, // RINIT
    NULL, // RSHUTDOWN
    NULL, // MINFO
    "0.1", //версия расширения
    STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_JCO
ZEND_GET_MODULE(jco)
#endif



Основным здесь является определение переменной с информацией о нашем модуле, в которой мы указали таблицу функций, функцию запуска и прочие необходимые данные (или не указали, заменив NULL`ами). А ZEND_GET_MODULE просто создает для нашей библиотеки функцию get_module, которая возвращает переменную jco_module_entry;

Отлично, теперь мы готовы собрать наше расширение. Запустим phpize, который сделает нам конфиги для сборщика конфигов для сборщика расширения (we need to go deeper!)

~/dev/bin/php/bin/phpize


И собираем расширение. В параметре --with-php-config указываем путь до файла php-config собраной нами debug-версии PHP

./configure --with-php-config=$HOME/dev/bin/php/bin/php-config
make && make install


Если все собралось без ошибок, то запускаем php с расширением (если нет, правим и все равно запускаем).

~/dev/bin/php/bin/php -dextension=jco.so --r "hello_from_jco();"
JCO ENABLED! YEY!

Кратко про zval и функции


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

Для объявления функции следует пользоваться макросами PHP_FUNCTION, PHP_NAMED_FUNCTION или PHP_METHOD. Различаются они только именем полученной функции.

void prefix_name(int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used void ***tsrm_ls)


Где

  • ht — количество аргументов, с которыми вызвается функция
  • return_value — указатель на переменную, в которую записывается результат
  • return_value_ptr — указатель на указатель на возвращаемую переменную, (в случае если нужно вернуть результат по ссылке)
  • this_ptr — указатель на объект, если вызывается метод
  • return_value_is_used — флаг, указывающий используется ли далее возвращаемая переменная
  • tsrm_ls — Thread Safe Resourse Manager Local Storage! Указатель на переменные потока

Аргументы функций определяются с помощью макросов ZEND_ARG_INFO_*.

//имя переменной, _, возвращается ли ссылка, количество обязательных переменных
ZEND_BEGIN_ARG_INFO_EX(arginfo_construct, 0, 0, 1)
//передается ли по ссылке, имя аргумента
ZEND_ARG_INFO(0, var1)
ZEND_ARG_INFO(0, var2)
ZEND_END_ARG_INFO()

/* Макросы отдадут нам
static const zend_arg_info arginfo_construct[] = {                                                                       
    { NULL, 0, NULL, 2, 0, 0, 0, 0 },
    { "var1", sizeof("var1")-1, NULL, 0, 0, 0, 0, 0 },
    { "var2", sizeof("var2")-1, NULL, 0, 0, 0, 0, 0 },
}
*/


ZEND_BEGIN_ARG_INFO_EX, ZEND_ARG_INFO и ZEND_END_ARG_INFO в результате дадут массив структур zend_arg_info. Причем, первый элемент массива приводится к типу zend_internal_function_info. Количество и типы полей у них одинаковые, различаются только названия.

Далее функции, с помощью макосов PHP_FE, PHP_ME, PHP_ME_MAPPING, перечисляются в таблице функций модуля/класса элементами типа zend_function_entry.

typedef struct _zend_function_entry {
    const char *fname; // Имя функции доступное в PHP
    void (*handler)(INTERNAL_FUNCTION_PARAMETERS); //Указатель на функцию
    const struct _zend_arg_info *arg_info; //Указатель на массив агументов
    zend_uint num_args; // количество аргументов (в массиве, на который указывает arg_info)
    zend_uint flags; // Различные флаги
} zend_function_entry


При регистрации модуля функции заносятся в глобальную таблицу функций (function_table). При регистрации класса — в таблицу функций класса.

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

  • num_args — количество аргументов
  • tsrm_ls — описано выше
  • type_spec — строка, в которой указываются типы аргументов
  • … — далее перечисляются указатели на переменные, в которые будут записаные полученные аргументы


Про zend_parse_parameters прочитать можно тут.

Для работы со переменными PHP использует zval, который хранит само значение в zvalue_value, его тип, счетчик ссылок и флаг, указывающий что перемменая используется по ссылке.

struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    zend_uint refcount__gc;
    zend_uchar type;    /* active type */
    zend_uchar is_ref__gc;
};


Для выделения памяти под zval следует использовать макросы ALLOC_ZVAL (просто выделяет память), MAKE_STD_ZVAL (ALLOC_ZVAL + инициализация значений) и другие.

Делать это следует потому, что вместо zval ALLOC_ZVAL выделит память под _zval_gc_info, который дополнительно хранит информацию для поиска циклических ссылок.

Для удаления zval используется функция zval_ptr_dtor. В отличие от zval_dtor, zval_ptr_dtor сначала уменьшает счетчик ссылок и удаляет zval только если счетчик становится равен нулю.

Так же стоит учитывать, что zvalue_value для всех значений сложнее числа хранит указатели. И потому, если у вас два zval ссылаются на одну и ту же строку в памяти, то при удалении одного их них второй будет уже ссылаться на некорректный участок памяти.

Подробнее о zval Можно почитать в phpintenralsbook. А про циклические ссылки в руководстве по PHP.

Классы


Вернемся к нашему расширению и добавим первый класс. Создаем файл jco_darray.h и пишем туда следующее.

#ifndef PHP_JCO_DARRAY_H
#define PHP_JCO_DARRAY_H 1

extern zend_class_entry *jco_darray_ce;

void jco_darray_init(TSRMLS_D);

#endif


Здесь мы поросто объявили для класса переменную jco_darray_ce типа zend_class_entry и функцию для инициализации.

Теперь создадим файл jco_darray.с.

jco_darray.c
#include "php.h"
#include "jco_darray.h"

zend_class_entry *jco_darray_ce;

PHP_METHOD(jco_darray, sayHello)
{
    RETURN_STRING("Hello from darray!", 1);
}

const zend_function_entry jco_darray_functions[] = {
    // имя класса, имя функции, arginfo, флаги
    PHP_ME(jco_darray, sayHello, NULL, ZEND_ACC_PUBLIC)
    PHP_FE_END
};

void jco_darray_init(TSRMLS_D)
{
    zend_class_entry tmp_ce;
    INIT_CLASS_ENTRY(tmp_ce, "JCO\\DArray", jco_darray_functions);

    jco_darray_ce = zend_register_internal_class(&tmp_ce TSRMLS_CC);

    return;
}



Здесь интересна только функция jco_darray_init. Сначала мы создаем временную структуру для нашего класса tmp_ce и заполняем ее с помощью INIT_CLASS_ENTRY. Во втором параметре макроса указывается имя класса, которое доступно из PHP, включая namespace.

Используем функцию zend_register_internal_class, которая регистрирует наш класс в таблице классов (class_table).

Теперь добавим вызов функции jco_darray_init в функцию jco_init (файл jco.h).

PHP_MINIT_FUNCTION(jco_init)
{
    jco_darray_init(TSRMLS_C);
    return SUCCESS;
}


И добавим новый файл jco_darray.c в config.m4 (список файлов указывается БЕЗ запятых).

PHP_NEW_EXTENSION(jco, jco.c jco_darray.c, $ext_shared)


Так как мы изменили config.m4, нам нужно еще раз запустить phpize

~/dev/bin/php/bin/phpize --clean
~/dev/bin/php/bin/phpize


./configure --with-php-config=$HOME/dev/bin/php/bin/php-config
make && make install


Сделаем php скрипт для тестирования нашего расширения (назовем его оригинально: jco.php)

sayHello() . PHP_EOL;

?>


И запускаем скрипт с нашим расширением

~/dev/bin/php/bin/php -dextension=jco.so jco.php

D for Dynamic


С классом, который только и умеет, что говорить «Hello», далеко не уедешь. Особенно если он задумывался как массив. Время взять и этот массив написать.

Создаем дирректорию ds и добавляем туда файл darray.h, в котором объявим структуру и функции для нашего массива.

ds/drray.h
#ifndef PHP_JCO_DS_DARRAY_H
#define PHP_JCO_DS_DARRAY_H 1

#include "php.h"

typedef  struct jco_ds_darray {
    size_t count; // количество неNULLевых элементов
    size_t length; // текущий размер массива
    size_t min_length; // минимальный размер массива
    size_t capacity; // мощность - на сколько будет увеличиться размер
    void *elements; // массив елементов (zval)
} jco_ds_darray;

jco_ds_darray *jco_ds_darray_create(size_t size, size_t capacity);
void jco_ds_darray_destroy(jco_ds_darray *array);

#define jco_ds_darray_length(array) ((array)->length)
#define jco_ds_darray_min_length(array) ((array)->min_length)
#define jco_ds_darray_capacity(array) ((array)->capacity)
#define jco_ds_darray_count(array) ((array)->count)
#define jco_ds_darray_first(array) ((zval *)(array)->elements)

#endif



Теперь в файле ds/darray.c определим объявленные выше функции. Пока что это только создание и удаление структуры.

ds/darray.c
#include "ds/darray.h"
#include "php.h"

jco_ds_darray *jco_ds_darray_create(size_t size, size_t capacity) {
    jco_ds_darray *array = emalloc(sizeof(jco_ds_darray));
    if (!array) {
        return NULL;
    }

    array->count = 0;
    array->length = 0;
    array->min_length = size;
    array->capacity = capacity;
    array->elements = NULL;


    return array;
}


void jco_ds_darray_destroy(jco_ds_darray *array) {
    if (!array) {
        return;
    }

    efree(array);
}



У нас есть класс, есть массив, и нужно их как-то связать. Для этого давайте проясним как php работает с объектами.

Для хранения объектов в переменных (которые zval) используется структура zend_object_value, которая имеет следующие поля.

typedef struct _zend_object_value {
    zend_object_handle handle;
    const zend_object_handlers *handlers;
} zend_object_value;


  • handlers — структура с указателями на функции, которые вызывает php когда мы что-нибудь делаем с объектом (читаем свойство, вызываем метод и т.п.). Подробней про zend_object_handlers мы поговорим чуть позже.
  • handle — это обычный int, «id» объекта в хранилище объектов object_store. При создании объекта php помещает структуру zend_object в object_store и возвращает нам целочисленный handle. При этом, если у класса есть функция create_object, то для создания zend_object вызывается она. Подробней про все это можно прочитать в php internals book


Итак, все что нам нужно — это отдавать свою структуру, которая расширяет zend_object. Для этого напишем свою функцию create_object и функцию для освобождения выделенной под структуру памяти. Добавим их после объявления jco_darray_ce.

jco_darray.c

zend_object_handlers jco_darray_handlers;

typedef struct jco_darray {
    zend_object std;
    jco_ds_darray *array;
} jco_darray;

static void jco_darray_free_object_storage(jco_darray *intern TSRMLS_DC)
{
    zend_object_std_dtor(&intern->std TSRMLS_CC);

    if (intern->array) {
        jco_ds_darray_destroy(intern->array);
    }

    efree(intern);
}

zend_object_value jco_darray_create_object(zend_class_entry *class_type TSRMLS_DC) {
    zend_object_value retval;

    jco_darray *intern = emalloc(sizeof(jco_darray));
    memset(intern, 0, sizeof(jco_darray));

    //Инициализирует объект: указавает ссылку на класс и обнуляет прочие поля
    zend_object_std_init(&intern->std, class_type TSRMLS_CC);
    //Копируем свойства класса в объект
    object_properties_init(&intern->std, class_type);

    // добавляет объект в хранилище объектов (object_store)
    retval.handle = zend_objects_store_put(
        intern,
        (zend_objects_store_dtor_t) zend_objects_destroy_object, // стандартный деструктор объекта
        (zend_objects_free_object_storage_t) jco_darray_free_object_storage, //наша функция бля освобождения памяти объекта
        NULL // функция копирования, нам не нужна
        TSRMLS_CC
    );

    //указываем ссылку на структуру с функциями-обаботчиками для объектов
    retval.handlers = &jco_darray_handlers;

    return retval;
}



zend_objects_store_put принимает три функции:

  • dtor (zend_objects_destroy_object) — деструктор объекта, который вызывается когда счетчик ссылок на объект становится 0, либо при завершении скрипта. Деструктор так же отвечает за выполнение пользовательского кода (__destruct). При этом, в определенном случае php может опустить вызов деструктора (например, если при вызове другого __destruct было выброшено исключение или вызван exit ())
  • free_storage (zend_objects_free_object_storage) — функция, которая освобождает память выделенную под объект (zend_object). Вызывается всегда: либо когда счетчик ссылок на объект становится 0, либо при завершении скрипта.
  • clone — функция, которая вызвается при копировании объекта. По умолчанию игнорируется, и чтобы ее использовать нужно явно указать обработчиком копирования функцию zend_objects_store_clone_obj. Намного проще просто взять и использовать собственную функцию сразу. Поэтому в 99% случаев в clone просто передаем NULL.

В функцию jco_darray_init добавим следующие строки

    //Указываем собственную функцию для создания zend_object
    jco_darray_ce->create_object = jco_darray_create_object;

    //Копируем стандартные обаботчики для объектов
    memcpy(&jco_darray_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));


А где же массив? А массив мы будем создавать в конструкторе.

jco_darray.c
PHP_METHOD(jco_darray, __construct)
{
    jco_darray *intern;
    long size = 0;
    long capacity = 0;

    zend_error_handling error_handling;

    //Заменим обработчик ошибок так, чтобы при ошибке получения аргументов конструктора было выброшено исключение
    zend_replace_error_handling(EH_THROW, NULL, &error_handling TSRMLS_CC);

    //третий параметр указывает на типы переменных (l - long), последующие - указатели на переменные
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ll", &size, &capacity) == FAILURE) {
        zend_restore_error_handling(&error_handling TSRMLS_CC);
        return;
    }

    // Восстанавливаем стандартный обработчик ошибок
    zend_restore_error_handling(&error_handling TSRMLS_CC);

    if (size <= 0) {
        zend_throw_exception(NULL, "Array size must be positive", 0 TSRMLS_CC);
        return;
    }

    if (capacity < 0) {
        zend_throw_exception(NULL, "Array capacity must be positive or 0", 0 TSRMLS_CC);
        return;
    }

    // получаем объект по handle
    intern = zend_object_store_get_object(getThis() TSRMLS_CC);

    intern->array = jco_ds_darray_create((size_t)size, (size_t)capacity);
    if (!intern->array) {
        zend_throw_exception(NULL, "Failed to allocate array", 0 TSRMLS_CC);
    }

    return;
}



Добавляем __construct в таблицу функций.


ZEND_BEGIN_ARG_INFO_EX(arginfo_construct, 0, 0, 1)
//передается ли по ссылке, имя аргумента
ZEND_ARG_INFO(0, size)
ZEND_ARG_INFO(0, capacity)
ZEND_END_ARG_INFO()

const zend_function_entry jco_darray_functions[] = {
    PHP_ME(jco_darray, __construct, arginfo_construct, ZEND_ACC_PUBLIC)
    PHP_ME(jco_darray, sayHello, arginfo_void, ZEND_ACC_PUBLIC)
    PHP_FE_END
};

Время собрать extension и убедится, что все компилируется нормально. Запускаем phpize, чтобы подхватить изменения config.m4 (Обещаю, это в последний раз)

~/dev/bin/php/bin/phpize --clean
~/dev/bin/php/bin/phpize

./configure --with-php-config=$HOME/dev/bin/php/bin/php-config
make && make install


И запускаем тестовый скрипт

~/dev/bin/php/bin/php -dextension=jco.so jco.php

ArrayAccess


В файле ds/darray.h добавим объявления функций для работы с массивом: get, set, unset (и clone заодно).

jco_ds_darray *jco_ds_darray_create(size_t size, size_t capacity);
jco_ds_darray *jco_ds_darray_clone(jco_ds_darray *array);
void jco_ds_darray_destroy(jco_ds_darray *array);
zval *jco_ds_darray_get(jco_ds_darray *array, size_t index);
zval *jco_ds_darray_set(jco_ds_darray *array, size_t index, zval *value);
void jco_ds_darray_unset(jco_ds_darray *array, size_t index);


И напишем эти функции

ds/darray.с
    #include "ds/darray.h"
#include "php.h"

#define ELEM_SIZE (sizeof(zval))

// Увеличиваем память под элементы массива. 
// Если указан index, то выделяем память кратно capacity настолько,
// чтобы было место для index-го элемента
static inline int _jco_ds_darray_expand(jco_ds_darray *array, size_t index) {
    if (array && array->capacity > 0) {
        size_t capacity = array->capacity;
        size_t max_elements = array->length;
        size_t expand_count;
        if (index) {
            expand_count = ((index + 1) / capacity) * capacity + capacity;
        } else {
            expand_count = (max_elements + capacity);
        }

        zval *elements;
        if (max_elements == 0 && !array->elements) {
            elements = (zval *)emalloc(ELEM_SIZE * expand_count);
        } else {
            elements = (zval *)erealloc((void *)array->elements, ELEM_SIZE * expand_count);
        }

        if (elements) {
            zval *ptr = (elements + max_elements);
            memset(ptr, 0, array->capacity * ELEM_SIZE);

            array->length = expand_count;
            array->elements = elements;

            return 1;
        }

        return 0;
    }

    return 0;
}


jco_ds_darray *jco_ds_darray_create(size_t size, size_t capacity) {
    jco_ds_darray *array = emalloc(sizeof(jco_ds_darray));
    if (!array) {
        return NULL;
    }

    array->length = 0;
    array->min_length = size;
    array->capacity = size;
    array->count = 0;
    array->elements = NULL;

    if (size > 0 && !_jco_ds_darray_expand(array, 0)) {
        efree(array);

        return NULL;
    }

    array->length = size;
    array->capacity = capacity;

    return array;
}


void jco_ds_darray_destroy(jco_ds_darray *array) {
    if (!array) {
        return;
    }

    if (array->length > 0) {
        zval *elem = (zval *)array->elements;
        while (array->length--) {
            if (elem != NULL && Z_REFCOUNT_P(elem) > 0) {
                zval_dtor(elem);
            }
            elem++;
        }
    }

    if (array->elements) {
        efree(array->elements);
    }

    efree(array);
}

jco_ds_darray *jco_ds_darray_clone(jco_ds_darray *array) {
    if (!array) {
        return NULL;
    }

    jco_ds_darray *new_array = emalloc(sizeof(jco_ds_darray));
    if (!new_array) {
        return NULL;
    }

    new_array->count = array->count;
    new_array->length = array->length;
    new_array->min_length = array->min_length;
    new_array->capacity = array->capacity;
    new_array->elements = (zval *)emalloc(ELEM_SIZE * array->length);
    if (!new_array->elements) {
        efree(new_array);

        return NULL;
    }

    memcpy(new_array->elements, array->elements, ELEM_SIZE * array->length);
    //memcpy скопировал нам только zval`ы, но они указывают на одни те же значения
    //Чтобы это исправить нужно пройтись по элементам функцией zval_copy_ctor
    size_t index;
    for (index = 0; index < array->length; index++) {
        zval *elem = (zval *)new_array->elements + index;
        if (elem != NULL && Z_REFCOUNT_P(elem) > 0) {
            zval_copy_ctor(elem);
        }
    }



    return new_array;

}


zval *jco_ds_darray_get(jco_ds_darray *array, size_t index) {
    if (!array || array->length < (index + 1)) {
        return NULL;
    }

    zval *elem = (zval *)(array->elements) + index;
    if (!elem || Z_TYPE_P(elem) == IS_NULL) {
        return NULL;
    }

    //На всякий случай убедимся, что is_ref__gc = 0
    Z_UNSET_ISREF_P(elem);
    return elem;
}


void jco_ds_darray_unset(jco_ds_darray *array, size_t index) {
    if (!array || array->length < (index + 1)) {
        return;
    }

    zval *elem = (zval *)array->elements + index;
    if (elem != NULL && Z_REFCOUNT_P(elem) > 0) {
        if (Z_TYPE_P(elem) != IS_NULL) {
            array->count--;
        }

        zval_dtor(elem);
        *elem = (zval) {0};
    }

}


zval *jco_ds_darray_set(jco_ds_darray *array, size_t index, zval *value) {
    if (!array) {
        return;
    }

    if ((index + 1) > array->length) {
        if (array->capacity == 0) {
            return NULL;
        }

        if (!_jco_ds_darray_expand(array, index)) {
            return NULL;
        }
    }
    zval *elem = (zval *)array->elements + index;
    int prev_is_not_null = 0;
    if (Z_REFCOUNT_P(elem) > 0 && Z_TYPE_P(elem)) {
        zval_dtor(elem);
        prev_is_not_null = 1;
    }

    elem->value = value->value;
    elem->type  = value->type;
    elem->refcount__gc = 1;
    elem->is_ref__gc = 0;
    zval_copy_ctor(elem);

    if (prev_is_not_null && Z_TYPE_P(elem) == IS_NULL) {
        array->count--;
    }
    else if (!prev_is_not_null && Z_TYPE_P(elem) != IS_NULL) {
        array->count++;
    }


    return elem;
}



Как видно, в jco_ds_darray_set ALLOC_ZVAL мы не использовали, а использовали ранее выделенную память. В нашем случае нам важно, чтобы массив элементов был непрерывным в памяти. К тому же напрямую элементы массива в пользовательский код мы отдавать не будем, так что GC будет лишним. Соответсвенно и для удаления мы используем zval_dtor вместо zval_ptr_dtor.

Теперь, используя новые функции, реализуем интерфейс ArrayAccess.

jco_darray.c
    PHP_METHOD(jco_darray, count)
{
    jco_darray *intern;
    long count;

    intern = zend_object_store_get_object(getThis() TSRMLS_CC);
    count = (long)jco_ds_darray_count(intern->array);

    ZVAL_LONG(return_value, count);
}

PHP_METHOD(jco_darray, length)
{
    jco_darray *intern;
    long length;

    intern = zend_object_store_get_object(getThis() TSRMLS_CC);
    length = (long) jco_ds_darray_length(intern->array);

    ZVAL_LONG(return_value, length);
}

PHP_METHOD(jco_darray, offsetSet)
{
    jco_darray *intern;
    zval *val;
    long index;


    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "lz", &index, &val) == FAILURE) {
        zend_throw_exception(NULL, "Failed to parse arguments", 0 TSRMLS_CC);
        return;
    }

    intern = zend_object_store_get_object(getThis() TSRMLS_CC);
    jco_ds_darray_set(intern->array, (size_t)index, val);

}

PHP_METHOD(jco_darray, offsetUnset)
{
    jco_darray *intern;
    long index;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &index) == FAILURE) {
        zend_throw_exception(NULL, "Invalid index passed", 0 TSRMLS_CC);
        return;
    }


    intern = zend_object_store_get_object(getThis() TSRMLS_CC);
    jco_ds_darray_unset(intern->array, (size_t)index);
}

PHP_METHOD(jco_darray, offsetGet)
{
    jco_darray *intern;
    long index;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &index) == FAILURE) {
        zend_throw_exception(NULL, "Invalid index passed", 0 TSRMLS_CC);
        return;
    }


    intern = zend_object_store_get_object(getThis() TSRMLS_CC);
    zval *val = jco_ds_darray_get(intern->array, (size_t)index);

    if (val) {
        //назначиние, источник, вызвать zval_copy_ctor, вызвать zval_ptr_dtor
        ZVAL_ZVAL(return_value, val, 1, 0);
    } else {
        ZVAL_NULL(return_value);
    }
}

PHP_METHOD(jco_darray, offsetExists)
{
    jco_darray *intern;
    long index;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &index) == FAILURE) {
        zend_throw_exception(NULL, "Invalid index passed", 0 TSRMLS_CC);
        return;
    }


    intern = zend_object_store_get_object(getThis() TSRMLS_CC);
    zval *val = jco_ds_darray_get(intern->array, (size_t)index);
    if (val) {
        ZVAL_TRUE(return_value);
    } else {
        ZVAL_FALSE(return_value);
    }
}


Добавим функции в таблицу функций класса.

jco_darray.c
ZEND_BEGIN_ARG_INFO_EX(arginfo_jco_darray_offset, 0, 0, 1)
ZEND_ARG_INFO(0, offset)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_jco_darray_offset_value, 0, 0, 2)
ZEND_ARG_INFO(0, offset)
ZEND_ARG_INFO(0, value)
ZEND_END_ARG_INFO()

const zend_function_entry jco_darray_functions[] = {
    PHP_ME(jco_darray, __construct, arginfo_construct, ZEND_ACC_PUBLIC)
    PHP_ME(jco_darray, offsetSet, arginfo_jco_darray_offset_value, ZEND_ACC_PUBLIC)
    PHP_ME(jco_darray, offsetGet, arginfo_jco_darray_offset, ZEND_ACC_PUBLIC)
    PHP_ME(jco_darray, offsetUnset, arginfo_jco_darray_offset, ZEND_ACC_PUBLIC)
    PHP_ME(jco_darray, offsetExists, arginfo_jco_darray_offset, ZEND_ACC_PUBLIC)
    PHP_ME(jco_darray, count, arginfo_void, ZEND_ACC_PUBLIC)
    PHP_ME(jco_darray, length, arginfo_void, ZEND_ACC_PUBLIC)
    PHP_FE_END
};


Теперь, укажем php что наш класс реализует интерфейс ArrayAccess

    zend_class_implements(jco_darray_ce TSRMLS_CC, 1, zend_ce_arrayaccess);


Последние параметры в функции — это количество интерфейсов и class_entry интерфейсов через запятую.

zend_ce_arrayaccess объявлен в файле zend_interfaces.h (вместе с zend_ce_traversable, zend_ce_aggregate, zend_ce_iterator и zend_ce_serializable), который нам нужно включить в файл jco_darray.c

#include "php.h"
#include "zend_interfaces.h"
#include "jco_darray.h"
#include "ds/darray.h"


Напишем тестовый код и заодно сравним с наш класс с обычным массивом

 &$val) {
    $jar[$index] = $val * 3;
}

echo "JCO\Darray" . PHP_EOL;
echo "TIME: " . (microtime(true) - $t1) . PHP_EOL;
echo "MEMORY: " . ((memory_get_usage() - $m1)/1048576) . PHP_EOL;
gc_collect_cycles();


$t1 = microtime(true);
$m1 = memory_get_usage();
$ar = [];
foreach($data as $index => &$val) {
    $ar[$index] = $val * 3;
}

echo "AR" . PHP_EOL;
echo "TIME: " . (microtime(true) - $t1) . PHP_EOL;
echo "MEMORY: " . ((memory_get_usage() - $m1)/1048576) . PHP_EOL;
gc_collect_cycles();

?>

Cкомпилируем и запустим php c нашим расширением

    make && make install

~/dev/bin/php/bin/php -dextension=jco.so jco.php

JCO\Darray
TIME: 0.43633484840393
MEMORY: 11.44548034668
Array
TIME: 0.3345410823822
MEMORY: 137.51664733887

Эй… Наш код получился медленней чем стандартный массив php!

Object Handlers


Давайте разберемся, почему наш массив получился таким медленным. Для этого вспомним про object_handlers, который упоминался выше.

ZEND_API zend_object_handlers std_object_handlers = {
    zend_objects_store_add_ref,             /* add_ref */
    zend_objects_store_del_ref,             /* del_ref */
    zend_objects_clone_obj,                 /* clone_obj */

    zend_std_read_property,                 /* read_property */
    zend_std_write_property,                /* write_property */
    zend_std_read_dimension,                /* read_dimension */
    zend_std_write_dimension,               /* write_dimension */
    zend_std_get_property_ptr_ptr,          /* get_property_ptr_ptr */
    NULL,                                   /* get */
    NULL,                                   /* set */
    zend_std_has_property,                  /* has_property */
    zend_std_unset_property,                /* unset_property */
    zend_std_has_dimension,                 /* has_dimension */
    zend_std_unset_dimension,               /* unset_dimension */
    zend_std_get_properties,                /* get_properties */
    zend_std_get_method,                    /* get_method */
    NULL,                                   /* call_method */
    zend_std_get_constructor,               /* get_constructor */
    zend_std_object_get_class,              /* get_class_entry */
    zend_std_object_get_class_name,         /* get_class_name */
    zend_std_compare_objects,               /* compare_objects */
    zend_std_cast_object_tostring,          /* cast_object */
    NULL,                                   /* count_elements */
    zend_std_get_debug_info,                /* get_debug_info */
    zend_std_get_closure,                   /* get_closure */
    zend_std_get_gc,                        /* get_gc */
    NULL,                                   /* do_operation */
    NULL,                                   /* compare */
};

Для того, чтобы работать с объектом как с массивом, используются следующие функции: read_dimension, write_dimension, has_dimension и unset_dimension.

Если мы посмотрим код zend_std_read_dimension, то увидим, что именно здесь происходит проверка на интерфейс ArrayAccess и вызывается соответсвующий метод offsetGet. А вызов php функции, как мы значем, это очень (ОЧЕНЬ!) медленно.

Решение очевидно: напишем свои функции (а заодно count и clone).

jco_darray.c

//Вспомогательная функция, которая приведет zval к long
static inline long zval_to_long(zval *zv) {
    if (Z_TYPE_P(zv) == IS_LONG) {
        return Z_LVAL_P(zv);
    } else {
        zval tmp = *zv;
        zval_copy_ctor(&tmp);
        convert_to_long(&tmp);
        return Z_LVAL(tmp);
    }
}

static zend_object_value jco_darray_clone(zval *object TSRMLS_DC) {
    jco_darray *old_object = zend_object_store_get_object(object TSRMLS_CC);

    zend_object_value new_object_val = jco_darray_create_object(Z_OBJCE_P(object) TSRMLS_CC);
    jco_darray *new_object = zend_object_store_get_object_by_handle(new_object_val.handle TSRMLS_CC);

    //Копируем свойства объекта
    zend_objects_clone_members(
        &new_object->std, new_object_val,
        &old_object->std, Z_OBJ_HANDLE_P(object) TSRMLS_CC
    );

    new_object->array = jco_ds_darray_clone(old_object->array);

    if (!new_object->array) {
        zend_throw_exception(NULL, "Failed to clone jco_darray", 0 TSRMLS_CC);
    }

    return new_object_val;
}

 
static zval *jco_darray_read_dimension(zval *object, zval *zv_offset, int type TSRMLS_DC) {
    jco_darray *intern = zend_object_store_get_object(object TSRMLS_CC);
    
    if (intern->std.ce->parent) {
        return zend_get_std_object_handlers()->read_dimension(object, zv_offset, type TSRMLS_CC);
    }

    if (!zv_offset) {
        zend_throw_exception(NULL, "Cannot append to a jco_darray", 0 TSRMLS_CC);
        return NULL;
    }

    long offset = zval_to_long(zv_offset);
    if (offset < 0 || offset > jco_ds_darray_length(intern->array)) {
        zend_throw_exception(NULL, "Offset out of range", 0 TSRMLS_CC);
        return NULL;
    }

    zval *return_value;
    zval *value = jco_ds_darray_get(intern->array, offset);

    if (value) {

        if (type != BP_VAR_R && type != BP_WAR_RW) {
            return_value = value;
            Z_SET_ISREF_P(return_value);
        } else {
            MAKE_STD_ZVAL(return_value);
            ZVAL_ZVAL(return_value, value, 1, 0);
            Z_DELREF_P(return_value);
        }
    } else {
        MAKE_STD_ZVAL(return_value);
        ZVAL_NULL(return_value);
        Z_DELREF_P(return_value);
    }

    return return_value;
}


static void jco_darray_write_dimension(zval *object, zval *zv_offset, zval *value TSRMLS_DC) {
    jco_darray *intern = zend_object_store_get_object(object TSRMLS_CC);

    if (intern->std.ce->parent) {
        return zend_get_std_object_handlers()->write_dimension(object, zv_offset, value TSRMLS_CC);
    }


    if (!zv_offset) {
        zend_throw_exception(NULL, "Cannot append to a jco_darray", 0 TSRMLS_CC);
    }

    long offset = zval_to_long(zv_offset);
    if (offset < 0) {
        zend_throw_exception(NULL, "Offset out of range", 0 TSRMLS_CC);
    }

    zval *saved_val = jco_ds_darray_set(intern->array, (size_t)offset, value);
    if (saved_val == NULL) {
        zend_throw_exception(NULL, "Error occured during dimension write", 0 TSRMLS_CC);
    }
}



static int jco_darray_has_dimension(zval *object, zval *zv_offset, int check_empty TSRMLS_DC) {
    jco_darray *intern = zend_object_store_get_object(object TSRMLS_CC);

    if (intern->std.ce->parent) {
        return zend_get_std_object_handlers()->has_dimension(object, zv_offset, check_empty TSRMLS_CC);
    }

    long offset = zval_to_long(zv_offset);
    if (offset < 0 || offset > jco_ds_darray_length(intern->array)) {
        return 0;
    }

    zval *value = jco_ds_darray_get(intern->array, offset);
    if (value == NULL) {
        return 0;
    }

    if (check_empty) {
        return zend_is_true(value);
    } else {
        return Z_TYPE_P(value) != IS_NULL;
    }

}

static void jco_darray_unset_dimension(zval *object, zval *zv_offset TSRMLS_DC) {
    jco_darray *intern = zend_object_store_get_object(object TSRMLS_CC);

    if (intern->std.ce->parent) {
        return zend_get_std_object_handlers()->unset_dimension(object, zv_offset TSRMLS_CC);
    }

    long offset = zval_to_long(zv_offset);
    if (offset < 0 || offset > jco_ds_darray_length(intern->array)) {
        zend_throw_exception(NULL, "Offset out of range", 0 TSRMLS_CC);
    }

    jco_ds_darray_unset(intern->array, offset);
}

int jco_darray_count_elements(zval *object, long *count TSRMLS_DC) {
    jco_darray *intern = zend_object_store_get_object(object TSRMLS_CC);

    if (intern->std.ce->parent) {
        return zend_get_std_object_handlers()->count_elements(object, count TSRMLS_CC);
    }

    if (intern && intern->array) {
        *count = (long)jco_ds_darray_count(intern->array);
        return SUCCESS;
    } else {
        *count = 0;
        return FAILURE;
    }
}


Здесь интересна функция jco_darray_read_dimension, которая третьим параметром принимает целочисленный type. Это флаг, указывающий в каком контексте была вызвана функция, и может примать значения BP_VAR_R, BP_VAR_W, BP_VAR_RW, BP_VAR_IS либо BP_VAR_UNSET.

$var[0][1]; // оба случая read_dimension BP_VAR_R
$var[0][1] = 1; // [0] - read_dimension BP_VAR_W, а [1] - write_dimension

isset($var[0][1]); // [0] - read_dimension BP_VAR_IS, а[1] - has_dimension


Если мы проигнорируем type и всегда будем возвращать копию значения, то во втором случае выше ничего не произойдет и значение массива внутри массива не поменяется. Чтобы это исправить в случае BP_VAR_W мы отдаем значение прямо из массива, а чтобы сборщик мусора не попытался его удалить, ставим zval→is_ref__gc = 1 (такой вот хак).

В каждой функции мы проверяем наличие (intern→std.ce→parent). Это на случай если кто-то отнаследуется от нашего класса и перезапишет методы ArrayAccess.

Чтобы php использовал наши функции вместо стандартных, добавим в jco_darray_init слудующие строчки

    jco_darray_handlers.has_dimension   = jco_darray_has_dimension;
    jco_darray_handlers.read_dimension  = jco_darray_read_dimension;
    jco_darray_handlers.write_dimension = jco_darray_write_dimension;
    jco_darray_handlers.unset_dimension = jco_darray_unset_dimension;
    jco_darray_handlers.count_elements  = jco_darray_count_elements;
    jco_darray_handlers.clone_obj = jco_darray_clone;

Cкомпилируем и запустим php c нашим расширением

    make && make install

~/dev/bin/php/bin/php -dextension=jco.so jco.php

JCO\Darray
TIME: 0.18597507476807
MEMORY: 11.44548034668
Array
TIME: 0.33455300331116
MEMORY: 137.51664733887

Потребление памяти на порядок ниже, а скорость выполнения выше почти в два раза. Успех!

Traversable

Чтобы наш объект был совсем массивом, нужно сделать его итерируемым. В object_handlers для итерации функций нет, но вот в zend_class_entry есть сразу функция get_iterator и структура iterator_funcs.

get_iterator возвращает zend_object_iterator, который используется для итерации (например в foreach).

struct _zend_object_iterator {
    void *data; // указатель на доп. данные класса
    zend_object_iterator_funcs *funcs; // указатель на функции итерирования и удаления итератора
    ulong index; //поле для опкодов. Мы его трогать не будем
};

iterator_funcs, насколько я понял, нужен для работы пользовательского кода: классов, которые реализуют интерфейсы Iterator или IteratorAggregate. Поля zf_* — (кэши?) соответвующих пользовательских php функций. А funcs аналогичен полю из _zend_object_iterator. Было бы хорошо, если бы в комментариях кто-нибудь дал более полное разъяснение как именно используется iterator_funcs.

В файле jco_darray.c после определения структуры jco_darray добавим структуру для хранения данных, нужных для итерирования.

typedef struct _jco_darray_iterator_data {
    zval *object_zval; //указатель на php объект (нужен, чтобы в процессе итерирвания его внезапно не уничтожили)
    jco_darray *object; // указатель на zend_object
    size_t offset; // текущая позиция
    zval *current; // текущее значение
} jco_darray_iterator_data;
 

Теперь напишем функцию get_iterator. В jco_darray.c после функции count_elements добавим функцию jco_darray_get_iterator.

//by_ref - флаг, указывающий что значения запрашиваются по ссылке.
zend_object_iterator *jco_darray_get_iterator(zend_class_entry *ce, zval *object, int by_ref TSRMLS_DC) {
    zend_object_iterator *iter;
    jco_darray_iterator_data *iter_data;

    if (by_ref) {
        zend_throw_exception(NULL, "UPS, no by reference iteration!", 0 TSRMLS_CC);
        return NULL;
    }

    iter = emalloc(sizeof(zend_object_iterator));
    iter->funcs = &jco_darray_iterator_funcs;

    iter_data = emalloc(sizeof(jco_darray_iterator_data));
    iter_data->object_zval = object;
    Z_ADDREF_P(object);

    iter_data->object = zend_object_store_get_object(object TSRMLS_CC);
    iter_data->offset = 0;
    iter_data->current = NULL;

    iter->data = iter_data;

    return iter;
}
 

И функции итерирования. Чтобы не объявлять их отдельно, добавим их перед функцией get_iterator.

jco_darray.c
static void jco_darray_iterator_dtor(zend_object_iterator *intern TSRMLS_DC) {
    jco_darray_iterator_data *data = (jco_darray_iterator_data *)intern->data;

    if (data->current != NULL) {
        zval_ptr_dtor(&data->current);
    }

    zval_ptr_dtor((zval **)&data->object_zval);
    efree(data);
    efree(intern);
}

static int jco_darray_iterator_valid(zend_object_iterator *intern TSRMLS_DC) {
    jco_darray_iterator_data *data = (jco_darray_iterator_data *)intern->data;

    return jco_ds_darray_length(data->object->array) > data->offset ? SUCCESS : FAILURE;
}

//
static void jco_darray_iterator_get_current_data(zend_object_iterator *intern, zval ***data TSRMLS_DC) {
    jco_darray_iterator_data *iter_data = (jco_darray_iterator_data *)intern->data;

    if (iter_data->current != NULL) {
        zval_ptr_dtor(&iter_data->current);
        iter_data->current = NULL;
    }

    if (iter_data->offset < jco_ds_darray_length(iter_data->object->array)) {
        zval *value = jco_ds_darray_get(iter_data->object->array, iter_data->offset);
        if (value != NULL) {
            MAKE_STD_ZVAL(iter_data->current);
            ZVAL_ZVAL(iter_data->current, value, 1, 0);

            *data = &iter_data->current;
        } else {
            *data = NULL;
        }

    } else {
        *data = NULL;
    }
}


#if ZEND_MODULE_API_NO >= 20121212
// версии php 5.5+
static void jco_darray_iterator_get_current_key(zend_object_iterator *intern, zval *key TSRMLS_DC) {
    jco_darray_iterator_data *data = (jco_darray_iterator_data *) intern->data;
    ZVAL_LONG(key, data->offset);
}
#else
//В более ранних версиях строковые и численные ключи нужно отдавать в отдельных переменных
// и возвращать HASH_KEY_IS_STRING, HASH_KEY_IS_LONG или HASH_KEY_NON_EXISTANT
static int jco_darray_iterator_get_current_key(zend_object_iterator *intern, char **str_key, uint *str_key_len, ulong *int_key TSRMLS_DC) {
    jco_darray_iterator_data *data = (jco_darray_iterator_data *) intern->data;

    *int_key = (ulong) data->offset;
    return HASH_KEY_IS_LONG;
}
#endif

static void jco_darray_iterator_move_forward(zend_object_iterator *intern TSRMLS_DC) {
    jco_darray_iterator_data *data = (jco_darray_iterator_data *) intern->data;

    data->offset++;
}

static void jco_darray_iterator_rewind(zend_object_iterator *intern TSRMLS_DC)
{
    jco_darray_iterator_data *data = (jco_darray_iterator_data *) intern->data;

    data->offset = 0;
    data->current = NULL;
}

static zend_object_iterator_funcs jco_darray_iterator_funcs = {
    jco_darray_iterator_dtor,
    jco_darray_iterator_valid,
    jco_darray_iterator_get_current_data,
    jco_darray_iterator_get_current_key,
    jco_darray_iterator_move_forward,
    jco_darray_iterator_rewind,
    NULL
};


Осталось только в jco_darray_init указать для класса наш get_iterator.

    jco_darray_ce->get_iterator = jco_darray_get_iterator;
    jco_darray_ce->iterator_funcs.funcs = &jco_darray_iterator_funcs;

Добавим в тестовый скрипт foreach

foreach($jar as $val) {
    if(($val % 100000) == 0) {
        echo $val . PHP_EOL;
    }
}

Cкомпилируем и запустим php c нашим расширением

    make && make install

~/dev/bin/php/bin/php -dextension=jco.so jco.php

Заключение


На этом, в принципе, все. С помощью интерфейсов Traversable и ArrayAccess мы написали быстрый индексный массив, который расходует на порядок меньше памяти, чем стандартный массив в PHP. За бортом осталась сериализация, но за ней советую обратиться к соответсвующей главе php internals book.

Несмотря на то, что в новом phpng изменилось очень многое (среди прочего сильно оптимизировали массивы), реализация внутренних интерфейсов для объектов, на момент написания статьи, не изменилась.

Ссылка на github-репозиторий.

И да, я соврал. Вот вам картинка.

cee55a9dbbc540a48d1e88a1887fb0e1.png

© Habrahabr.ru