在閱讀下面的內容以前,咱們假定你已經對 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 註冊資源 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 ...
直接用 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.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作爲索引的依據。