# 9.1 PHP中的資源類型
<div class="tip-common">
講述之前,先描述下{資源}類型在內核中的結構:
</div>
````c
//每一個資源都是通過它來實現的。
typedef struct _zend_rsrc_list_entry
{
void *ptr;
int type;
int refcount;
}zend_rsrc_list_entry;
````
在真實世界中,我們經常需要操作一些不好用標量值表現的數據,比如某個文件的句柄,而對于C來說,它也僅僅是個指針而已。
````c
#include <stdio.h>
int main(void)
{
FILE *fd;
fd = fopen("/home/jdoe/.plan", "r");
fclose(fd);
return 0;
}
````
C語言中stdio的文件描述符(file descriptor)是與每個打開的文件相匹配的一個變量,它實際上是一個FILE類型的指針,它將在程序與硬件交互通訊時使用。我們可以使用fopen函數來打開一個文件獲取句柄,之后只需把這個句柄傳遞給feof()、fread()、fwrite()、fclose()之類的函數,便可以對這個文件進行后續操作了。既然這個數據在C語言中就無法直接用標量數據來表示,那我們如何對其進行封裝才能保證用戶在PHP語言中也能使用到它呢?這便是PHP中資源類型變量的作用了!所以它也是通過一個zval結構來進行封裝的。
資源類型的實現并不復雜,它的值其實僅僅是一個整數,內核將根據這個整數值去一個類似資源池的地方尋找最終需要的數據。
### 資源類型變量的使用
資源類型的變量在實現中也是有類型區分的!為了區分不同類型的資源,比如一個是文件句柄,一個是mysql鏈接,我們需要為其賦予不同的分類名稱。首先,我們需要先把這個分類添加到程序中去。這一步的操作可以在MINIT中來做:
````c
#define PHP_SAMPLE_DESCRIPTOR_RES_NAME "山寨文件描述符"
static int le_sample_descriptor;
ZEND_MINIT_FUNCTION(sample)
{
le_sample_descriptor = zend_register_list_destructors_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, module_number);
ZEND_API int zend_register_list_destructors(void (*ld)(void *), void (*pld)(void *), int module_number);
ZEND_API int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, char *type_name, int module_number);
````
接下來,我們把定義好的MINIT階段的函數添加到擴展的module_entry里去,只需要把原來的"NULL, /* MINIT */"一行替換掉即可:
````c
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 opportunity to register destructor functionsthe NULL values, which you'll change soon enoughfor a resource type that will be thereafter known by a unique integer ID.
看到zend_register_list_destructors_ex()函數,你肯定會想是不是也存在一個zend_register_list_destructors()函數呢?是的,確實有這么一個函數,它的參數中比前者少了資源類別的名稱。那這兩這的區別在哪呢?
````php
eaco $re_1;
//resource(4) of type (山寨版File句柄)
echo $re_2;
//resource(4) of type (Unknown)
````
### 創建資源
我們在上面向內核中注冊了一種新的資源類型,下一步便可以創建這種類型的資源變量了。接下來讓我們簡單的重新實現一個fopen函數,現在叫sample_open:
````c
PHP_FUNCTION(sample_fopen)
{
FILE *fp;
char *filename, *mode;
int filename_len, mode_len;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",&filename, &filename_len,&mode, &mode_len) == FAILURE)
{
RETURN_NULL();
}
if (!filename_len || !mode_len)
{
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, E_WARNING,"Unable to open %s using mode %s",filename, mode);
RETURN_FALSE;
}
//將fp添加到資源池中去,并標記它為le_sample_descriptor類型的。
ZEND_REGISTER_RESOURCE(return_value,fp,le_sample_descriptor);
}
````
如果前面章節的知識你都看過的話,應該可以猜出最后一行代碼是干啥的了。它創建了一個新的le_sample_descriptor類型的資源,此資源的值是fp,另外它把這個資源加入到一個存儲資源的HashTable中,并把此資源在其中對應的數字Key賦給return_value。
<div class="tip-common">
資源并不局限于文件句柄,我們可以申請一塊內存,并且指向它的指針來作為一種資源。所以資源可以對應任意類型的數據。
</div>
### 銷毀資源
世間萬物皆有喜有悲,有生有滅,到了我們探討如何銷毀資源的時候了。最簡單的一種莫過于仿照fclose寫一個sample_close()函數,在它里面實現對某種{資源:專指PHP的資源類型變量代表的值}的釋放。
但是,如果用戶端的腳本通過unset()函數來釋放某個資源類型的變量會如何呢?它們可不知道它的值最終對應一個FILE*指針啊,所以也無法使用fclose()函數來釋放它,這個FILE*句柄很有可能會一直存在于內存中,直到PHP程序掛掉,由OS來回收。但在一個平常的Web環境中,我們的服務器都會長時間運行的。
難道就沒有解決方案了嗎?當然不是,謎底就在那個NULL參數里,就是我們在上面為了生成新的資源類型,調用的zend_register_list_destructors_ex()函數的第一個參數和第二個參數。這兩個參數都各自代表一個回調參數。第一個回調函數會在腳本中的相應類型的資源變量被釋放掉的時候觸發,比如作用域結束了,或者被unset()掉了。
第二個回調函數則是用在一個類似于長鏈接類型的資源上的,也就是這個資源創建后會一直存在于內存中,而不會在request結束后被釋放掉。它將會在Web服務器進程終止時調用,相當于在MSHUTDOWN階段被內核調用。有關persistent resources的事宜,我們將在下一節里詳述。
我們先來定義第一種回調函數。
````c
static void php_sample_descriptor_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
FILE *fp = (FILE*)rsrc->ptr;
fclose(fp);
}
````
然后用它替換掉zend_register_list_destructors_ex()函數的第一個參數NULL:
````c
le_sample_descriptor = zend_register_list_destructors_ex(
php_sample_descriptor_dtor,
NULL,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
module_number);
````
現在,如果腳本中得到了一個上述類型的資源變量,當它被unset的時候,或者因為作用域執行完被內核釋放掉的時候都會被內核調用底層的php_sample_descriptor_dtor來預處理它。這樣一來,貌似我們根本就不需要sample_close()函數了!
````php
<?php
$fp = sample_fopen("/home/jdoe/notes.txt", "r");
unset($fp);
?>
````
unset($fp)執行后,內核會自動的調用php_sample_descriptor_dtor函數來清理這個變量對應的一些數據。
當然,事情絕對沒有這么簡單,讓我們先記住這個疑問,繼續往下看。
### Decoding Resources
我們把資源變量比作書簽,可如果僅有書簽的話絕對沒有任何作用啊!我們需要通過書簽找到相應的頁才行。對于資源變量,我們必須能夠通過它找到相應的最終數據才行!
````c
ZEND_FUNCTION(sample_fwrite)
{
FILE *fp;
zval *file_resource;
char *data;
int data_len;
if (zend_parse_parameters(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);
/* Write the data, and
* return the number of bytes which were
* successfully written to the file */
RETURN_LONG(fwrite(data, 1, data_len, fp));
}
````
zend_parse_parameters()函數中的r占位符代表著接收資源類型的變量,它的載體是一個zval*。然后讓我們看一下ZEND_FETCH_RESOURCE()宏函數。
````c
#define ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id,default_id, resource_type_name, resource_type)
rsrc = (rsrc_type) zend_fetch_resource(passed_id TSRMLS_CC,default_id, resource_type_name, NULL,1, resource_type);
ZEND_VERIFY_RESOURCE(rsrc);
//在我們的例子中,它是這樣的:
fp = (FILE*) zend_fetch_resource(&file_descriptor TSRMLS_CC, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, NULL,1, le_sample_descriptor);
if (!fp)
{
RETURN_FALSE;
}
````
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()向其中寫入點數據吧!。<p>
<div class="tip-common">
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 fail silently instead.
</div>
我們也可以通過另一種方法來獲取我們最終想要的數據。
````c
ZEND_FUNCTION(sample_fwrite)
{
FILE *fp;
zval *file_resource;
char *data;
int data_len, rsrc_type;
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(file_resource),&rsrc_type);
if (!fp || rsrc_type != le_sample_descriptor) {
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Invalid resource provided");
RETURN_FALSE;
}
RETURN_LONG(fwrite(data, 1, data_len, fp));
}
````
可以根據自己習慣來選擇到底使用哪一種形式,不過推薦使用ZEND_FETCH_RESOURCE()宏函數。
### Forcing Destruction
在上面我們還有個疑問沒有解決,就是類似于我們上面實現的unset($fp)真的是萬能的么?當然不是,看一下下面的代碼:
````php
<?php
$fp = sample_fopen("/home/jdoe/world_domination.log", "a");
$evil_log = $fp;
unset($fp);
?>
````
這次,$fp和$evil_log共用一個zval,雖然$fp被釋放了,但是它的zval并不會被釋放,因為$evil_log還在用著。也就是說,現在$evil_log代表的文件句柄仍然是可以寫入的!所以為了避免這種錯誤,真的需要我們手動來close it!sample_close()函數是必須存在的!
````c
PHP_FUNCTION(sample_fclose)
{
FILE *fp;
zval *file_resource;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r",&file_resource) == FAILURE ) {
RETURN_NULL();
}
/* While it's not necessary to actually fetch the
* FILE* resource, performing the fetch provides
* an opportunity to verify that we are closing
* the correct resource type. */
ZEND_FETCH_RESOURCE(fp, FILE*, &file_resource, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);
/* Force the resource into self-destruct mode */
zend_hash_index_del(&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()函數時的第一個參數。
<div class="tip-common">在很多地方,我們都會看到一個專門用來刪除的zend_list_delete()宏函數,因為它考慮了資源數據自己的引用計數,所以我們將在后面的章節中介紹它。</div>
## links
* 9 [PHP中的資源類型](<9.md>)
* 9.2 [Persistent Resources](<9.2.md>)
- about
- 開始閱讀
- 目錄
- 1 PHP的生命周期
- 1.讓我們從SAPI開始
- 2.PHP的啟動與終止
- 3.PHP的生命周期
- 4.線程安全
- 5.小結
- 2 PHP變量在內核中的實現
- 1. 變量的類型
- 2. 變量的值
- 3. 創建PHP變量
- 4. 變量的存儲方式
- 5. 變量的檢索
- 6. 類型轉換
- 7. 小結
- 3 內存管理
- 1. 內存管理
- 2. 引用計數
- 3. 總結
- 4 動手編譯PHP
- 1. 編譯前的準備
- 2. PHP編譯前的config配置
- 3. Unix/Linux平臺下的編譯
- 4. 在Win32平臺上編譯PHP
- 5. 小結
- 5 Your First Extension
- 1. 一個擴展的基本結構
- 2. 編譯我們的擴展
- 3. 靜態編譯
- 4. 編寫函數
- 5. 小結
- 6 函數返回值
- 1. 一個特殊的參數:return_value
- 2. 引用與函數的執行結果
- 3. 小結
- 7 函數的參數
- 1. zend_parse_parameters
- 2. Arg Info 與類型綁定
- 3. 小結
- 8 使用HashTable與{數組}
- 1. 數組(C中的)與鏈表
- 2. 操作HashTable的API
- 3. 在內核中操作PHP語言中數組
- 4. 小結
- 9 PHP中的資源類型
- 1. 復合類型的數據——{資源}
- 2. Persistent Resources
- 3. {資源}自有的引用計數
- 4. 小結
- 10 PHP中的面向對象(一)
- 1. zend_class_entry
- 2. 定義一個類
- 3. 定義一個接口
- 4. 類的繼承與接口的實現
- 5. 小結
- 11 PHP中的面向對象(二)
- 1. 生成對象的實例與調用方法
- 2. 讀寫對象的屬性
- 3. 小結
- 12 啟動與終止的那點事
- 2. 小結
- 1. 關于生命周期
- 2. MINFO與phpinfo
- 3. 常量
- 4. PHP擴展中的全局變量
- 5. PHP語言中的超級全局變量
- 6. 小結
- 13 INI設置
- 1. 聲明和訪問ini設置
- 2. 小結
- 2. 小結
- 14 流式訪問
- 1. 概覽
- 2. 打開流
- 3. 訪問流
- 4. 靜態資源操作
- 5. 小結
- 15 流的實現
- 1. php流的表象之下
- 2. 包裝器操作
- 3. 實現一個包裝器
- 4. 操縱
- 5. 檢查
- 6. 小結
- 16 有趣的流
- 1. 上下文
- 2. 過濾器
- 3. 小結
- 17 配置和鏈接
- 1. autoconf
- 2. 庫的查找
- 3. 強制模塊依賴
- 4. Windows方言
- 5. 小結
- 18 擴展生成
- 1. ext_skel
- 2. PECL_Gen
- 3. 小結
- 19 設置宿主環境
- 1. 嵌入式SAPI
- 2. 構建并編譯一個宿主應用
- 3. 通過嵌入包裝重新創建cli
- 4. 老技術新用
- 5. 小結
- 20 高級嵌入式
- 1. 回調到php中
- 2. 錯誤處理
- 3. 初始化php
- 4. 覆寫INI_SYSTEM和INI_PERDIR選項
- 5. 捕獲輸出
- 6. 同時擴展和嵌入
- 7. 小結
- 約定