先描述下{資源}類型在內核中的結構:
//每一個資源都是透過它來實現的。
typedef struct _zend_rsrc_list_entry
{
void *ptr;
int type;
int refcount int type;
int refcount ,例如某個文件的句柄,而對C來說,它也只是個指針而已。
#include
{
FILE *fd;
fd = fopen("/home/jdoe/.plan", "r"); ;
}
C語言中stdio的檔案描述子(file descriptor)是與每個開啟的檔案相符的一個變量,它實際上十一個FILE類型的指針,它將在程式與硬體互動通訊時使用。我們可以使用fopen函數來開啟一個檔案取得句柄,之後只要把這個句柄傳遞給feof()、fread()、fwrite()、fclose()之類的函數,便可以對這個檔案進行後續操作了。既然這個數據在C語言中就無法直接用標量數據來表示,那我們如何對其進行封裝才能保證用戶在PHP語言中也能使用到它呢?這便是PHP中資源類型變數的作用了!所以它也是透過一個zval結構來進行封裝的。 資源類型的實作並不複雜,它的值其實只是一個整數,核心將根據這個整數值去一個類似資源池的地方尋找最終需要的資料。
資源類型變數的使用
資源類型的變數在實作上也是有類型區分的!為了區分不同類型的資源,例如一個是文件句柄,一個是mysql鏈接,我們需要為其賦予不同的分類名稱。首先,我們要先把這個分類加到程式中去。這一步驟的操作可以在MINIT中來做:
#define PHP_SAMPLE_DESCRIPTOR_RES_NAME "山寨文件描述符"
static int le_sample_descriptor;
ZEND_MINIT_FUNCTION(sample)list_descriptor;
ZEND_MINIT_FUNC_saat) ex(NULL, NULL, PHP_SAMPLE_DESCRIPTOR_RES_NAME,module_number);
return SUCCESS ;
}
//附加資料
#define register_list_destructors(ld, pld) zend_register_list_destructors((void (*)(void *))ld, (void (*)(void))pld, modn zend_register_list_destructors(void (*ld)(void *), void (*pld)(void *), int module_number);
ZEND_API int zend_register_list_destructors_ex(rsrc_dtor_func_API int zend_register_list_destructors_ex(rsrc_dtor_func_t intld, _cunc_t int.接下來,我們把定義好的MINIT階段的函數加到擴充的module_entry裡去,只要把原來的"NULL, /* MINIT */"一行替換掉即可:
ZEND_MINIT(sample), /* MINIT */
ZEND_MINIT_FUNCTION( )巨集用來幫助我們定義MINIT階段的函數,這我們已經在第一章裡描述過了,但將會在第12章和第三章有更詳細的描述。 What's important to know at this juncture is that the MINIT method is executed once when your extension is first loaded and before any requests have been received. Here you've used that opportor solwkwhewion solf functions sed that opportorm. a resource type that will be thereafter known by a unique integer ID. 看到zend_register_list_destructors_ex()函數,你一定會想是不是也存在一個zend_register_list_destructors()函數呢?是的,確實有這麼一個函數,它的參數中比前者少了資源類別的名稱。那這兩這的差別在哪呢?
eaco $re_1;
//resource(4) of type (山寨版File句柄)
echo $re_2;
//resource(4) of type (Unknown)
創建資源 一種新的資源類型,下一步便可以建立這種類型的資源變數了。接下來讓我們簡單的重新實作一個fopen函數,現在稱為sample_open:
PHP_FUNCTION(sample_fopen)
{
FILE *fp;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",&filename, &filename_len,&mode, &mode_len) == FAILURE)
{
RETURN_NULL();
{
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Invalid filename or mode length");
RETURN_FALSE;
}
fp = fopen(filename, mode);
if (!fp)
{
php_error_docref(NULL TSRMLS_CC, Eode.com,"able php_error_docref(NULL TSRMLS_CC, Eode_name, openable, com RETURN_FALSE;
}
//將fp加入資源池中去,並標記它為le_sample_descriptor類型的。
ZEND_REGISTER_RESOURCE(return_value,fp,le_sample_descriptor);
}
如果前面章節的知識你都看過的話,應該可以猜出最後一行程式碼是乾啥的了。它創建了一個新的le_sample_descriptor類型的資源,此資源的值是fp,另外它把這個資源加入到一個儲存資源的HashTable中,並把此資源在其中對應的數字Key賦給return_value。
資源並不局限於文件句柄,我們可以申請一塊內存,並它指向它的指針來作為一種資源。所以資源可以對應任意類型的資料。
銷毀資源
世間萬物皆有喜有悲,有生有滅,到了我們探討如何銷毀資源的時候了。最簡單的一種莫過於仿照fclose寫一個sample_close()函數,在它裡面實現對某種{資源:專指PHP的資源類型變數所代表的值}的釋放。
但是,如果使用者端的腳本透過unset()函數來釋放某個資源類型的變數會如何呢?它們可不知道它的值最終對應一個FILE指標啊,所以也無法使用fclose()函數來釋放它,這個FILE句柄很有可能會一直存在於記憶體中,直到PHP程式掛掉,由OS來回收。但在一個平常的Web環境中,我們的伺服器都會長時間運作的。 難道就沒有解決方案了嗎?當然不是,謎底就在那個NULL參數裡,就是我們在上面為了產生新的資源型別,所呼叫的zend_register_list_destructors_ex()函式的第一個參數和第二個參數。這兩個參數都各自代表一個回呼參數。第一個回呼函數會在腳本中對應的類型的資源變數被釋放掉的時候觸發,例如作用域結束了,或是被unset()掉了。
第二個回呼函數則是用在一個類似長連結類型的資源上的,也就是這個資源創建後會一直存在於記憶體中,而不會在request結束後被釋放掉。它將在Web伺服器進程終止時調用,相當於在MSHUTDOWN階段被核心調用。有關persistent resources的事宜,我們將在下一節中詳述。
我們先定義第一種回呼函數。
static void php_sample_descriptor_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
FILE *fp = (FILE*)rs _list_destructors_ex()函數的第一個參數NULL :
le_sample_descriptor = zend_register_list_destructors_ex(
php_sample_descriptor_dtor,
module_number);
現在,如果腳本中得到了一個上述類型的資源變量,當它被unset的時候,或者因為作用域執行完被內核釋放掉的時候都會被核心呼叫底層的php_sample_descriptor_dtor來預處理它。這樣一來,似乎我們根本就不需要sample_close()函數了!
$fp = sample_fopen("/home/jdoe/notes.txt", "r");
unset($fp);
?>
unset($fp)執行後,核心會自動執行後,核心會的呼叫php_sample_descriptor_dtor函數來清理這個變數對應的一些資料。 當然,事情絕對沒有這麼簡單,讓我們先記住這個疑問,繼續往下看。
Decoding Resources
我們把資源變數比喻為書籤,但如果僅有書籤的話絕對沒有任何作用啊!我們需要透過書籤找到對應的頁才行。對於資源變量,我們必須能夠透過它找到相應的最終數據才行!
ZEND_FUNCTION(sample_fwrite)
{
FILE *fp;
zval *file_resource;
meters(ZEND_NUM_ARGS() TSRMLS_CC, "rs",&file_resource, &data, &data_len) == FAILURE )
{
RETURN_NULL();
}
/* Use the zval* to verify the resource type and
* retrieve its pointer from the lookup table */
ZEND_FETCH_RESOURCE(fp,FILE*,&file_resource,-1,PHP_SAMPLE_DESCRIPTOR_RES_NAME,le_sample_descriptor);
* successfully written to the file */
RETURN_LONG(fwrite(data , 1, data_len, fp));
}
zend_parse_parameters()函數中的r佔位符代表接收資源類型的變量,它的載體是一個zval*。然後讓我們來看看ZEND_FETCH_RESOURCE()宏函數。
#define ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id,default_id, resource_type_name, resource_type)
rsrc = (rsrc_type) zend_fetch_resource(Wdefed_id TSRM_sc. ZEND_VERIFY_RESOURCE(rsrc);
//在在我們的例子中,它是這樣的:
fp = (FILE*) zend_fetch_resource(&file_descriptor TSRMLS_CC, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, NULL,1, le_sample_descriptor);
}
zend_fetch_resource( )是zend_hash_find()的一層封裝,它使用一個數字key去一個保存各種{資源}的HashTable中尋找最終需要的數據,找到之後,我們用ZEND_VERIFY_RESOURCE()宏函數校驗一下這個數據。從上面的程式碼我們可以看出,NULL、0是絕對不能作為一種資源的。 在上述的範例中,zend_fetch_resource()函數先取得le_sample_descriptor代表的資源類型,如果資源不存在或接收的zval不是一個資源類型的變數,它就會傳回NULL,並拋出對應的錯誤訊息。 最後的ZEND_VERIFY_RESOURCE()巨集函數如果偵測到錯誤,便會自動傳回,使我們可以從錯誤偵測中脫離出來,更專注於程式的主邏輯。現在我們已經取得到對應的FILE*了,下面就用fwrite()向其中寫入點資料吧了! 。
To avoid having zend_fetch_resource() generate an error on failure, simply pass NULL for the resource_type_name parameter. Without a meaningful error message to display, zend_fetch_resource() will failsource(想要的數據。
ZEND_FUNCTION(sample_fwrite)
{
FILE *fp;
zval *file_resource; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs",&file_resource, &data, &data_len) = = FAILURE ) {
RETURN_NULL();
}
fp = (FILE*)zend_list_find(Z_RESVAL_P( != le_sample_descriptor) {
php_error_docref(NULL TSRMLS_CC, E_WARNING," Invalid resource provided");
RETURN_FALSE;
}
RETURN_LONG(fwrite(data, 1, data_len,FEENDp); _RESOURCE()巨集函數。
Forcing Destruction
在上面我們還有個疑問沒有解決,就是類似於我們上面實現的unset($fp)真的是萬能的麼?當然不是,請看下面的程式碼:
$fp = sample_fopen("/home/jdoe/world_domination.log", "a");
$ ?>
這次,$fp和$evil_log共用一個zval,雖然$fp被釋放了,但是它的zval並不會被釋放,因為$evil_log還在用著。也就是說,現在$evil_log代表的檔案句柄仍然是可以寫入的!所以為了避免這種錯誤,真的需要我們手動來close it! sample_close()函數是必須存在的!
PHP_FUNCTION(sample_fclose)
{
FILE *fp;
zval *file_resource;
¡ ) {
RETURN_NULL();
}
/* While it's not necessary to actually fetch the
* FILE* resource, performing the fetch provides * an opportunity to verify that we are clo案ZEND_FETCH_RESOURCE(fp, FILE*, &file_resource, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);
&EG(regular_list),Z_RESVAL_P(file_resource));
RETURN_TRUE;
}
這個刪除操作也再次說明了資源資料是保存在HashTable中的。雖然我們可以透過zend_hash_index_find()或zend_hash_next_index_insert()之類的函數來操作這個儲存資源的HashTable,但這絕不是一個好主意,因為在後續的版本中,PHP可能會修改有關這一部分的實作方式,到那時上述方法便不起作用了,所以為了更好的相容性,請使用標準的巨集函數或api函數。 當我們在EG(regular_list)這個HashTable中刪除資料的時候,回調用一個dtor函數,它根據資源變數的類別來呼叫對應的dtor函數實現,就是我們呼叫zend_register_list_destructors_ex()函數時的第一個參數。
在很多地方,我們都會看到一個專門用來刪除的zend_list_delete()巨集函數,因為它考慮了資源資料自己的參考計數。