Kenan
Assistant Engineer
Assistant Engineer
  • UID621
  • Fans0
  • Follows0
  • Posts55
Reads:1266Replies:0

Implement third-party extension with resource package in PHP 7 and its source code

Created#
More Posted time:Sep 13, 2016 10:10 AM
Before proceeding to the content below, you should have some preliminary knowledge in the basic data structure of PHP 7, as it is the premise for understanding the following content.
We will illustrate in two parts:
First, implement a custom file operation extension for opening, reading, writing and closing the file.
Then we analyze the implementation principles behind each operation. For some of them, I will make a comparative analysis with the source code of third-party extension with resource package in PHP 5.3.
Generate extension skeleton through prototype
First, add a prototype file for file operation in the ext directory of the source code.
[root@localhost php-src-php-7.0.3]# cd ext/
[root@localhost ext]# vim tipi_file.proto


Edit the prototype file as below.
resource file_open(string filename, string mode)
string file_read(resource filehandle, int size)
bool file_write(resource filehandle, string buffer)
bool file_close(resource filehandle)


Generate the skeleton.
[root@localhost ext]# ./ext_skel --extname=tipi_file --proto=./tipi_file.proto

So a simple extension code skeleton for file operation is generated.
The complete code can be found at tipi_file.c. Your rough understanding in it is helpful for you to have a clearer mind for the latter part of this article.
Implement extension
1.1 Register resource type
1.1.1 Register resource API
ZEND_API int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, const char *type_name, int module_number)



The API returns a resource type id which should be saved in the extension as a global variable for being passed to other resource APIs as necessary.
1.1.2 Add resource release callback function
static void tipi_file_dtor(zend_resource *rsrc TSRMLS_DC){
     FILE *fp = (FILE *) rsrc->ptr;
     fclose(fp);
}


We can see that the function's parameter type is zend_resource. This is a new data structure in PHP 7. It corresponds to zend_rsrc_list_entry in PHP 5. Details will be illustrated later.
1.1.3 Register in PHP_MINIT_FUNCTION
We know that in the PHP lifecycle, when PHP is loaded, PHP_MINIT_FUNCTION (module initiation function) will be called by an engine. The engine will perform one-off initiation of resource types, registration INI variables and so on.
Here, we need to register resource types through zend_register_list_destructors_ex in PHP_MINIT_FUNCTION.
PHP_MINIT_FUNCTION(tipi_file)
{
    /* If you have INI entries, uncomment these lines
    REGISTER_INI_ENTRIES();
    */
 
    le_tipi_file = zend_register_list_destructors_ex(tipi_file_dtor, NULL, TIPI_FILE_TYPE, module_number);
    return SUCCESS;
}


Among them, TIPI_FILE_TYPE has been defined above, the alias of the extension (More details can be found in tipi_file.c).
1.2 Register resource
1.2.1 Register resource API
In PHP 7, ZEND_REGISTER_RESOURCE macro is deleted, and zend_register_resource function is used directly.
ZEND_API zend_resource* zend_register_resource(void *rsrc_pointer, int rsrc_type)



1.2.2 Implement resource registration in file_open function
PHP_FUNCTION(file_open)
{
    char *filename = NULL;
    char *mode = NULL;
    int argc = ZEND_NUM_ARGS();
    size_t filename_len;
    size_t mode_len;
 
    if (zend_parse_parameters(argc TSRMLS_CC, "ss", &filename, &filename_len, &mode, &mode_len) == FAILURE)
        return;
 
    // Replace standard C file operation function with VCWD macro
    FILE *fp = VCWD_FOPEN(filename, mode);
 
    if (fp == NULL) {
        RETURN_FALSE;
    }
 
    RETURN_RES(zend_register_resource(fp, le_tipi_file));
}


In specific, the RETURN_RES macro adds the returned zend_resource to zval, and makes the last zval the returned value. That is, the returned value of the function is zval pointer. RETURN_RES(zend_register_resource(fp, le_tipi_file)) will set the returned value value.res as fp, and set u1.type_info as IS_RESOURCE_EX. You can get an intuitive idea about it from the source code, so I will not go into the detail.
1.3 Use resource
1.3.1 Use resource API
ZEND_API void *zend_fetch_resource(zend_resource *res, const char *resource_type_name, int resource_type)

In PHP 7, ZEND_FETCH_RESOURCE macro is deleted, and zend_fetch_resource function is used directly. The parsing is also much easier and more efficient compared with PHP 5. We will make a comparison through the illustrative chart later.


1.3.2 Implement parsing resource
When we implement file reads, native fread function is used. So here we still need to parse zend_resource into the original FILE * pointer of the resource package through zend_fetch_resource.
PHP_FUNCTION(file_read)
{
    int argc = ZEND_NUM_ARGS();
    int filehandle_id = -1;
    zend_long size;
    zval *filehandle = NULL;
    FILE *fp = NULL;
    char *result;
    size_t bytes_read;
 
    if (zend_parse_parameters(argc TSRMLS_CC, "rl", &filehandle, &size) == FAILURE)
        return;
 
    if ((fp = (FILE *)zend_fetch_resource(Z_RES_P(filehandle), TIPI_FILE_TYPE, le_tipi_file)) == NULL) {
        RETURN_FALSE;
    }
 
    result = (char *) emalloc(size+1);
    bytes_read = fread(result, 1, size, fp);
    result[bytes_read] = '\0';
 
    RETURN_STRING(result, 0);
 
}


Note: The automatically generated extension code still sticks to ZEND_FETCH_RESOURCE, which is a bug, because the automatic generation script (ext/skeleton/create_stubs) is not updated yet.
The same case applies to file writing operations. Here I will just copy the code, and you can visit tipi_file.c to view the complete code.
1.4 Delete resource
1.4.1 Resource deletion API
ZEND_API int zend_list_close(zend_resource *res)

You only need to pass in the resource to be deleted. The API looks simple, but it does a lot of work, which will be explained in detail later.
1.4.2 Implement resource deletion
We need to call the resource deletion API in file_close function.
PHP_FUNCTION(file_close)
{
    int argc = ZEND_NUM_ARGS();
    int filehandle_id = -1;
    zval *filehandle = NULL;
 
    if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) == FAILURE)
        return;
 
    zend_list_close(Z_RES_P(filehandle));
    RETURN_TRUE;
}


1.5 Compile, install and test
1.5.1 Compile and install
Through the code above, a simple third-party extension is ready.
You can customize the following command configuration according to your environment.
root@localhost tipi_file]# php7ize
Configuring for:
PHP Api Version:         20151012
Zend Module Api No:      20151012
Zend Extension Api No:   320151012
[root@localhost tipi_file]# ./configure --with-php-config=/usr/local/php7/bin/php-config
...
[root@localhost tipi_file]# make
...
[root@localhost tipi_file]# make install
...


1.5.2 Test
Test using PHP script directly without writing testing samples function by function. Edit tipi_file.php file.
$fp = file_open("./CREDITS","r+");
var_dump($fp);
var_dump(file_read($fp,6));
var_dump(file_write($fp,"zhoumengakng"));
var_dump(file_close($fp));


Execute the command line
php7 -d"extension=tipi_file.so" tipi_file.php

Source code analysis
2.1 Register resource type source code
ZEND_API int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, const char *type_name, int module_number)
{
   zend_rsrc_list_dtors_entry *lde;
   zval zv;
 
   lde = malloc(sizeof(zend_rsrc_list_dtors_entry));
   lde->list_dtor_ex = ld;
   lde->plist_dtor_ex = pld;
   lde->module_number = module_number;
   lde->resource_id = list_destructors.nNextFreeElement;
   lde->type_name = type_name;
   ZVAL_PTR(&zv, lde);
 
   if (zend_hash_next_index_insert(&list_destructors, &zv) == NULL) {
      return FAILURE;
   }
   return list_destructors.nNextFreeElement-1;
}


Among them,
ZVAL_PTR(&zv, lde);

after being expanded, is equivalent to
zv.value.ptr = (lde);
zv.u1.type_info = IS_PTR;


List_destructors is a global static HashTable. During resource type registration, a zval struct variable zv is stored into arData of list_destructors, and the value.ptr of zv points to zend_rsrc_list_dtors_entry *lde. lde contains the resource releasing function pointer, the persistent resource releasing function pointer, the resource type name, and the index by information (resource_id) of the resource in hashtable.
The resource_id here is the returned value of the function. So resource_id is always needed during our parsing this type of variable later.
The whole registration process can be summarized as follows:


2.2 Register resource
ZEND_API zend_resource* zend_register_resource(void *rsrc_pointer, int rsrc_type)
{
   zval *zv;
 
   zv = zend_list_insert(rsrc_pointer, rsrc_type);
 
   return Z_RES_P(zv);
}


The function serves to return the resource pointer in zval returned by zend_list_insert. Z_RES_P macro is defined in Zend/zend_types.h.
We will focus on zend_list_insert.
ZEND_API zval *zend_list_insert(void *ptr, int type)
{
   int index;
   zval zv;
 
   index = zend_hash_next_free_element(&EG(regular_list));
   if (index == 0) {
      index = 1;
   }
   ZVAL_NEW_RES(&zv, index, ptr, type);
   return zend_hash_index_add_new(&EG(regular_list), index, &zv);
}


The zend_hash_next_free_element macro returns nNextFreeElement of the &EG(regular_list) table as the index by data.
ZVAL_NEW_RES macro is a new feature in PHP 7. It loads a resource into zval, because in PHP 7, Bucket can only store zval.
#define ZVAL_NEW_RES(z, h, p, t) do {                         \
        zend_resource *_res =                                 \
        (zend_resource *) emalloc(sizeof(zend_resource));     \
        zval *__z;                                         \
        GC_REFCOUNT(_res) = 1;                                    \
        GC_TYPE_INFO(_res) = IS_RESOURCE;                     \
        _res->handle = (h);                                        \
        _res->type = (t);                                      \
        _res->ptr = (p);                                       \
        __z = (z);                                            \
        Z_RES_P(__z) = _res;                                  \
        Z_TYPE_INFO_P(__z) = IS_RESOURCE_EX;                  \
    } while (0)


The code is clear. First, a resource is created based on h, p, t, and stored to z struct of zval. (The last two macros have been discussed above)
The last one is the zend_hash_index_add_new macro. From its code, we can see it is equivalent to calling
_zend_hash_index_add_or_update_i(&EG(regular_list), index, &zv, HASH_ADD | HASH_ADD_NEW ZEND_FILE_LINE_RELAY_CC)

Specific operations on HashTable are not discussed here. We can talk about it later separately.


2.3 Parse resource source code
ZEND_API void *zend_fetch_resource(zend_resource *res, const char *resource_type_name, int resource_type)
{
   if (resource_type == res->type) {
      return res->ptr;
   }
 
   if (resource_type_name) {
      const char *space;
      const char *class_name = get_active_class_name(&space);
      zend_error(E_WARNING, "%s%s%s(): supplied resource is not a valid %s resource", class_name, space, get_active_function_name(), resource_type_name);
   }
 
   return NULL;
}


In the example above, we parse like this:
(FILE *)zend_fetch_resource(Z_RES_P(filehandle), TIPI_FILE_TYPE, le_tipi_file)

First, through Z_RES_P macro, we get zend_resource in zval variable of filehandle.  Then, zend_fetch_resource compares the type of zend_resource against our expectation, and returns *ptr of zend_resource and converts it to FILE * pointer.
The resource parsing in PHP 7 is much easier and quicker than that in PHP 5, thanks to its zval structure change.
In PHP 5, resource needs to be checked through EG(regular_list), as shown in the figure below:


But in PHP 7, you can parse zend_resource from zval directly, as shown in the figure below:


2.4 Delete resource source code
ZEND_API int zend_list_close(zend_resource *res)
{
   if (GC_REFCOUNT(res) <= 0) {
      return zend_list_free(res);
   } else if (res->type >= 0) {
      zend_resource_dtor(res);
   }
   return SUCCESS;
}


Unlike PHP 5, PHP 7 won’t decrement the reference count by one every time a resource is passed in, but calls zend_resource_dtor function.
static void zend_resource_dtor(zend_resource *res)
{
   zend_rsrc_list_dtors_entry *ld;
   zend_resource r = *res;
 
   res->type = -1;
   res->ptr = NULL;
 
   ld = zend_hash_index_find_ptr(&list_destructors, r.type);
   if (ld) {
      if (ld->list_dtor_ex) {
         ld->list_dtor_ex(&r);
      }
   } else {
      zend_error(E_WARNING, "Unknown list entry type (%d)", r.type);
   }
}


Only when the reference count is already 0 or smaller than 0 will PHP 7 delete the resource from EG(regular_list).
ZEND_API int zend_list_free(zend_resource *res)
{
   if (GC_REFCOUNT(res) <= 0) {
      return zend_hash_index_del(&EG(regular_list), res->handle);
   } else {
      return SUCCESS;
   }
}


The diagram of the principle can follow the above registration resource type and register resource diagram:


First, it finds the releasing callback function for the resource type through type indexing in list_destructors from zend_resource, then executes the releasing callback function on the resource.
Later, the resource is deleted in EG(regular_list) by making res->handler the index criterion.
Guest