PHP7 使用資源包裹第三方擴(kuò)展的實(shí)現(xiàn)及其源碼解讀

在閱讀下面的內(nèi)容之前,我假定已看到的人已經(jīng)對(duì) PHP 7 基本的數(shù)據(jù)結(jié)構(gòu)都有大致的了解了,這是下面內(nèi)容閱讀的前提。

我們分為兩大塊:

首先實(shí)現(xiàn)一個(gè)自定義的文件打開、讀取、寫入、關(guān)閉的文件操作擴(kuò)展;

然后分析各個(gè)操作背后的實(shí)現(xiàn)原理,其中某些部分的實(shí)現(xiàn)我會(huì)和PHP 5.3 使用資源包裹第三方擴(kuò)展源碼解讀對(duì)比分析。

0 通過原型生成擴(kuò)展骨架

首先進(jìn)入到源碼目錄的ext目錄中,添加一個(gè)文件操作的原型文件

[root@localhost?php-src-php-7.0.3]#?cd?ext/

[root@localhost?ext]#?vim?tipi_file.proto

編輯原型為

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)

[root@localhost?ext]#?./ext_skel?--extname=tipi_file?--proto=./tipi_file.proto

這樣一個(gè)簡(jiǎn)單的文件操作擴(kuò)展的代碼骨架就生成了。

完整代碼tipi_file.c(https://github.com/zhoumengkang/notes/blob/master/php-extension/php7.0/tipi_file/tipi_file.c),可以先有一個(gè)大致的了解,這樣后面閱讀時(shí),思路可能會(huì)清晰很多。

1 擴(kuò)展的實(shí)現(xiàn)

1.1?注冊(cè)資源類型

1.1.1 注冊(cè)資源 API

ZEND_APIintzend_register_list_destructors_ex(rsrc_dtor_func_t?ld,?rsrc_dtor_func_t?pld,constchar*type_name,intmodule_number)

參數(shù)解釋

ld釋放該資源時(shí)調(diào)用的函數(shù)。

pld釋放用于在不同請(qǐng)求中始終存在的永久資源的函數(shù)。

type_name是一個(gè)具有描述性類型名稱的字符串。

module_number為引擎內(nèi)部使用,當(dāng)我們調(diào)用這個(gè)函數(shù)時(shí),我們只需要傳遞一個(gè)已經(jīng)定義好的module_number變量。

該 API 返回一個(gè)資源類型 id,該id應(yīng)當(dāng)被作為全局變量保存在擴(kuò)展里,以便在必要的時(shí)候傳遞給其他資源API。

1.1.2 添加資源釋放回調(diào)函數(shù)

staticvoidtipi_file_dtor(zend_resource?*rsrc?TSRMLS_DC){

FILE*fp?=?(FILE*)?rsrc->ptr;

fclose(fp);

}

我們發(fā)現(xiàn)該函數(shù)的參數(shù)類型是zend_resource。這是 PHP7 新增的數(shù)據(jù)結(jié)構(gòu),在 PHP 5 則是zend_rsrc_list_entry。細(xì)節(jié)的內(nèi)容,我們留在后面分析。

1.1.3 在PHP_MINIT_FUNCTION中注冊(cè)

我們知道在 PHP 生命周期中,當(dāng) PHP 被裝載時(shí),PHP_MINIT_FUNCTION(模塊啟動(dòng)函數(shù))即被引擎調(diào)用。這使得引擎做一些例如資源類型,注冊(cè)INI變量等的一次初始化。

那么我們需要在這里通過zend_register_list_destructors_ex在PHP_MINIT_FUNCTION來(lái)注冊(cè)資源類型。

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

returnSUCCESS;

}

其中TIPI_FILE_TYPE在前面已經(jīng)定義了,是該擴(kuò)展的別名(具體可以對(duì)比著代碼 tipi_file.c 查看(https://github.com/zhoumengkang/notes/blob/master/php-extension/php7.0/tipi_file/tipi_file.c))

1.2 注冊(cè)資源

1.2.1 注冊(cè)資源 API

在 PHP 7 中刪除了原來(lái)的ZEND_REGISTER_RESOURCE宏,直接使用zend_register_resource函數(shù)

ZEND_API?zend_resource*?zend_register_resource(void*rsrc_pointer,intrsrc_type)

參數(shù)解釋

rsrc_pointer資源數(shù)據(jù)指針

rsrc_type注冊(cè)資源類型時(shí)獲得的資源類型 id

1.2.2 在 file_open函數(shù)中實(shí)現(xiàn)資源的注冊(cè)

PHP_FUNCTION(file_open)

{

char*filename?=?NULL;

char*mode?=?NULL;

intargc?=?ZEND_NUM_ARGS();

size_tfilename_len;

size_tmode_len;

if(zend_parse_parameters(argc?TSRMLS_CC,"ss",?&filename,?&filename_len,?&mode,?&mode_len)?==?FAILURE)

return;

//?使用?VCWD?宏取代標(biāo)準(zhǔn)?C?文件操作函數(shù)

FILE*fp?=?VCWD_FOPEN(filename,?mode);

if(fp?==?NULL)?{

RETURN_FALSE;

}

RETURN_RES(zend_register_resource(fp,?le_tipi_file));

}

其中RETURN_RES宏的作用是將返回的zend_resource添加到zval中,然后將最后的zval作為返回值。也就是說(shuō)該函數(shù)的返回值為zval指針。RETURN_RES(zend_register_resource(fp, le_tipi_file))會(huì)將返回值的value.res設(shè)為fp,u1.type_info設(shè)為IS_RESOURCE_EX。大家可以根據(jù)源碼非常直觀的了解到,這里不粘貼代碼詳細(xì)說(shuō)明了。

1.3 使用資源

1.3.1 使用資源 API

ZEND_APIvoid*zend_fetch_resource(zend_resource?*res,constchar*resource_type_name,intresource_type)

在 PHP 7 中刪除了原有的ZEND_FETCH_RESOURCE宏,直接使用函數(shù)zend_fetch_resource,而且解析方式也變得簡(jiǎn)單了很多,想比 PHP 5 要高效很多,后面我們?cè)偻ㄟ^圖片分析對(duì)比。

參數(shù)含義

res資源指針

resource_type_name該類資源的字符串別名

resource_type該類資源的類型 id

1.3.2 解析資源的實(shí)現(xiàn)

當(dāng)我們要實(shí)現(xiàn)文件的讀取時(shí),最終還是需要使用原生的fread函數(shù),所以這里需要通過zend_fetch_resource將zend_resource解析成為該資源包裹的原始的FILE *的指針。

PHP_FUNCTION(file_read)

{

intargc?=?ZEND_NUM_ARGS();

intfilehandle_id?=?-1;

zend_long?size;

zval?*filehandle?=?NULL;

FILE*fp?=?NULL;

char*result;

size_tbytes_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);

}

這里需要說(shuō)明,腳本自動(dòng)生成的擴(kuò)展代碼中還是使用ZEND_FETCH_RESOURCE, 是個(gè) BUG,因?yàn)樽詣?dòng)生成的腳本(ext/skeleton/create_stubs)還沒更新。

與之類似的文件的寫入操作,也很類似,這里就復(fù)制代碼了,請(qǐng)查看完整的代碼 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_APIintzend_list_close(zend_resource?*res)

傳入需要被刪除的資源即可。該 API 看似非常簡(jiǎn)單,實(shí)際做了很多工作,后面原理分析細(xì)說(shuō)。

1.4.2 資源刪除的實(shí)現(xiàn)

我們?cè)诤瘮?shù)file_close中需要調(diào)用資源刪除 API

PHP_FUNCTION(file_close)

{

intargc?=?ZEND_NUM_ARGS();

intfilehandle_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 編譯安裝以及測(cè)試

1.5.1 編譯安裝

通過上面的編碼,一個(gè)簡(jiǎn)單的第三方的擴(kuò)展就實(shí)現(xiàn)了。查看完整版(https://github.com/zhoumengkang/notes/tree/master/php-extension/php7.0/tipi_file)

下面的一些命令配置請(qǐng)根據(jù)自己的環(huán)境而定(安裝的過程可以參考最基礎(chǔ)的擴(kuò)展開發(fā)教程(https://mengkang.net/660.html))

[root@localhost?tipi_file]#?php7ize

Configuringfor:

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 測(cè)試

直接用 php 腳本測(cè)試,就不一個(gè)功能一個(gè)功能寫測(cè)試樣例了,修改tipi_file.php文件。

$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));

然后通過命令行執(zhí)行

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

2 源碼分析

2.1 注冊(cè)資源類型源碼

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)?{

returnFAILURE;

}

returnlist_destructors.nNextFreeElement-1;

}

其中

ZVAL_PTR(&zv,?lde);

等價(jià)于

zv.value.ptr?=?(lde);

zv.u1.type_info?=?IS_PTR;

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

而這里的resource_id則是該函數(shù)的返回值,所以后面我們?cè)诮馕鲈擃愋妥兞繒r(shí),都需要將resource_id帶上。

整個(gè)的注冊(cè)步驟可以總結(jié)為下圖:

2.2 資源的注冊(cè)

ZEND_API?zend_resource*?zend_register_resource(void*rsrc_pointer,intrsrc_type)

{

zval?*zv;

zv?=?zend_list_insert(rsrc_pointer,?rsrc_type);

returnZ_RES_P(zv);

}

該函數(shù)的功能則是將zend_list_insert返回的zval中的資源指針返回。Z_RES_P宏在Zend/zend_types.h中定義。

重點(diǎn)分析zend_list_insert

ZEND_API?zval?*zend_list_insert(void?*ptr,?inttype)

{

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

returnzend_hash_index_add_new(&EG(regular_list),?index,?&zv);

}

其中zend_hash_next_free_element宏,返回&EG(regular_list)表的nNextFreeElement,后面用來(lái)作為索引查詢的依據(jù)。

而ZVAL_NEW_RES宏是 PHP 7 新增的一套東西,把一個(gè)資源裝載到zval里去,因?yàn)镻HP 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)

代碼比較清晰,首先根據(jù)h,p,t新建了一個(gè)資源,然后一起存入了z這個(gè)zval的結(jié)構(gòu)體。(最后兩個(gè)宏前面剛剛討論過了)

最后就是zend_hash_index_add_new宏了,追蹤代碼發(fā)現(xiàn)其最后等價(jià)于調(diào)用的是

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

關(guān)于HashTable的具體操作,這里暫不做細(xì)致的分析,后面單獨(dú)再單獨(dú)說(shuō)。

2.3 解析資源源碼分析

ZEND_APIvoid*zend_fetch_resource(zend_resource?*res,constchar*resource_type_name,intresource_type)

{

if(resource_type?==?res->type)?{

returnres->ptr;

}

if(resource_type_name)?{

constchar*space;

constchar*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);

}

returnNULL;

}

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

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

而現(xiàn)在 PHP7的解析則直接從zval里解析出zend_resource,如下圖所示:

2.4 刪除資源源碼分析

ZEND_APIintzend_list_close(zend_resource?*res)

{

if(GC_REFCOUNT(res)?<=?0)?{

returnzend_list_free(res);

}elseif(res->type?>=?0)?{

zend_resource_dtor(res);

}

returnSUCCESS;

}

與PHP5 不同的地方,這里不是每次都進(jìn)來(lái)將其引用計(jì)數(shù)減一操作,而是直接調(diào)用zend_resource_dtor函數(shù)。

staticvoidzend_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);

}

}

如果引用計(jì)數(shù)已經(jīng)等于0或者小于0了,那么才從EG(regular_list)中刪除

ZEND_APIintzend_list_free(zend_resource?*res)

{

if(GC_REFCOUNT(res)?<=?0)?{

returnzend_hash_index_del(&EG(regular_list),?res->handle);

}else{

returnSUCCESS;

}

}

原理圖還是引用上面的注冊(cè)資源類型、并注冊(cè)資源的圖:

先從zend_resource逆向通過其type在list_destructors中索引層層關(guān)聯(lián),找到該類資源的釋放回調(diào)函數(shù),然后對(duì)該資源執(zhí)行釋放回調(diào)函數(shù)。

而后面的從EG(regular_list)中刪除,則是通過res->handler做為索引的依據(jù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容