antonio's blog

antonio's blog


Блог о всяком разном, связанном с разработкой ПО. Пишу редко, когда есть время и желание.

Anton Dobkin
Author

Share


Tags


Разработка расширений для PHP. Ресурсы

Anton DobkinAnton Dobkin

Ресурсы в PHP - это специальный, абстрактный тип в котором можно хранить любые пользовательские данные. К таким данным можно отнести дескрипторы файлов, структуры данных, различные хэндлы и т.д.

У каждого ресурса есть уникальный идентификатор - простое цело число, которое храниться в структуре zval и используется как индекс внутренней хэш-таблицы (см. главу "Хэш-таблицы"), в которой храниться указатель на зарегистрированные пользовательских данные.

Ресурсы автоматически уничтожаются, когда счетчик ссылок достигает 0 (см. главу "Данные и переменные"), при уничтожении вызывается функция-деструктор, если она была указана.

Ресурсы разделяются на два типа: простые и постоянные. Простые ресурсы - существуют в пределах обработки одного запроса и уничтожаются в конце сессии. Постоянные ресурсы - существуют между запросами и уничтожаются при выгрузке модуля.

Регистрация ресурса

Регистрацию ресурса можно разделить на несколько простых действий:

Как уже было сказано у ресурса есть уникальный идентификатор, помимо это идентификатора у ресурса есть идентификатор типа, который представляет из себя простое цело число. В расширениях, идентификатор типа должен храниться в глобальной переменной с типом int. Принято, что такие переменные имею префикс le_. Объявления переменной, хранящей тип ресурса, необходимо выполнить в самом начале, перед объявлением функций:

static int le_my_resource;  

Помимо идентификатора типа, ресурс также может иметь человекопонятное название типа, которое является константным значением. Название типа ресурса можно определить в заголовочном файле (или в файле с кодом) расширения следующим образом:

#define PHP_MY_RESOURCE_NAME "My resource"

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

Первым делом необходимо зарегистрировать свой тип ресурса с помощью одной из из следующих функций:

int zend_register_list_destructors(void (*ld)(void*), void (*pld)(void*), int module_number)

int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, char* res_name, int module_number)

Первым параметром функции принимают указатель на функцию-деструктор, которая будут вызвана при уничтожении обычного ресурса. Вторым - указатель на функцию-деструктор, которая будут вызвана при уничтожении постоянного ресурса. В качестве первого и/или второго аргумента допустимо использование NULL, если нет необходимости в функциях-деструкторах. Последним аргументом обе функции принимают идентификатор модуля.

Вторая функция, в качестве третьего аргумента, принимает человекопонятное название ресурса.

Необходимо обратить внимание, на то, что функции принимают указатели на функции-деструкторы разного типа. Первая функция принимает указатель на void функцию-деструктор, которая не принимает никаких аргументов. Вторая функция принимает на функцию функцию-деструктор со следующим прототипом:

typedef void (*rsrc_dtor_func_t)(zend_rsrc_list_entry *rsrc TSRMLS_DC)  

При вызове функции в качестве единственного аргумента в нее будет передан указатель на структуру zend_rsrc_list_entry:

typedef struct _zend_rsrc_list_entry {  
    void *ptr;     /* Указатель на данные */
    int type;      /* Идентификатор типа ресурса */
    int refcount;  /* Кол-во ссылок */
} zend_rsrc_list_entr

Функции возвращают идентификатор типа ресурса, который необходимо присвоить глобальной переменной, объявленной в самом начале. В дальнейшем этот идентификатор будет использоваться при непосредственной регистрации ресурса, а также поиске/получении ресурса по его идентификатору.

Регистрация типа ресурса должна осуществляться только один раз, в функции PHP_MINIT_FUNCTION() - функция вызывается единожды, при загрузке модуля.

Примеры регистрации типа ресурса:

#define PHP_MY_RESOURCE_NAME "My resource"

/*…*/

static int le_my_resource;

static void php_my_res_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC) {  
    FILE *fp = (FILE*)rsrc->ptr;
    fclose(fp);
}

PHP_MINIT_FUNCTION(test_module) {  
   /*…*/

   le_my_resource = zend_register_list_destructors_ex(php_my_res_dtor, NULL, PHP_MY_RESOURCE_NAME, module_number);
   return SUCCESS;
}

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

ZEND_REGISTER_RESOURCE(zval *rsrc, void *ptr, int type) - осуществляет регистрацию ресурса и возвращает его уникальный идентификатор.

Первым аргументом макрос принимает указатель на инициализированный zval-контейнер. Вторым аргументом - указатель на наши данные. Третьим - уникальный идентификатор типа ресурса, который мы получили и сохранили в глобальной переменной, когда регистрировали функции-деструкторы.

По сути данный макрос является синонимом к функции zend_register_resource(zval *rsrc_result, void *rsrc_pointer, int rsrc_type):

#define ZEND_REGISTER_RESOURCE(rsrc_result, rsrc_pointer, rsrc_type)  \
    zend_register_resource(rsrc_result, rsrc_pointer, rsrc_type);

Когда происходит регистрация ресурса, указатель на пользовательские данные rsrc_pointer и идентификатор типа ресурса rsrc_type помещаются в хэш-таблицу regular_list, которая расположена в глобальной таблице executor_globals (EG(regular_list). С помощью функции zend_hash_next_free_element(), функция возвращает идентификатор элемента хэш-таблицы, который заноситься в переменную rsrc_result, если указатель на нее был передан:

if (rsrc_result) {  
    rsrc_result->value.lval = rsrc_id;
    rsrc_result->type = IS_RESOURCE;
}

Примеры регистрации ресурса:

PHP_FUNCTION(test_fopen)  
{
   FILE *fp = NULL;
   char *fname = NULL;
   char *mode = NULL;
   int fname_len = 0;
   int mode_len = 0;

   if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &fname;, &fname;_len, &mode;, &mode;_len) == FAILURE) {
      RETURN_FALSE;
   }

   if (!fname_len || !mode_len) {
      zend_error(E_WARNING, "Invalid filename or mode");
       RETURN_FALSE;
   }

   fp = fopen(fname, mode);
   if (NULL == fp) {
      RETURN_FALSE;
   }
   ZEND_REGISTER_RESOURCE(return_value, fp, le_my_resource);
}

Если нет необходимости в том, чтобы макрос ZEND_REGISTER_RESOURCE() заносил идентификатор в zval-контейнер, то в качестве значения первого параметра необходимо передать NULL. Предыдущий пример можно переписать следующим, эквивалентным образом:

PHP_FUNCTION(test_fopen)  
{
   FILE *fp = NULL;
   char *fname = NULL;
   char *mode = NULL;
   int fname_len = 0;
   int mode_len = 0;
   int rsrc_id = -1;

   if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &fname;, &fname;_len, &mode;, &mode;_len) == FAILURE) {
      RETURN_FALSE;
   }

   if (!fname_len || !mode_len) {
     zend_error(E_WARNING, "Invalid filename or mode");
     RETURN_FALSE;
   }

   fp = fopen(fname, mode);
   if (NULL == fp) {
      RETURN_FALSE;
   }

   rsrc_id = ZEND_REGISTER_RESOURCE(NULL, fp, le_my_resource);

   /* some code */

   RETURN_RESOURCE(rsrc_id)
}

Выполнение следующего кода:

$file_res = test_fopen("test_file.txt", "r");
var_dump($file_res);  

выведет на экран:

resource(1) of type (My resource)  

Получение ресурса

void *zend_list_find(int rsrc_id, int *found_rsrc_type TSRMLS_DC) - функция осуществляет поиск ресурса. Первым аргументом функция принимает идентификатор ресурса. Если в качестве второго аргумента передан указатель на переменную, то в эту переменную будет записан идентификатор типа найденного ресурса. Функция возвращает NULL, если ресурс небыл найден, иначе указатель на ресурс.

PHP_FUNCTION(test_fwrite) {  
   zval *res = NULL;
   int rsrc_type = -1;
   char *data = NULL;
   int data_len = 0;
   FILE *fp = NULL;

   if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs", &res;, &data;, &data;_len) == FAILURE) {
      RETURN_FALSE
   }

   fp = (FILE *) zend_list_find(Z_RESVAL_P(res), &rsrc;_type TSRMLS_CC);

   if (!fp || rsrc_type != le_my_resource) {
      zend_error(E_WARNING, "Invalid resource provided");
      RETURN_FALSE
  }

  if(data_len == fwrite(data, sizeof(char), data_len, fp)) {
     RETURN_TRUE
  }
  RETURN_FALSE
}

Поиск осуществляется в хэш-таблице EG(regular_list). Для поиска ресурса в хэш-таблице вызывается функция zend_hash_find()

zend_fetch_resource(zval **rsrc TSRMLS_DC, int rsrc_id, const char *rsrc_name, int *found_rsrc_type, int num_rsrc_types, …) - функция также как и zend_list_find() осуществляет поиск ресурса, но в отличие от нее, функция, для поиска, принимает идентификатор ресурса rsrc_id или zval-контейнер. Также в функции производятся дополнительные проверки типа найденного ресурса.

Аргументы функции:

АргументОписание
rsrc_zvalконтейнер, в котором содержится идентификатор ресурса
rsrc_idидентификатор ресурса
rsrc_nameчеловекопонятное название ресурса, используется только в сообщениях об ошибках
found_rsrc_typeуказатель на переменную, в которую будет записан идентификатор типа найденного ресурса
num_rsrcкол-во идентификаторов типа ресурса
идентификаторы типа ресурса

Если в функцию, в качестве значения аргумента rsrc, передать zval-контейнер, а в качестве значения аргумента rsrc_id -1, то идентификатор ресурса будет извлечен из zval-контейнера и поиск будет осуществляться по этому идентификатору. Иначе, поиск будет осуществлен по идентификатору переданному в качестве аргумента rsrc_id. Если в функцию переданы оба аргумента, то приоритет у rsrc_id.

Если в функцию в качестве аргумента found_rsrc_type передан указатель на переменную, то в эту переменную будет записан идентификатор типа найденного ресурса.

Аргумент num_rsrc_types принимает число - кол-во идентификаторов типа, которые будут переданы после этого аргумента, которым может соответствовать искомый ресурс.

После аргумента num_rsrc_types в функцию передаются идентификаторы типов, с которыми будет сравниваться идентификатор типа найденного ресурса. Как только будет найдено совпадение, функция завершит свою работу, переменной found_rsrc_type будет присвоен идентификатор найденного типа.

Внутри функции zend_fetch_resource() сравнение происходит след. образом:

/* … */

resource = zend_list_find(id, &actual;_resource_type);

/* … */

for (i = 0; i < num_rsrc_types; i++) {  
    if (actual_resource_type == va_arg(resource_types, int)) {
       va_end(resource_types);
       if (found_rsrc_type) {
           *found_rsrc_type = actual_resource_type;
       }
       return resource;
   }
}

/* … */

Если ресурс не найден или тип найденного ресурса не соответствует не одному заданному типу, функция возвращает NULL и генерирует предупреждение. Обратите внимание, на то, что предупреждение не будет сгенерировано, если в качестве значения аргумента rsrc_name будет передан NULL.

Изменим пример, который был приведен при рассмотрении функции zend_list_find():

PHP_FUNCTION(test_fwrite)  
{
    zval *res = NULL;
    int rsrc_type = -1;
    char *data = NULL;
    int data_len = 0;
    FILE *fp = NULL;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs", &res;, &data;, &data;_len) == FAILURE) {
       RETURN_FALSE
    }

    fp = (FILE *) zend_fetch_resource(&res; TSRMLS_CC, -1, PHP_MY_RESOURCE_NAME, NULL, 1, le_my_resource);
    if(fp == NULL) {
      RETURN_FALSE
    }

    if(data_len == fwrite(data, sizeof(char), data_len, fp)){
       RETURN_TRUE
    }
   RETURN_FALSE
}

В данном примере вместо функции zend_list_find() была использована функция zend_fetch_resource(). Как было сказано в функции производятся проверки типа найденного ресурса с переданными типом, поэтому блок проверки:

if (!fp || rsrc_type != le_my_resource) {  
    zend_error(E_WARNING, "Invalid resource provided");
    RETURN_FALSE
}

излишен.

echo "Test 1<br></br>";  
echo "--------------<br></br>";  
res = curl_init();  
_d($res);  
/* Ошибка! */
_d(test_fwrite($res, "Hello, world!\n"));

/* … */
echo "<br></br>Test 2<br></br>";  
echo "--------------<br></br>";  
$file = test_fopen("/tmp/file.txt", "w+");
var_dump($file);  
var_dump(test_fwrite($file, "Hello, world!\n"));  

Результат работы скрипта:

Test 1
--------------
resource(2) of type (curl)
Warning: Invalid resource provided in /Users/antonio/Sites/test.php on line 38
bool(false)

Test 2
--------------
resource(3) of type (My resource)
bool(true)

Ниже представлены макросы, обертки над функцией zend_fetch_resource():

ZEND_FETCH_RESOURCE(rsrc, resrc_type, passed_id, default_id, resource_type_name, resource_type) - осуществляет поиск ресурса. Если функция zend_fetch_resource() вернула NULL (ресурс не был найден или тип найденного ресурса не соответствует искомому типу resource_type), функция, в которой был использован макрос, прервется и вернет в пользовательское пространство FALSE

ZEND_FETCH_RESOURCE_NO_RETURN(rsrc, resrc_type, passed_id, default_id, resource_type_name, resource_type) - аналогичен макросу ZEND_FETCH_RESOURCE(), но, в отличие от него, не прерывает работу функции, в которой он был использован, если функция zend_fetch_resource() вернула NULL

Аргументы макросов:

АргументОписание
rsrcпеременная-указатель, которой будет присвоен результат поиска
resrc_typeтип к которому будет приведен найденный ресурс
passed_idzval-контейнер, в котором содержится идентификатор ресурса
default_idидентификатор ресурса
resource_type_nameчеловекопонятное название ресурса, используется только в сообщениях об ошибках
resource_typeидентификатор типа искомого ресурса

Если в функцию, в качестве значения параметра passed_id, передать zval-контейнер, а в качестве значения параметра default_id передать -1, то идентификатор ресурса будет извлечен из zval-контейнера. Иначе поиск будет осуществлен по идентификатору переданному в качестве аргумента default_id. Если в макрос переданы оба аргумента, то приоритет у default_id.

Еще раз изменим нашу функцию testfwrite(), для получения ресурса используем макрос _ZENDFETCHRESOURCE():

PHP_FUNCTION(test_fwrite) {  
   zval *res = NULL;
   int rsrc_type = -1;
   char *data = NULL;
   int data_len = 0;
   FILE *fp = NULL;

   if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs", &res;, &data;, &data;_len) == FAILURE) {
     RETURN_FALSE
   }

   ZEND_FETCH_RESOURCE(fp, FILE *, &res;, -1, PHP_MY_RESOURCE_NAME, le_my_resource);

   if(data_len == fwrite(data, sizeof(char), data_len, fp)) {
     RETURN_TRUE
   }
   RETURN_FALSE
}

ZEND_FETCH_RESOURCE2(rsrc, resrc_type, passed_id, default_id, resource_type_name, resource_type1, resource_type2)

ZEND_FETCH_RESOURCE2_NO_RETURN(rsrc, resrc_type, passed_id, default_id, resource_type_name, resource_type1, resource_type2)

Эти макросы идентичным макросам описанным выше за одним лишь исключением - сравнение типа найденного ресурса осуществляется с двумя типами переданными в качестве значений аргументов resource_type и resource_type2. Данные макросы используются в функциях, которые могут работать с разными типами ресурсов, например, c обычным и постоянным типом ресурса.

Аргументы макросов:

АргументОписание
rsrcпеременная-указатель, которой будет присвоен результат поиска
resrc_typeтип к которому будет приведен найденный ресурс
passed_id_zval_-контейнер, в котором содержится идентификатор ресурса
default_idидентификатор ресурса
resource_type_nameчеловекопонятное название ресурса, используется только в сообщениях об ошибках
resource_type1идентификатор типа искомого ресурса
resource_type2идентификатор типа искомого ресурса

Определение макросов:

#define ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id, default_id, resource_type_name, resource_type) \
  rsrc = (rsrc_type) zend_fetch_resource(passed_id TSRMLS_CC, default_id, resource_type_name, NULL, 1, resource_type); \
  ZEND_VERIFY_RESOURCE(rsrc);

#define ZEND_FETCH_RESOURCE_NO_RETURN(rsrc, rsrc_type, passed_id, default_id, resource_type_name, resource_type) \
  (rsrc = (rsrc_type) zend_fetch_resource(passed_id TSRMLS_CC, default_id, resource_type_name, NULL, 1, resource_type))

#define ZEND_FETCH_RESOURCE2(rsrc, rsrc_type, passed_id, default_id, resource_type_name, resource_type1, resource_type2) \
  rsrc = (rsrc_type) zend_fetch_resource(passed_id TSRMLS_CC, default_id, resource_type_name, NULL, 2, resource_type1, resource_type2); \
  ZEND_VERIFY_RESOURCE(rsrc);

#define ZEND_FETCH_RESOURCE2_NO_RETURN(rsrc, rsrc_type, passed_id, default_id, resource_type_name, resource_type1, resource_type2) \
  (rsrc = (rsrc_type) zend_fetch_resource(passed_id TSRMLS_CC, default_id, resource_type_name, NULL, 2, resource_type1, resource_type2))

Уничтожение ресурса

Как только счетчик ссылок на ресурс достигнет нуля, ресурс будет автоматически уничтожен. Также для уничтожения ресурса можно воспользоваться функцией zend_list_delete(int id), которая единственным параметром принимает идентификатор ресурса. Обратите внимание на то, что ресурс может быть удален не сразу. Функция уменьшает кол-во ссылок на ресурс на единицу, после этого, если счетчик ссылок достиг нуля, происходит уничтожения ресурса.

Для уничтожения ресурса вызывается функция-деструктор, если она была зарегистрирована.

Для немедленного удаления ресурса его необходимо из хэш-таблицы EG**(regular_list):

PHP_FUNCTION(test_fclose)  
{
    FILE *fp = NULL;
    zval *res = NULL;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &res;) == FAILURE) {
      RETURN_FALSE;
    }

    ZEND_FETCH_RESOURCE(fp, FILE *, &res;, -1, PHP_MY_RESOURCE_NAME, le_my_resource);

    /* При удалении элемента из хэш таблицы будет вызвана функция-деструктор */
    zend_hash_index_del(&EG;(regular_list), Z_RESVAL_P(res));

    RETURN_TRUE;
}

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

Другие макросы и функции

int zend_fetch_list_dtor_id(char *type_name) - возвращает идентификатор типа ресурса по его человекопонятному описанию.

int zend_list_addref(int id TSRMLS_DC) - увеличивает на единицу счетчик ссылок на ресурс

ZEND_VERIFY_RESOURCE(rsrc) - проверка значения переменной rsrc, если значения NULL, то работа функции, в которой используется макрос, будет прервана и в пользовательское пространство функция вернет FALSE

Определение макроса:

#define ZEND_VERIFY_RESOURCE(rsrc) \
  if (!rsrc) {                   \
    RETURN_FALSE;              \
  }

ZEND_GET_RESOURCE_TYPE_ID(le_id, le_type_name) - макрос, обертка над функцией zend_fetch_list_dtor_id()

Определение макроса:

#define ZEND_GET_RESOURCE_TYPE_ID(le_id, le_type_name) \
   if (le_id == 0) {                                  \
     le_id = zend_fetch_list_dtor_id(le_type_name); \
   }
Anton Dobkin
Author

Anton Dobkin

Comments