PHP取得成功的一個主要原因之一是她擁有大量的可用擴充。web開發者無論有何種需求,這種需求最有可能在PHP發行包裡找到。PHP發行包包括支援各種資料庫,圖形檔案格式,壓縮,XML技術擴充在内的許多擴充。
擴充API的引入使PHP3取得了巨大的進展,擴充API機制使PHP開發社群很容易的開發出幾十種擴充。現在,兩個版本過去了,API仍然和PHP3時的非常相似。擴充主要的思想是:盡可能的從擴充編寫者那裡隐藏PHP的内部機制和腳本引擎本身,僅僅需要開發者熟悉API。
有兩個理由需要自己編寫PHP擴充。第一個理由是:PHP需要支援一項她還未支援的技術。這通常包括包裹一些現成的C函數庫,以便提供PHP接口。例如,如果一個叫FooBase的資料庫已推出市場,你需要建立一個PHP擴充幫助你從PHP裡調用FooBase的C函數庫。這個工作可能僅由一個人完成,然後被整個PHP社群共享(如果你願意的話)。第二個不是很普遍的理由是:你需要從性能或功能的原因考慮來編寫一些商業邏輯。
如果以上的兩個理由都和你沒什麼關系,同時你感覺自己沒有冒險精神,那麼你可以跳過本章。
本章教你如何編寫相對簡單的PHP擴充,使用一部分擴充API函數。對于大多數打算開發自定義PHP擴充開發者而言,它含概了足夠的資料。學習一門程式設計課程的最好方法之一就是動手做一些極其簡單的例子,這些例子正是本章的線索。一旦你明白了基礎的東西,你就可以在網際網路上通過閱讀文擋、原代碼或參加郵件清單新聞討論區讨論來豐富自己。是以,本章集中在讓你如何開始的話題。在UNIX下一個叫ext_skel的腳本被用于建立擴充的骨架,骨架資訊從一個描述擴充接口的定義檔案中取得。是以你需要利用UNIX來建立一個骨架。Windows開發者可以使用Windows ext_skel_win32.php代替ext_skel。
然而,本章關于用你開發的擴充編譯PHP的指導僅涉及UNIX編譯系統。本章中所有的對API的解釋與UNIX和Windows下開發的擴充都有聯系。
當你閱讀完這章,你能學會如何
☞建立一個簡單的商業邏輯擴充。
☞ .建議個C函數庫的包裹擴充,尤其是有些标準C檔案操作函數比如fopen()
快速開始
本節沒有介紹關于腳本引擎基本構造的一些知識,而是直接進入擴充的編碼講解中,是以不要擔心你無法立刻獲得對擴充整體把握的感覺。假設你正在開發一個網站,需要一個把字元串重複n次的函數。下面是用PHP寫的例子:
function self_concat($string, $n)
{
$result = "";
for ($i = 0; $i < $n; $i++) {
$result .= $string;
}
return $result;
}
self_concat("One", 3) returns "OneOneOne".
self_concat("One", 1) returns "One".
假設由于一些奇怪的原因,你需要時常調用這個函數,而且還要傳給函數很長的字元串和大值n。這意味着在腳本裡有相當巨大的字元串連接配接量和記憶體重新配置設定過程,以至顯著地降低腳本執行速度。如果有一個函數能夠更快地配置設定大量且足夠的記憶體來存放結果字元串,然後把$string重複n次,就不需要在每次循環疊代中配置設定記憶體。
為擴充建立函數的第一步是寫一個函數定義檔案,該函數定義檔案定義了擴充對外提供的函數原形。該例中,定義函數隻有一行函數原形self_concat() :
string self_concat(string str, int n)
函數定義檔案的一般格式是一個函數一行。你可以定義可選參數和使用大量的PHP類型,包括: bool, float, int, array等。
儲存為myfunctions.def檔案至PHP原代碼目錄樹下。
該是通過擴充骨架(skeleton)構造器運作函數定義檔案的時機了。該構造器腳本叫ext_skel,放在PHP原代碼目錄樹的ext/目錄下(PHP原碼主目錄下的README.EXT_SKEL提供了更多的資訊)。假設你把函數定義儲存在一個叫做myfunctions.def的檔案裡,而且你希望把擴充取名為myfunctions,運作下面的指令來建立擴充骨架
./ext_skel --extname=myfunctions --proto=myfunctions.def
這個指令在ext/目錄下建立了一個myfunctions/目錄。你要做的第一件事情也許就是編譯該骨架,以便編寫和測試實際的C代碼。編譯擴充有兩種方法:
☞ 作為一個可裝載子產品或者DSO(動态共享對象)
☞ 靜态編譯到PHP
因為第二種方法比較容易上手,是以本章采用靜态編譯。如果你對編譯可裝載擴充子產品感興趣,可以閱讀PHP原代碼根目錄下的README.SELF-CONTAINED_EXTENSIONS檔案。為了使擴充能夠被編譯,需要修改擴充目錄ext/myfunctions/下的config.m4檔案。擴充沒有包裹任何外部的C庫,你需要添加支援--enable-myfunctions配置開關到PHP編譯系統裡(–with-extension 開關用于那些需要使用者指定相關C庫路徑的擴充)。可以去掉自動生成的下面兩行的注釋來開啟這個配置。
PHP_ARG_ENABLE(myfunctions, whether to enable myfunctions support,
[ --enable-myfunctions Include myfunctions support])
現在剩下的事情就是在PHP原代碼樹根目錄下運作./buildconf,該指令會生成一個新的配置腳本。通過檢視./configure --help輸出資訊,可以檢查新的配置選項是否被包含到配置檔案中。現在,打開你喜好的配置選項開關和--enable-myfunctions重新配置一下PHP。最後的但不是最次要的是,用make來重新編譯PHP。
ext_skel應該把兩個PHP函數添加到你的擴充骨架了:打算實作的self_concat()函數和用于檢測myfunctions 是否編譯到PHP的confirm_myfunctions_compiled()函數。完成PHP的擴充開發後,可以把後者去掉。
<?php
print confirm_myfunctions_compiled("myextension");
?>
運作這個腳本會出現類似下面的輸出:
"Congratulations! You have successfully modified ext/myfunctions
config.m4. Module myfunctions is now compiled into PHP."
另外,ext_skel腳本生成一個叫myfunctions.php的腳本,你也可以利用它來驗證擴充是否被成功地編譯到PHP。它會列出該擴充所支援的所有函數。
現在你學會如何編譯擴充了,該是真正地研究self_concat()函數的時候了。
下面就是ext_skel腳本生成的骨架結構:
PHP_FUNCTION(self_concat)
}
char *str = NULL;
int argc = ZEND_NUM_ARGS();
int str_len;
long n;
if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
return;
php_error(E_WARNING, "self_concat: not yet implemented");
}
自動生成的PHP函數周圍包含了一些注釋,這些注釋用于自動生成代碼文檔和vi、Emacs等編輯器的代碼折疊。函數自身的定義使用了宏PHP_FUNCTION(),該宏可以生成一個适合于Zend引擎的函數原型。邏輯本身分成語義各部分,取得調用函數的參數和邏輯本身。
為了獲得函數傳遞的參數,可以使用zend_parse_parameters()API函數。下面是該函數的原型:
zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, …);
第一個參數是傳遞給函數的參數個數。通常的做法是傳給它ZEND_NUM_ARGS()。這是一個表示傳遞給函數參數總個數的宏。第二個參數是為了線程安全,總是傳遞TSRMLS_CC宏,後面會講到。第三個參數是一個字元串,指定了函數期望的參數類型,後面緊跟着需要随參數值更新的變量清單。因為PHP采用松散的變量定義和動态的類型判斷,這樣做就使得把不同類型的參數轉化為期望的類型成為可能。例如,如果使用者傳遞一個整數變量,可函數需要一個浮點數,那麼zend_parse_parameters()就會自動地把整數轉換為相應的浮點數。如果實際值無法轉換成期望類型(比如整形到數組形),會觸發一個警告。
下表列出了可能指定的類型。我們從完整性考慮也列出了一些沒有讨論到的類型。
類型指定符 | 對應的C類型 | 描述 |
l | long | 符号整數 |
d | double | 浮點數 |
s | char *, int | 二進制字元串,長度 |
b | zend_bool | 邏輯型(1或0) |
r | zval * | 資源(檔案指針,資料庫連接配接等) |
a | zval * | 聯合數組 |
o | zval * | 任何類型的對象 |
O | zval * | 指定類型的對象。需要提供目标對象的類類型 |
z | zval * | 無任何操作的zval |
為了容易地了解最後幾個選項的含義,你需要知道zval是Zend引擎的值容器[1]。無論這個變量是布爾型,字元串型或者其他任何類型,其資訊總會包含在一個zval聯合體中。本章中我們不直接存取zval,而是通過一些附加的宏來操作。下面的是或多或少在C中的zval, 以便我們能更好地了解接下來的代碼。
typedef union _zval {
long lval;
double dval;
struct {
char *val;
int len;
} str;
HashTable *ht;
zend_object_value obj;
} zval;
在我們的例子中,我們用基本類型調用zend_parse_parameters(),以本地C類型的方式取得函數參數的值,而不是用zval容器。
為了讓zend_parse_parameters()能夠改變傳遞給它的參數的值,并傳回這個改變值,需要傳遞一個引用。仔細檢視一下self_concat():
if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
return;
注意到自動生成的代碼會檢測函數的傳回值FAILUER(成功即SUCCESS)來判斷是否成功。如果沒有成功則立即傳回,并且由zend_parse_parameters()負責觸發警告資訊。因為函數打算接收一個字元串l和一個整數n,是以指定 ”sl” 作為其類型訓示符。s需要兩個參數,是以我們傳遞參考char * 和 int (str 和 str_len)給zend_parse_parameters()函數。無論什麼時候,記得總是在代碼中使用字元串長度str_len來確定函數工作在二進制安全的環境中。不要使用strlen()和strcpy(),除非你不介意函數在二進制字元串下不能工作。二進制字元串是包含有nulls的字元串。二進制格式包括圖象檔案,壓縮檔案,可執行檔案和更多的其他檔案。”l” 隻需要一個參數,是以我們傳遞給它n的引用。盡管為了清晰起見,骨架腳本生成的C變量名與在函數原型定義檔案中的參數名一樣;這樣做不是必須的,盡管在實踐中鼓勵這樣做。
回到轉換規則中來。下面三個對self_concat()函數的調用使str, str_len和n得到同樣的值:
self_concat("321", 5);
self_concat(321, "5");
self_concat("321", "5");
str points to the string "321", str_len equals 3, and n equals 5.
str 指向字元串"321",str_len等于3,n等于5。
在我們編寫代碼來實作連接配接字元串傳回給PHP的函數前,還得談談兩個重要的話題:記憶體管理、從PHP内部傳回函數值所使用的API。
記憶體管理
用于從堆中配置設定記憶體的PHP API幾乎和标準C API一樣。在編寫擴充的時候,使用下面與C對應(是以不必再解釋)的API函數:
emalloc(size_t size);
efree(void *ptr);
ecalloc(size_t nmemb, size_t size);
erealloc(void *ptr, size_t size);
estrdup(const char *s);
estrndup(const char *s, unsigned int length);
在這一點上,任何一位有經驗的C程式員應該象這樣思考一下:“什麼?标準C沒有strndup()?”是的,這是正确的,因為GNU擴充通常在Linux下可用。estrndup()隻是PHP下的一個特殊函數。它的行為與estrdup()相似,但是可以指定字元串重複的次數(不需要結束空字元),同時是二進制安全的。這是推薦使用estrndup()而不是estrdup()的原因。
在幾乎所有的情況下,你應該使用這些記憶體配置設定函數。有一些情況,即擴充需要配置設定在請求中永久存在的記憶體,進而不得不使用malloc(),但是除非你知道你在做什麼,你應該始終使用以上的函數。如果沒有使用這些記憶體函數,而相反使用标準C函數配置設定的記憶體傳回給腳本引擎,那麼PHP會崩潰。
這些函數的優點是:任何配置設定的記憶體在偶然情況下如果沒有被釋放,則會在頁面請求的最後被釋放。是以,真正的記憶體洩漏不會産生。然而,不要依賴這一機制,從調試和性能兩個原因來考慮,應當確定釋放應該釋放的記憶體。剩下的優點是在多線程環境下性能的提高,調試模式下檢測記憶體錯誤等。
還有一個重要的原因,你不需要檢查這些記憶體配置設定函數的傳回值是否為null。當記憶體配置設定失敗,它們會發出E_ERROR錯誤,進而決不會傳回到擴充。
從PHP函數中傳回值
擴充API包含豐富的用于從函數中傳回值的宏。這些宏有兩種主要風格:第一種是RETVAL_type()形式,它設定了傳回值但C代碼繼續執行。這通常使用在把控制交給腳本引擎前還希望做的一些清理工作的時候使用,然後再使用C的傳回聲明 ”return” 傳回到PHP;後一個宏更加普遍,其形式是RETURN_type(),他設定了傳回類型,同時傳回控制到PHP。下表解釋了大多數存在的宏。
設定傳回值并且結束函數 | 設定傳回值 | 宏傳回類型和參數 |
RETURN_LONG(l) | RETVAL_LONG(l) | 整數 |
RETURN_BOOL(b) | RETVAL_BOOL(b) | 布爾數(1或0) |
RETURN_NULL() | RETVAL_NULL() | NULL |
RETURN_DOUBLE(d) | RETVAL_DOUBLE(d) | 浮點數 |
RETURN_STRING(s, dup) | RETVAL_STRING(s, dup) | 字元串。如果dup為1,引擎會調用estrdup()重複s,使用拷貝。如果dup為0,就使用s |
RETURN_STRINGL(s, l, dup) | RETVAL_STRINGL(s, l, dup) | 長度為l的字元串值。與上一個宏一樣,但因為s的長度被指定,是以速度更快。 |
RETURN_TRUE | RETVAL_TRUE | 傳回布爾值true。注意到這個宏沒有括号。 |
RETURN_FALSE | RETVAL_FALSE | 傳回布爾值false。注意到這個宏沒有括号。 |
RETURN_RESOURCE(r) | RETVAL_RESOURCE(r) | 資源句柄。 |
完成self_concat()
現在你已經學會了如何配置設定記憶體和從PHP擴充函數裡傳回函數值,那麼我們就能夠完成self_concat()的編碼:
PHP_FUNCTION(self_concat)
}
char *str = NULL;
int argc = ZEND_NUM_ARGS();
int str_len;
long n;
char *result;
char *ptr;
int result_length;
if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
return;
result_length = (str_len * n);
result = (char *) emalloc(result_length + 1);
ptr = result;
while (n--) {
memcpy(ptr, str, str_len);
ptr += str_len;
}
*ptr = '/0';
RETURN_STRINGL(result, result_length, 0);
}
現在要做的就是重新編譯一下PHP,這樣就完成了第一個PHP函數。
讓我門檢查函數是否真的工作。在最新編譯過的PHP樹下執行[2]下面的腳本:
<?php
for ($i = 1; $i <= 3; $i++) {
print self_concat("ThisIsUseless", $i);
print "/n";
}
?>
你應該得到下面的結果:
ThisIsUseless
ThisIsUselessThisIsUseless
ThisIsUselessThisIsUselessThisIsUseless
執行個體小結
你已經學會如何編寫一個簡單的PHP函數。回到本章的開頭,我們提到用C編寫PHP功能函數的兩個主要的動機。第一個動機是用C實作一些算法來提高性能和擴充功能。前一個例子應該能夠指導你快速上手這種類型擴充的開發。第二個動機是包裹三方函數庫。我們将在下一步讨論。
包裹第三方的擴充
本節中你将學到如何編寫更有用和更完善的擴充。該節的擴充包裹了一個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相當豐富,使你能夠開發面向對象的擴充。幾乎沒有文檔談幾許多進階特性。當然,依靠本章所學的基礎知識,你可以通過浏覽現有的原碼學到很多。