PHP7 使用資源包裹第三方擴展的實現及其源碼解讀

在閱讀下面的內容以前,咱們假定你已經對 PHP 7 基本的數據結構都有大體的瞭解了,這是下面內容閱讀的前提。php

咱們分爲兩大塊:html

首先實現一個自定義的文件打開、讀取、寫入、關閉的文件操做擴展;git

而後分析各個操做背後的實現原理,其中某些部分的實現我會和 PHP 5.3 使用資源包裹第三方擴展源碼解讀 對比分析。github

0 經過原型生成擴展骨架vim

首先進入到源碼目錄的ext目錄中,添加一個文件操做的原型文件php7

1 [root@localhost php-src-php-7.0.3]# cd ext/
2 [root@localhost ext]# vim tipi_file.proto
編輯原型爲數據結構

1 resource file_open(string filename, string mode)
2 string file_read(resource filehandle, int size)
3 bool file_write(resource filehandle, string buffer)
4 bool file_close(resource filehandle)
5 [root@localhost ext]# ./ext_skel --extname=tipi_file --proto=./tipi_file.proto函數

這樣一個簡單的文件操做擴展的代碼骨架就生成了。源碼分析

完整代碼 tipi_file.c(https://github.com/zhoumengkang/notes/blob/master/php-extension/php7.0/tipi_file/tipi_file.c),能夠先有一個大體的瞭解,這樣後面閱讀時,思路可能會清晰不少。測試

1 擴展的實現

1.1 註冊資源類型

1.1.1 註冊資源 API

1 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)

參數 解釋
ld 釋放該資源時調用的函數。
pld 釋放用於在不一樣請求中始終存在的永久資源的函數。
type_name 是一個具備描述性類型名稱的字符串。
module_number 爲引擎內部使用,當咱們調用這個函數時,咱們只須要傳遞一個已經定義好的module_number變量。

該 API 返回一個資源類型 id,該id應當被做爲全局變量保存在擴展裏,以便在必要的時候傳遞給其餘資源API。

1.1.2 添加資源釋放回調函數

1 static void tipi_file_dtor(zend_resource *rsrc TSRMLS_DC){
   2      FILE *fp = (FILE *) rsrc->ptr;
   3      fclose(fp);
   4 }

咱們發現該函數的參數類型是zend_resource。這是 PHP7 新增的數據結構,在 PHP 5 則是zend_rsrc_list_entry。細節的內容,咱們留在後面分析。

1.1.3 在PHP_MINIT_FUNCTION中註冊

咱們知道在 PHP 生命週期中,當 PHP 被裝載時,PHP_MINIT_FUNCTION(模塊啓動函數)即被引擎調用。這使得引擎作一些例如資源類型,註冊INI變量等的一次初始化。

那麼咱們須要在這裏經過zend_register_list_destructors_ex在PHP_MINIT_FUNCTION來註冊資源類型。

1  PHP_MINIT_FUNCTION(tipi_file)
  2  {
  3     /* If you have INI entries, uncomment these lines
  4      REGISTER_INI_ENTRIES();
  5      */
  6   
  7     le_tipi_file = zend_register_list_destructors_ex(tipi_file_dtor, NULL, TIPI_FILE_TYPE, module_number);
  8      return SUCCESS;
  9  }

其中TIPI_FILE_TYPE在前面已經定義了,是該擴展的別名(具體能夠對比着代碼 tipi_file.c 查看連接描述

1.2 註冊資源

1.2.1 註冊資源 API

在 PHP 7 中刪除了原來的ZEND_REGISTER_RESOURCE宏,直接使用zend_register_resource函數

1 ZEND_API zend_resource* zend_register_resource(void *rsrc_pointer, int rsrc_type)

參數 解釋
rsrc_pointer 資源數據指針
rsrc_type 註冊資源類型時得到的資源類型 id

1.2.2 在 file_open函數中實現資源的註冊

1  PHP_FUNCTION(file_open)
2    {
3        char *filename = NULL;
4        char *mode = NULL;
5        int argc = ZEND_NUM_ARGS();
6        size_t filename_len;
7        size_t mode_len;
8     
9       if (zend_parse_parameters(argc TSRMLS_CC, "ss", &filename, &filename_len, &mode, &mode_len) == FAILURE) 
10            return;
11     
12        // 使用 VCWD 宏取代標準 C 文件操做函數
13       FILE *fp = VCWD_FOPEN(filename, mode);
14    
15       if (fp == NULL) {
16           RETURN_FALSE;
17       }
18    
19        RETURN_RES(zend_register_resource(fp, le_tipi_file));
20       }

其中RETURN_RES宏的做用是將返回的zend_resource添加到zval中,而後將最後的zval做爲返回值。也就是說該函數的返回值爲zval指針。RETURN_RES(zend_register_resource(fp, le_tipi_file))會將返回值的value.res設爲fp,u1.type_info設爲IS_RESOURCE_EX。你們能夠根據源碼很是直觀的瞭解到,這裏不粘貼代碼詳細說明了。

1.3 使用資源

1.3.1 使用資源 API
1 ZEND_API void zend_fetch_resource(zend_resource res, const char *resource_type_name, int resource_type)

在 PHP 7 中刪除了原有的ZEND_FETCH_RESOURCE宏,直接使用函數zend_fetch_resource,並且解析方式也變得簡單了不少,想比 PHP 5 要高效不少,後面咱們再經過圖片分析對比。

參數 含義
res 資源指針
resource_type_name 該類資源的字符串別名
resource_type 該類資源的類型 id

1.3.2 解析資源的實現

當咱們要實現文件的讀取時,最終仍是須要使用原生的fread函數,因此這裏須要經過zend_fetch_resource將zend_resource解析成爲該資源包裹的原始的FILE *的指針。

1 PHP_FUNCTION(file_read)
2 {
3 int argc = ZEND_NUM_ARGS();
4 int filehandle_id = -1;
5 zend_long size;
6 zval *filehandle = NULL;
7 FILE *fp = NULL;
8 char *result;
9 size_t bytes_read;
10
11 if (zend_parse_parameters(argc TSRMLS_CC, "rl", &filehandle, &size) == FAILURE)
12 return;
13
14 if ((fp = (FILE *)zend_fetch_resource(Z_RES_P(filehandle), TIPI_FILE_TYPE, le_tipi_file)) == NULL) {
15 RETURN_FALSE;
16 }
17
18 result = (char *) emalloc(size+1);
19 bytes_read = fread(result, 1, size, fp);
20 result[bytes_read] = '0';
21
22 RETURN_STRING(result, 0);
23
24 }

這裏須要說明,腳本自動生成的擴展代碼中仍是使用ZEND_FETCH_RESOURCE, 是個 BUG,由於自動生成的腳本(ext/skeleton/create_stubs)還沒更新。

與之相似的文件的寫入操做,也很相似,這裏就複製代碼了,請查看完整的代碼 tipi_file.c(https://github.com/zhoumengkang/notes/blob/master/php-extension/php7.0/tipi_file/tipi_file.c

1.4 資源的刪除

1.4.1 資源刪除 API

ZEND_API int zend_list_close(zend_resource *res)

傳入須要被刪除的資源便可。該 API 看似很是簡單,實際作了不少工做,後面原理分析細說。

1.4.2 資源刪除的實現

咱們在函數file_close中須要調用資源刪除 API

1  PHP_FUNCTION(file_close)
2  {
3    int argc = ZEND_NUM_ARGS();
4    int filehandle_id = -1;
5    zval *filehandle = NULL;
6 
7    if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) == FAILURE) 
8        return;
9 
10    zend_list_close(Z_RES_P(filehandle));
11    RETURN_TRUE;
12   }

1.5 編譯安裝以及測試

1.5.1 編譯安裝

經過上面的編碼,一個簡單的第三方的擴展就實現了。查看完整版連接描述
下面的一些命令配置請根據本身的環境而定(安裝的過程能夠參考最基礎的擴展開發教程連接描述

1 [root@localhost tipi_file]# php7ize
2 Configuring for:
3 PHP Api Version: 20151012
4 Zend Module Api No: 20151012
5 Zend Extension Api No: 320151012
6 [root@localhost tipi_file]# ./configure --with-php-config=/usr/local/php7/bin/php-config
7 ...
8 [root@localhost tipi_file]# make
9 ...
10 [root@localhost tipi_file]# make install
11 ...

1.5.2 測試

直接用 php 腳本測試,就不一個功能一個功能寫測試樣例了,修改tipi_file.php文件。
1 $fp = file_open("./CREDITS","r+");
2 var_dump($fp);
3 var_dump(file_read($fp,6));
4 var_dump(file_write($fp,"zhoumengakng"));
5 var_dump(file_close($fp));

而後經過命令行執行

1 php7 -d"extension=tipi_file.so" tipi_file.php

2 源碼分析

2.1 註冊資源類型源碼

1 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)
  2  {
  3 zend_rsrc_list_dtors_entry *lde;
  4 zval zv;
  5
  6 lde = malloc(sizeof(zend_rsrc_list_dtors_entry));
  7 lde->list_dtor_ex = ld;
  8 lde->plist_dtor_ex = pld;
  9 lde->module_number = module_number;
  10 lde->resource_id = list_destructors.nNextFreeElement;
  11 lde->type_name = type_name;
  12 ZVAL_PTR(&zv, lde);
  13
  14 if (zend_hash_next_index_insert(&list_destructors, &zv) == NULL) {
  15   return FAILURE;
  16 }
  17 return list_destructors.nNextFreeElement-1;
  18   }

其中

1  ZVAL_PTR(&zv, lde);

等價於

1 zv.value.ptr = (lde);
 2 zv.u1.type_info = IS_PTR;

list_destructors是一個全局靜態HashTable,資源類型註冊時,將一個zval結構體變量zv存放入list_destructors的arData中,而zv的value.ptr卻指向了zend_rsrc_list_dtors_entry *lde,lde中包含的該種資源釋放函數指針、持久資源的釋放函數指針,資源類型名稱,該資源在 hashtable 中的索引依據 (resource_id)等。

而這裏的resource_id則是該函數的返回值,因此後面咱們在解析該類型變量時,都須要將resource_id帶上。

整個的註冊步驟能夠總結爲下圖:
圖片描述
2.2 資源的註冊

1 ZEND_API zend_resource* zend_register_resource(void *rsrc_pointer, int rsrc_type)
 2  {
 3  zval *zv;
 4
 5  zv = zend_list_insert(rsrc_pointer, rsrc_type);
 6
 7  return Z_RES_P(zv);
 8   }

該函數的功能則是將zend_list_insert返回的zval中的資源指針返回。Z_RES_P宏在Zend/zend_types.h中定義。
重點分析zend_list_insert

1  ZEND_API zval *zend_list_insert(void *ptr, int type)
    2   {
    3   int index;
    4    zval zv;
    5
    6   index = zend_hash_next_free_element(&EG(regular_list));
    7   if (index == 0) {
    8  index = 1;
    9   }
    10   ZVAL_NEW_RES(&zv, index, ptr, type);
    11   return zend_hash_index_add_new(&EG(regular_list), index, &zv);
    12   }

其中zend_hash_next_free_element宏,返回&EG(regular_list)表的nNextFreeElement,後面用來做爲索引查詢的依據。

而ZVAL_NEW_RES宏是 PHP 7 新增的一套東西,把一個資源裝載到zval裏去,由於PHP 7 中Bucket只能存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)

代碼比較清晰,首先根據h,p,t新建了一個資源,而後一塊兒存入了z這個zval的結構體。(最後兩個宏前面剛剛討論過了)

最後就是zend_hash_index_add_new宏了,追蹤代碼發現其最後等價於調用的是

_zend_hash_index_add_or_update_i(&EG(regular_list), index, &zv, HASH_ADD | HASH_ADD_NEW ZEND_FILE_LINE_RELAY_CC)

關於HashTable的具體操做,這裏暫不作細緻的分析,後面單獨再單獨說
圖片描述
2.3 解析資源源碼分析
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;
}

在上面的例子中咱們是這樣解析的

(FILE *)zend_fetch_resource(Z_RES_P(filehandle), TIPI_FILE_TYPE, le_tipi_file)

圖片描述
2.4 刪除資源源碼分析

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;
}
與PHP5 不一樣的地方,這裏不是每次都進來將其引用計數減一操做,而是直接調用zend_resource_dtor函數。

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);
   }
}
若是引用計數已經等於0或者小於0了,那麼才從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;

}
}
原理圖仍是引用上面的註冊資源類型、並註冊資源的圖:
圖片描述

先從zend_resource逆向經過其type在list_destructors中索引層層關聯,找到該類資源的釋放回調函數,而後對該資源執行釋放回調函數。

然後面的從EG(regular_list)中刪除,則是經過res->handler作爲索引的依據。

相關文章
相關標籤/搜索