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" = "так"; 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! ЇJ!", 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! ЇJ!


Коротко про 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 Local Storage Manager! Покажчик на змінні потоку


Аргументи функцій визначаються за допомогою макросів 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)
<?php
$darray = new \JCO\DArray();
echo $darray->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"

Напишемо тестовий код і заодно порівняємо з наш клас із звичайним масивом
<?php
ini_set("memory_limit", "512M");

$data = range(1, 500000);

$t1 = microtime(true);
$m1 = memory_get_usage();

$jar = new \JCO\DArray(500000, 0);
foreach($data as $index => &$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 "АР" . PHP_EOL;
echo "TIME: " . (microtime(true) - $t1) . PHP_EOL;
echo "MEMORY: " . ((memory_get_usage() - $m1)/1048576) . PHP_EOL;
gc_collect_cycles();

?>


Скомпилируем і запустимо 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;


Скомпилируем і запустимо 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;
}
}


Скомпилируем і запустимо php c нашим розширенням
make && make install

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


Висновок
На цьому, в принципі, все. За допомогою інтерфейсів Traversable і ArrayAccess ми написали швидкий індексний масив, який витрачає на порядок менше пам'яті, ніж стандартний масив в PHP. За бортом залишилася серіалізація, але за нею раджу звернутися до відповідної чолі php internals book.

Незважаючи на те, що в новому phpng змінилося дуже багато (серед іншого сильно оптимізували масиви), реалізація внутрішніх інтерфейсів для об'єктів, на момент написання статті, не змінилася.

Посилання на github-репозиторій.

І так, я збрехав. Ось вам картинка.



Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.