天天看點

php包裹第三方的擴充

如何編寫PHP擴充(2) 2008年06月02日 星期一 13:10

包裹第三方的擴充

本節中你将學到如何編寫更有用和更完善的擴充。該節的擴充包裹了一個C庫,展示了如何編寫一個含有多個互相依賴的PHP函數擴充。

動機 也許最常見的PHP擴充是那些包裹第三方C庫的擴充。這些擴充包括MySQL或Oracle的資料庫服務庫,libxml2的 XML技術庫,ImageMagick 或GD的圖形操縱庫。

在本節中,我們編寫一個擴充,同樣使用腳本來生成骨架擴充,因為這能節省許多工作量。這個擴充包裹了标準C函數fopen(), fclose(), fread(), fwrite()和 feof().

擴充使用一個被叫做資源的抽象資料類型,用于代表已打開的檔案FILE*。你會注意到大多數處理比如資料庫連接配接、檔案句柄等的PHP擴充使用了資源類型,這是因為引擎自己無法直接“了解”它們。我們計劃在PHP擴充中實作的C API清單如下:

FILE *fopen(const char *path, const char *mode);

int fclose(FILE *stream);

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

int feof(FILE *stream);

我 們實作這些函數,使它們在命名習慣和簡單性上符合PHP腳本。如果你曾經向PHP社群貢獻過代碼,你被期望遵循一些公共習俗,而不是跟随C庫裡的API。 并不是所有的習俗都寫在PHP代碼樹的CODING_STANDARDS檔案裡。這即是說,此功能已經從PHP發展的很早階段即被包含在PHP中,并且與 C庫API類似。PHP安裝已經支援fopen(), fclose()和更多的PHP函數。

以下是PHP風格的API:

resource file_open(string filename, string mode)

file_open()接收兩個字元串(檔案名和模式),傳回一個檔案的資源句柄。

bool file_close(resource filehandle)

file_close()接收一個資源句柄,傳回真/假訓示是否操作成功。

string file_read(resource filehandle, int size)

file_read()接收一個資源句柄和讀入的總位元組數,傳回讀入的字元串。

bool file_write(resource filehandle, string buffer)

file_write接收一個資源句柄和被寫入的字元串,傳回真/假訓示是否操作成功。

bool file_eof(resource filehandle)

file_eof()接收一個資源句柄,傳回真/假訓示是否到達檔案的尾部。

是以,我們的函數定義檔案——儲存為ext/目錄下的myfile.def——内容如下:

resource file_open(string filename, string mode)

bool file_close(resource filehandle)

string file_read(resource filehandle, int size)

bool file_write(resource filehandle, string buffer)

bool file_eof(resource filehandle)

下一步,利用ext_skel腳本在ext./ 原代碼目錄執行下面的指令:

./ext_skel --extname=myfile --proto=myfile.def

然後,按照前一個例子的關于編譯建立立腳本的步驟操作。你會得到一些包含FETCH_RESOURCE()宏行的編譯錯誤,這樣骨架腳本就無法順利完成編譯。為了讓骨架擴充順利通過編譯,把那些出錯行[3]注釋掉即可。

資源 資源是一個能容納任何資訊的抽象資料結構。正如前面提到的,這個資訊通常包括例如檔案句柄、資料庫連接配接結構和其他一些複雜類型的資料。

使用資源的主要原因是因為:資源被一個集中的隊列所管理,該隊列可以在PHP開發人員沒有在腳本裡面顯式地釋放時可以自動地被釋放。

       舉個例子,考慮到編寫一個腳本,在腳本裡調用mysql_connect()打開一個MySQL連接配接,可是當該資料庫連接配接資源不再使用時卻沒有調用 mysql_close()。在PHP裡,資源機制能夠檢測什麼時候這個資源應當被釋放,然後在目前請求的結尾或通常情況下更早地釋放資源。這就為減少内 存洩漏賦予了一個“防彈”機制。如果沒有這樣一個機制,經過幾次web請求後,web伺服器也許會潛在地洩漏許多記憶體資源,進而導緻伺服器當機或出錯。

注冊資源類型 如何使用資源?Zend引擎讓使用資源變地非常容易。你要做的第一件事就是把資源注冊到引擎中去。使用這個API函數:

int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, char *type_name, int module_number)

這 個函數傳回一個資源類型id,該id應當被作為全局變量儲存在擴充裡,以便在必要的時候傳遞給其他資源API。ld:該資源釋放時調用的函數。pld用于 在不同請求中始終存在的永久資源,本章不會涉及。type_name是一個具有描述性類型名稱的字元串,module_number為引擎内部使用,當我 們調用這個函數時,我們隻需要傳遞一個已經定義好的module_number變量。

回到我們的例子中來:我們會添加下面的代碼到 myfile.c原檔案中。該檔案包括了資源釋放函數的定義,此資源函數被傳遞給 zend_register_list_destructors_ex()注冊函數(資源釋放函數應該提早添加到檔案中,以便在調用 zend_register_list_destructors_ex()時該函數已被定義):

static void myfile_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)

{

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

fclose(fp);

}

把注冊行添加到PHP_MINIT_FUNCTION()後,看起來應該如下面的代碼:

PHP_MINIT_FUNCTION(myfile)

{

le_myfile = zend_register_list_destructors_ex(myfile_dtor,NULL,"standard-c-file", module_number);

return SUCCESS;

}

l       注意到le_myfile是一個已經被ext_skel腳本定義好的全局變量。

PHP_MINIT_FUNCTION()是一個先于子產品(擴充)的啟動函數,是暴露給擴充的一部分API。下表提供可用函數簡要的說明。

函數聲明宏

函數聲明宏

語義

PHP_MINIT_FUNCTION()

當PHP被裝載時,子產品啟動函數即被引擎調用。這使得引擎做一些例如資源類型,注冊INI變量等的一次初始化。

PHP_MSHUTDOWN_FUNCTION()

當PHP完全關閉時,子產品關閉函數即被引擎調用。通常用于登出INI條目

PHP_RINIT_FUNCTION()

在每次PHP請求開始,請求前啟動函數被調用。通常用于管理請求前邏輯。

PHP_RSHUTDOWN_FUNCTION()

在每次PHP請求結束後,請求前關閉函數被調用。經常應用在清理請求前啟動函數的邏輯。

PHP_MINFO_FUNCTION()

調用phpinfo()時子產品資訊函數被呼叫,進而列印出子產品資訊。

建立和注冊新資源 我們準備實作file_open()函數。當我們打開檔案得到一個FILE *,我們需要利用資源機制注冊它。下面的主要宏實作注冊功能:

ZEND_REGISTER_RESOURCE(rsrc_result, rsrc_pointer, rsrc_type);

參考表格對宏參數的解釋

ZEND_REGISTER_RESOURCE 宏參數

宏參數

參數類型

rsrc_result

zval *, which should be set with the registered resource information.

zval * 設定為已注冊資源資訊

rsrc_pointer

Pointer to our resource data.

資源資料指針

rsrc_type

The resource id obtained when registering the resource type.

注冊資源類型時獲得的資源id

檔案函數 現在你知道了如何使用ZEND_REGISTER_RESOURCE()宏,并且準備好了開始編寫file_open()函數。還有一個主題我們需要講述。

       當PHP運作在多線程伺服器上,不能使用标準的C檔案存取函數。這是因為在一個線程裡正在運作的PHP腳本會改變目前工作目錄,是以另外一個線程裡的腳本 使用相對路徑則無法打開目标檔案。為了阻止這種錯誤發生,PHP架構提供了稱作VCWD (virtual current working directory 虛拟目前工作目錄)宏,用來代替任何依賴目前工作目錄的存取函數。這些宏與被替代的函數具備同樣的功能,同時是被透明地處理。在某些沒有标準C函數庫平台 的情況下,VCWD架構則不會得到支援。例如,Win32下不存在chown(),就不會有相應的VCWD_CHOWN()宏被定義。

VCWD清單

标準C庫

VCWD宏

說明

getcwd()

VCWD_GETCWD()

fopen()

VCWD_FOPEN

open()

VCWD_OPEN()

用于兩個參數的版本

open()

VCWD_OPEN_MODE()

用于三個參數的open()版本

creat()

VCWD_CREAT()

chdir()

VCWD_CHDIR()

getwd()

VCWD_GETWD()

realpath()

VCWD_REALPATH()

rename()

VCWD_RENAME()

stat()

VCWD_STAT()

lstat()

VCWD_LSTAT()

unlink()

VCWD_UNLINK()

mkdir()

VCWD_MKDIR()

rmdir()

VCWD_RMDIR()

opendir()

VCWD_OPENDIR()

popen()

VCWD_POPEN()

access()

VCWD_ACCESS()

utime()

VCWD_UTIME()

chmod()

VCWD_CHMOD()

chown()

VCWD_CHOWN()

編寫利用資源的第一個PHP函數

實作file_open()應該非常簡單,看起來像下面的樣子:

PHP_FUNCTION(file_open)

{

char *filename = NULL;

char *mode = NULL;

int argc = ZEND_NUM_ARGS();

int filename_len;

int mode_len;

FILE *fp;

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

return;

}

fp = VCWD_FOPEN(filename, mode);

if (fp == NULL) {

RETURN_FALSE;

}

ZEND_REGISTER_RESOURCE(return_value, fp, le_myfile);

}

你 可能會注意到資源注冊宏的第一個參數return_value,可此地找不到它的定義。這個變量自動的被擴充架構定義為zval * 類型的函數傳回值。先前讨論的、能夠影響傳回值的RETURN_LONG() 和RETVAL_BOOL()宏确實改變了return_value的值。是以很容易猜到程式注冊了我們取得的檔案指針fp,同時設定 return_value為該注冊資源。

通路資源 需要使用下面的宏通路資源(參看表對宏參數的解釋)

ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id, default_id,

resource_type_name, resource_type);

ZEND_FETCH_RESOURCE 宏參數

參數

含義

rsrc

資源值儲存到的變量名。它應該和資源有相同類型。

rsrc_type

rsrc的類型,用于在内部把資源轉換成正确的類型

passed_id

尋找的資源值(例如zval **)

default_id

如果該值不為-1,就使用這個id。用于實作資源的預設值。

resource_type_name

資源的一個簡短名稱,用于錯誤資訊。

resource_type

注冊資源的資源類型id

使用這個宏,我們現在能夠實作file_eof():

PHP_FUNCTION(file_eof)

int argc = ZEND_NUM_ARGS();

zval *filehandle = NULL;

FILE *fp;

if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) ==FAILURE) {

return;

       }

ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-c-file",le_myfile);

if (fp == NULL) {

RETURN_FALSE;

       }

if (feof(fp) <= 0) {

RETURN_TRUE;

       }

RETURN_FALSE;

删除一個資源 通常使用下面這個宏删除一個資源:

int zend_list_delete(int id)

傳 遞給宏一個資源id,傳回SUCCESS或者FAILURE。如果資源存在,優先從Zend資源列隊中删除,該過程中會調用該資源類型的已注冊資源清理函 數。是以,在我們的例子中,不必取得檔案指針,調用fclose()關閉檔案,然後再删除資源。直接把資源删除掉即可。

使用這個宏,我們能夠實作file_close():

PHP_FUNCTION(file_close)

{

int argc = ZEND_NUM_ARGS();

zval *filehandle = NULL;

if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) == FAILURE) {

return;

       }

if (zend_list_delete(Z_RESVAL_P(filehandle)) == FAILURE) {

RETURN_FALSE;

       }

RETURN_TRUE;

       }

你 肯定會問自己Z_RESVAL_P()是做什麼的。當我們使用zend_parse_parameters()從參數清單中取得資源的時候,得到的是 zval的形式。為了獲得資源id,我們使用Z_RESVAL_P()宏得到id,然後把id傳遞給zend_list_delete()。

       有一系列宏用于通路存儲于zval值(參考表的宏清單)。盡管在大多數情況下zend_parse_parameters()傳回與c類型相應的值,我們仍希望直接處理zval,包括資源這一情況。

Zval通路宏

通路對象

C 類型

Z_LVAL, Z_LVAL_P,

Z_LVAL_PP

整型值

long

Z_BVAL, Z_BVAL_P,

Z_BVAL_PP

布爾值

zend_bool

Z_DVAL, Z_DVAL_P,

Z_DVAL_PP

浮點值

double

Z_STRVAL, Z_STRVAL_P,

Z_STRVAL_PP

字元串值

char *

Z_STRLEN, Z_STRLEN_P, Z_STRLEN_PP

字元串長度值

int

Z_RESVAL, Z_RESVAL_P,Z_RESVAL_PP

資源值

long

Z_ARRVAL, Z_ARRVAL_P,

Z_ARRVAL_PP

聯合數組

HashTable *

Z_TYPE, Z_TYPE_P,

Z_TYPE_PP

Zval類型

Enumeration (IS_NULL, IS_LONG, IS_DOUBLE, IS_STRING, IS_ARRAY, IS_OBJECT, IS_BOOL, IS_RESOURCE)

Z_OBJPROP,

Z_OBJPROP_P,

Z_OBJPROP_PP

對象屬性hash(本章不會談到)

HashTable *

Z_OBJCE, Z_OBJCE_P,

Z_OBJCE_PP

對象的類資訊(本章不會談到)

zend_class_entry

用 于通路zval值的宏        所有的宏都有三種形式:一個是接受zval s,另外一個接受zval *s,最後一個接受zval **s。它們的差別是在命名上,第一個沒有字尾,zval *有字尾_P(代表一個指針),最後一個 zval **有字尾_PP(代表兩個指針)。

       現在,你有足夠的資訊來獨立完成 file_read()和 file_write()函數。這裡是一個可能的實作:

PHP_FUNCTION(file_read)

{

int argc = ZEND_NUM_ARGS();

long size;

zval *filehandle = NULL;

FILE *fp;

char *result;

size_t bytes_read;

if (zend_parse_parameters(argc TSRMLS_CC, "rl", &filehandle,&size) == FAILURE) {

return;

}

ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-cfile", le_myfile);

result = (char *) emalloc(size+1);

bytes_read = fread(result, 1, size, fp);

result[bytes_read] = '/0';

RETURN_STRING(result, 0);

}

PHP_FUNCTION(file_write)

{

char *buffer = NULL;

int argc = ZEND_NUM_ARGS();

int buffer_len;

zval *filehandle = NULL;

FILE *fp;

if (zend_parse_parameters(argc TSRMLS_CC, "rs", &filehandle,&buffer, &buffer_len) == FAILURE) {

return;

}

ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-cfile", le_myfile);

if (fwrite(buffer, 1, buffer_len, fp) != buffer_len) {

RETURN_FALSE;

}

RETURN_TRUE;

}

測試擴充 你現在可以編寫一個測試腳本來檢測擴充是否工作正常。下面是一個示例腳本,該腳本打開檔案test.txt,輸出檔案類容到标準輸出,建立一個拷貝test.txt.new。

<?php

$fp_in = file_open("test.txt", "r") or die("Unable to open input file/n");

$fp_out = file_open("test.txt.new", "w") or die("Unable to open output file/n");

while (!file_eof($fp_in)) {

$str = file_read($fp_in, 1024);

print($str);

file_write($fp_out, $str);

}

file_close($fp_in);

file_close($fp_out);

?>

全局變量

你 可能希望在擴充裡使用全局C變量,無論是獨自在内部使用或通路php.ini檔案中的INI擴充注冊标記(INI在下一節中讨論)。因為PHP是為多線程 環境而設計,是以不必定義全局變量。PHP提供了一個建立全局變量的機制,可以同時應用線上程和非線程環境中。我們應當始終利用這個機制,而不要自主地定 義全局變量。用一個宏通路這些全局變量,使用起來就像普通全局變量一樣。

       用于生成myfile工程骨架檔案的ext_skel腳本建立了必要的代碼來支援全局變量。通過檢查php_myfile.h檔案,你應當發現類似下面的被注釋掉的一節,

ZEND_BEGIN_MODULE_GLOBALS(myfile)

int global_value;

char *global_string;

ZEND_END_MODULE_GLOBALS(myfile)

你 可以把這一節的注釋去掉,同時添加任何其他全局變量于這兩個宏之間。檔案後部的幾行,骨架腳本自動地定義一個MYFILE_G(v)宏。這個宏應當被用于 所有的代碼,以便通路這些全局變量。這就確定在多線程環境中,通路的全局變量僅是一個線程的拷貝,而不需要互斥的操作。

       為了使全局變量有效,最後需要做的是把myfile.c:

ZEND_DECLARE_MODULE_GLOBALS(myfile)

注釋去掉。

你也許希望在每次PHP請求的開始初始化全局變量。另外,做為一個例子,全局變量已指向了一個已配置設定的記憶體,在每次PHP請求結束時需要釋放記憶體。為了達到這些目的,全局變量機制提供了一個特殊的宏,用于注冊全局變量的構造和析構函數(參考表對宏參數的說明):

ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)

表 ZEND_INIT_MODULE_GLOBALS 宏參數

參數

含義

module_name

與傳遞給ZEND_BEGIN_MODULE_GLOBALS()宏相同的擴充名稱。

globals_ctor

構造函數指針。在myfile擴充裡,函數原形與void php_myfile_init_globals(zend_myfile_globals *myfile_globals)類似

globals_dtor

析構函數指針。例如,php_myfile_init_globals(zend_myfile_globals *myfile_globals)

你可以在myfile.c裡看到如何使用構造函數和ZEND_INIT_MODULE_GLOBALS()宏的示例。

添加自定義INI指令

       INI檔案(php.ini)的實作使得PHP擴充注冊和監聽各自的INI條目。如果這些INI條目由php.ini、Apache的htaccess或 其他配置方法來指派,注冊的INI變量總是更新到正确的值。整個INI架構有許多不同的選項以實作其靈活性。我們涉及一些基本的(也是個好的開端),借助 本章的其他材料,我們就能夠應付日常開發工作的需要。

通過在PHP_INI_BEGIN()/PHP_INI_END()宏之間的STD_PHP_INI_ENTRY()宏注冊PHP INI指令。例如在我們的例子裡,myfile.c中的注冊過程應當如下:

PHP_INI_BEGIN()

STD_PHP_INI_ENTRY("myfile.global_value", "42", PHP_INI_ALL, OnUpdateInt, global_value, zend_myfile_globals, myfile_globals)

STD_PHP_INI_ENTRY("myfile.global_string", "foobar", PHP_INI_ALL, OnUpdateString, global_string, zend_myfile_globals, myfile_globals)

PHP_INI_END()

除了STD_PHP_INI_ENTRY()其他宏也能夠使用,但這個宏是最常用的,可以滿足大多數需要(參看表對宏參數的說明):

STD_PHP_INI_ENTRY(name, default_value, modifiable, on_modify, property_name, struct_type, struct_ptr)

STD_PHP_INI_ENTRY 宏參數表

參數

含義

name

INI條目名

default_value

如果沒有在INI檔案中指定,條目的預設值。預設值始終是一個字元串。

modifiable

設定在何種環境下INI條目可以被更改的位域。可以的值是:

• PHP_INI_SYSTEM. 能夠在php.ini或http.conf等系統檔案更改

• PHP_INI_PERDIR. 能夠在 .htaccess中更改

• PHP_INI_USER. 能夠被使用者腳本更改

• PHP_INI_ALL. 能夠在所有地方更改

on_modify

處理INI條目更改的回調函數。你不需自己編寫處理程式,使用下面提供的函數。包括:

• OnUpdateInt

• OnUpdateString

• OnUpdateBool

• OnUpdateStringUnempty

• OnUpdateReal

property_name

應當被更新的變量名

struct_type

變量駐留的結構類型。因為通常使用全局變量機制,是以這個類型自動被定義,類似于zend_myfile_globals。

struct_ptr

全局結構名。如果使用全局變量機制,該名為myfile_globals。

最 後,為了使自定義INI條目機制正常工作,你需要分别去掉PHP_MINIT_FUNCTION(myfile)中的 REGISTER_INI_ENTRIES()調用和PHP_MSHUTDOWN_FUNCTION(myfile)中的 UNREGISTER_INI_ENTRIES()的注釋。

       通路兩個示例全局變量中的一個與在擴充裡編寫MYFILE_G(global_value) 和MYFILE_G(global_string)一樣簡單。

       如果你把下面的兩行放在php.ini中,MYFILE_G(global_value)的值會變為99。

; php.ini – The following line sets the INI entry myfile.global_value to 99.

myfile.global_value = 99

線程安全資源管理宏

現在,你肯定注意到以TSRM(線程安全資料總管)開頭的宏随處使用。這些宏提供給擴充擁有獨自的全局變量的可能,正如前面提到的。

當 編寫PHP擴充時,無論是在多程序或多線程環境中,都是依靠這一機制通路擴充自己的全局變量。如果使用全局變量通路宏(例如MYFILE_G()宏),需 要確定TSRM上下文資訊出現在目前函數中。基于性能的原因,Zend引擎試圖把這個上下文資訊作為參數傳遞到更多的地方,包括 PHP_FUNCTION()的定義。正因為這樣,在PHP_FUNCTION()内當編寫的代碼使用通路宏(例如MYFILE_G()宏)時,不需要做 任何特殊的聲明。然而,如果PHP函數調用其他需要通路全局變量的C函數,要麼把上下文作為一個額外的參數傳遞給C函數,要麼提取上下文(要慢點)。

       在需要通路全局變量的代碼塊開頭使用TSRMLS_FETCH()來提取上下文。例如:

void myfunc()

{

TSRMLS_FETCH();

MYFILE_G(myglobal) = 2;

}

如 果希望讓代碼更加優化,更好的辦法是直接傳遞上下文給函數(正如前面叙述的,PHP_FUNCTION()範圍内自動可用)。可以使用 TSRMLS_C(C表示調用Call)和TSRMLS_CC(CC邊式調用Call和逗号Comma)宏。前者應當用于僅當上下文作為一個單獨的參數, 後者應用于接受多個參數的函數。在後一種情況中,因為根據取名,逗号在上下文的前面,是以TSRMLS_CC不能是第一個函數參。

在函數原形中,可以分别使用TSRMLS_D和TSRMLS_DC宏聲名正在接收上下文。

       下面是前一例子的重寫,利用了參數傳遞上下文。

void myfunc(TSRMLS_D)

{

MYFILE_G(myglobal) = 2;

}

PHP_FUNCTION(my_php_function)

{

myfunc(TSRMLS_C);

}

總 結

現在,你已經學到了足夠的東西來建立自己的擴充。本章講述了一些重要的基礎來編寫和了解PHP擴充。Zend引擎提供的擴充API相當豐富,使你能夠開發面向對象的擴充。幾乎沒有文檔談幾許多進階特性。當然,依靠本章所學的基礎知識,你可以通過浏覽現有的原碼學到很多。

       更多關于資訊可以在PHP手冊的擴充PHP章節http://www.php.net/manual/en/zend.php中找到。另外,你也可以考慮 加入PHP開發者郵件清單internals@ lists.php.net,該郵件清單圍繞開發PHP 本身。你還可以檢視一下新的擴充生成工具——PECL_Gen(http://pear.php.net/package/PECL_Gen),這個工具 正在開發之中,比起本章使用的ext_skel有更多的特性。

詞彙表

binary safe 二進制安全

context 上下文

extensions 擴充

entry 條目

skeleton 骨架

Thread-Safe Resource Manager TSRM 線程安全資料總管

Contact info:

Email: taft # wjl.cn

[1] 可參考譯者寫的

[2] 譯者:可以使用phpcli程式在控制台裡執行php檔案。

[3] 譯者:可以檢視到生成的FETCH_RESOURCE()宏參數是一些’???’。