英文版下載: PHP 5 Power Programming http://www.jb51.net/books/61020.html
PHP取得成功的一個主要原因之一是她擁有大量的可用擴展。web開發(fā)者無論有何種需求,這種需求最有可能在PHP發(fā)行包里找到。PHP發(fā)行包包括支持各種數(shù)據(jù)庫,圖形文件格式,壓縮,XML技術擴展在內的許多擴展。
擴展API的引入使PHP3取得了巨大的進展,擴展API機制使PHP開發(fā)社區(qū)很容易的開發(fā)出幾十種擴展?,F(xiàn)在,兩個版本過去了,API仍然和PHP3時的非常相似。擴展主要的思想是:盡可能的從擴展編寫者那里隱藏PHP的內部機制和腳本引擎本身,僅僅需要開發(fā)者熟悉API。
有兩個理由需要自己編寫PHP擴展。第一個理由是:PHP需要支持一項她還未支持的技術。這通常包括包裹一些現(xiàn)成的C函數(shù)庫,以便提供PHP接口。例如,如果一個叫FooBase的數(shù)據(jù)庫已推出市場,你需要建立一個PHP擴展幫助你從PHP里調用FooBase的C函數(shù)庫。這個工作可能僅由一個人完成,然后被整個PHP社區(qū)共享(如果你愿意的話)。第二個不是很普遍的理由是:你需要從性能或功能的原因考慮來編寫一些商業(yè)邏輯。
如果以上的兩個理由都和你沒什么關系,同時你感覺自己沒有冒險精神,那么你可以跳過本章。
本章教你如何編寫相對簡單的PHP擴展,使用一部分擴展API函數(shù)。對于大多數(shù)打算開發(fā)自定義PHP擴展開發(fā)者而言,它含概了足夠的資料。學習一門編程課程的最好方法之一就是動手做一些極其簡單的例子,這些例子正是本章的線索。一旦你明白了基礎的東西,你就可以在互聯(lián)網上通過閱讀文擋、原代碼或參加郵件列表新聞組討論來豐富自己。因此,本章集中在讓你如何開始的話題。在UNIX下一個叫ext_skel的腳本被用于建立擴展的骨架,骨架信息從一個描述擴展接口的定義文件中取得。因此你需要利用UNIX來建立一個骨架。Windows開發(fā)者可以使用Windows ext_skel_win32.php代替ext_skel。
然而,本章關于用你開發(fā)的擴展編譯PHP的指導僅涉及UNIX編譯系統(tǒng)。本章中所有的對API的解釋與UNIX和Windows下開發(fā)的擴展都有聯(lián)系。
當你閱讀完這章,你能學會如何
•建立一個簡單的商業(yè)邏輯擴展。
•建議個C函數(shù)庫的包裹擴展,尤其是有些標準C文件操作函數(shù)比如fopen()
快速開始
本節(jié)沒有介紹關于腳本引擎基本構造的一些知識,而是直接進入擴展的編碼講解中,因此不要擔心你無法立刻獲得對擴展整體把握的感覺。假設你正在開發(fā)一個網站,需要一個把字符串重復n次的函數(shù)。下面是用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".
假設由于一些奇怪的原因,你需要時常調用這個函數(shù),而且還要傳給函數(shù)很長的字符串和大值n。這意味著在腳本里有相當巨大的字符串連接量和內存重新分配過程,以至顯著地降低腳本執(zhí)行速度。如果有一個函數(shù)能夠更快地分配大量且足夠的內存來存放結果字符串,然后把$string重復n次,就不需要在每次循環(huán)迭代中分配內存。
為擴展建立函數(shù)的第一步是寫一個函數(shù)定義文件,該函數(shù)定義文件定義了擴展對外提供的函數(shù)原形。該例中,定義函數(shù)只有一行函數(shù)原形self_concat() :
. 代碼如下:
string self_concat(string str, int n)
函數(shù)定義文件的一般格式是一個函數(shù)一行。你可以定義可選參數(shù)和使用大量的PHP類型,包括: bool, float, int, array等。
保存為myfunctions.def文件至PHP原代碼目錄樹下。
該是通過擴展骨架(skeleton)構造器運行函數(shù)定義文件的時機了。該構造器腳本叫ext_skel,放在PHP原代碼目錄樹的ext/目錄下(PHP原碼主目錄下的README.EXT_SKEL提供了更多的信息)。假設你把函數(shù)定義保存在一個叫做myfunctions.def的文件里,而且你希望把擴展取名為myfunctions,運行下面的命令來建立擴展骨架
. 代碼如下:
./ext_skel --extname=myfunctions --proto=myfunctions.de
這個命令在ext/目錄下建立了一個myfunctions/目錄。你要做的第一件事情也許就是編譯該骨架,以便編寫和測試實際的C代碼。編譯擴展有兩種方法:
•作為一個可裝載模塊或者DSO(動態(tài)共享對象)
•靜態(tài)編譯到PHP
PHP擴展開發(fā)導圖
因為第二種方法比較容易上手,所以本章采用靜態(tài)編譯。如果你對編譯可裝載擴展模塊感興趣,可以閱讀PHP原代碼根目錄下的README.SELF-CONTAINED_EXTENSIONS文件。為了使擴展能夠被編譯,需要修改擴展目錄ext/myfunctions/下的config.m4文件。擴展沒有包裹任何外部的C庫,你需要添加支持–enable-myfunctions配置開關到PHP編譯系統(tǒng)里(–with-extension 開關用于那些需要用戶指定相關C庫路徑的擴展)。可以去掉自動生成的下面兩行的注釋來開啟這個配置。
. 代碼如下:
./ext_skel --extname=myfunctions --proto=myfunctions.def
PHP_ARG_ENABLE(myfunctions, whether to enable myfunctions support,
[ --enable-myfunctions Include myfunctions support]
現(xiàn)在剩下的事情就是在PHP原代碼樹根目錄下運行./buildconf,該命令會生成一個新的配置腳本。通過查看./configure –help輸出信息,可以檢查新的配置選項是否被包含到配置文件中。現(xiàn)在,打開你喜好的配置選項開關和–enable-myfunctions重新配置一下PHP。最后的但不是最次要的是,用make來重新編譯PHP。
ext_skel應該把兩個PHP函數(shù)添加到你的擴展骨架了:打算實現(xiàn)的self_concat()函數(shù)和用于檢測myfunctions 是否編譯到PHP的confirm_myfunctions_compiled()函數(shù)。完成PHP的擴展開發(fā)后,可以把后者去掉。
. 代碼如下:
<?php
print confirm_myfunctions_compiled("myextension");
?>
運行這個腳本會出現(xiàn)類似下面的輸出:
. 代碼如下:
"Congratulations! You have successfully modified ext/myfunctions
config.m4. Module myfunctions is now compiled into PHP.
另外,ext_skel腳本生成一個叫myfunctions.php的腳本,你也可以利用它來驗證擴展是否被成功地編譯到PHP。它會列出該擴展所支持的所有函數(shù)。
現(xiàn)在你學會如何編譯擴展了,該是真正地研究self_concat()函數(shù)的時候了。
下面就是ext_skel腳本生成的骨架結構:
. 代碼如下:
/* {{{ proto string self_concat(string str, int n)
*/
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函數(shù)周圍包含了一些注釋,這些注釋用于自動生成代碼文檔和vi、Emacs等編輯器的代碼折疊。函數(shù)自身的定義使用了宏PHP_FUNCTION(),該宏可以生成一個適合于Zend引擎的函數(shù)原型。邏輯本身分成語義各部分,取得調用函數(shù)的參數(shù)和邏輯本身。
為了獲得函數(shù)傳遞的參數(shù),可以使用zend_parse_parameters()API函數(shù)。下面是該函數(shù)的原型:
. 代碼如下:
zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, …);
第一個參數(shù)是傳遞給函數(shù)的參數(shù)個數(shù)。通常的做法是傳給它ZEND_NUM_ARGS()。這是一個表示傳遞給函數(shù)參數(shù)總個數(shù)的宏。第二個參數(shù)是為了線程安全,總是傳遞TSRMLS_CC宏,后面會講到。第三個參數(shù)是一個字符串,指定了函數(shù)期望的參數(shù)類型,后面緊跟著需要隨參數(shù)值更新的變量列表。因為PHP采用松散的變量定義和動態(tài)的類型判斷,這樣做就使得把不同類型的參數(shù)轉化為期望的類型成為可能。例如,如果用戶傳遞一個整數(shù)變量,可函數(shù)需要一個浮點數(shù),那么zend_parse_parameters()就會自動地把整數(shù)轉換為相應的浮點數(shù)。如果實際值無法轉換成期望類型(比如整形到數(shù)組形),會觸發(fā)一個警告。
下表列出了可能指定的類型。我們從完整性考慮也列出了一些沒有討論到的類型。
類型指定符 |
對應的C類型 |
描述 |
l |
long |
符號整數(shù) |
d |
double |
浮點數(shù) |
s |
char *, int |
二進制字符串,長度 |
b |
zend_bool |
邏輯型(1或0) |
r |
zval * |
資源(文件指針,數(shù)據(jù)庫連接等) |
a |
zval * |
聯(lián)合數(shù)組 |
o |
zval * |
任何類型的對象 |
O |
zval * |
指定類型的對象。需要提供目標對象的類類型 |
z |
zval * |
無任何操作的zval |
為了容易地理解最后幾個選項的含義,你需要知道zval是Zend引擎的值容器[1]。無論這個變量是布爾型,字符串型或者其他任何類型,其信息總會包含在一個zval聯(lián)合體中。本章中我們不直接存取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類型的方式取得函數(shù)參數(shù)的值,而不是用zval容器。
為了讓zend_parse_parameters()能夠改變傳遞給它的參數(shù)的值,并返回這個改變值,需要傳遞一個引用。仔細查看一下self_concat():
. 代碼如下:
if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)return;
注意到自動生成的代碼會檢測函數(shù)的返回值FAILUER(成功即SUCCESS)來判斷是否成功。如果沒有成功則立即返回,并且由zend_parse_parameters()負責觸發(fā)警告信息。因為函數(shù)打算接收一個字符串l和一個整數(shù)n,所以指定 ”sl” 作為其類型指示符。s需要兩個參數(shù),所以我們傳遞參考char * 和 int (str 和 str_len)給zend_parse_parameters()函數(shù)。無論什么時候,記得總是在代碼中使用字符串長度str_len來確保函數(shù)工作在二進制安全的環(huán)境中。不要使用strlen()和strcpy(),除非你不介意函數(shù)在二進制字符串下不能工作。二進制字符串是包含有nulls的字符串。二進制格式包括圖象文件,壓縮文件,可執(zhí)行文件和更多的其他文件?!眑” 只需要一個參數(shù),所以我們傳遞給它n的引用。盡管為了清晰起見,骨架腳本生成的C變量名與在函數(shù)原型定義文件中的參數(shù)名一樣;這樣做不是必須的,盡管在實踐中鼓勵這樣做。
回到轉換規(guī)則中來。下面三個對self_concat()函數(shù)的調用使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
在我們編寫代碼來實現(xiàn)連接字符串返回給PHP的函數(shù)前,還得談談兩個重要的話題:內存管理、從PHP內部返回函數(shù)值所使用的API。
內存管理
用于從堆中分配內存的PHP API幾乎和標準C API一樣。在編寫擴展的時候,使用下面與C對應(因此不必再解釋)的API函數(shù):
. 代碼如下:
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下的一個特殊函數(shù)。它的行為與estrdup()相似,但是可以指定字符串重復的次數(shù)(不需要結束空字符),同時是二進制安全的。這是推薦使用estrndup()而不是estrdup()的原因。
在幾乎所有的情況下,你應該使用這些內存分配函數(shù)。有一些情況,即擴展需要分配在請求中永久存在的內存,從而不得不使用malloc(),但是除非你知道你在做什么,你應該始終使用以上的函數(shù)。如果沒有使用這些內存函數(shù),而相反使用標準C函數(shù)分配的內存返回給腳本引擎,那么PHP會崩潰。
這些函數(shù)的優(yōu)點是:任何分配的內存在偶然情況下如果沒有被釋放,則會在頁面請求的最后被釋放。因此,真正的內存泄漏不會產生。然而,不要依賴這一機制,從調試和性能兩個原因來考慮,應當確保釋放應該釋放的內存。剩下的優(yōu)點是在多線程環(huán)境下性能的提高,調試模式下檢測內存錯誤等。
還有一個重要的原因,你不需要檢查這些內存分配函數(shù)的返回值是否為null。當內存分配失敗,它們會發(fā)出E_ERROR錯誤,從而決不會返回到擴展。
從PHP函數(shù)中返回值
擴展API包含豐富的用于從函數(shù)中返回值的宏。這些宏有兩種主要風格:第一種是RETVAL_type()形式,它設置了返回值但C代碼繼續(xù)執(zhí)行。這通常使用在把控制交給腳本引擎前還希望做的一些清理工作的時候使用,然后再使用C的返回聲明 ”return” 返回到PHP;后一個宏更加普遍,其形式是RETURN_type(),他設置了返回類型,同時返回控制到PHP。下表解釋了大多數(shù)存在的宏。
設置返回值并且結束函數(shù) |
設置返回值 |
宏返回類型和參數(shù) |
RETURN_LONG(l) |
RETVAL_LONG(l) |
整數(shù) |
RETURN_BOOL(b) |
RETVAL_BOOL(b) |
布爾數(shù)(1或0) |
RETURN_NULL() |
RETVAL_NULL() |
NULL |
RETURN_DOUBLE(d) |
RETVAL_DOUBLE(d) |
浮點數(shù) |
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()
現(xiàn)在你已經學會了如何分配內存和從PHP擴展函數(shù)里返回函數(shù)值,那么我們就能夠完成self_concat()的編碼:
. 代碼如下:
/* {{{ proto string self_concat(string str, int n)
*/
PHP_FUNCTION(self_concat)
}
char *str = NULL;
int argc = ZEND_NUM_ARGS();
int str_len;
long n;
char *result; /* Points to resulting string */
char *ptr; /* Points at the next location we want to copy to */
int result_length; /* Length of resulting string */
if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
return;
/* Calculate length of result */
result_length = (str_len * n);
/* Allocate memory for result */
result = (char *) emalloc(result_length + 1);
/* Point at the beginning of the result */
ptr = result;
while (n--) {
/* Copy str to the result */
memcpy(ptr, str, str_len);
/* Increment ptr to point at the next position we want to write to */
ptr += str_len;
}
/* Null terminate the result. Always null-terminate your strings
even if they are binary strings */
*ptr = '\0';
/* Return result to the scripting engine without duplicating it*/
RETURN_STRINGL(result, result_length, 0);
}
/* }}} */
現(xiàn)在要做的就是重新編譯一下PHP,這樣就完成了第一個PHP函數(shù)。
讓我門檢查函數(shù)是否真的工作。在最新編譯過的PHP樹下執(zhí)行[2]下面的腳本:
. 代碼如下:
<?php
for ($i = 1; $i <= 3; $i++){
print self_concat("ThisIsUseless", $i);
print "\n";
}
?>
你應該得到下面的結果:
. 代碼如下:
ThisIsUseless
ThisIsUselessThisIsUseless
ThisIsUselessThisIsUselessThisIsUseles
實例小結
你已經學會如何編寫一個簡單的PHP函數(shù)?;氐奖菊碌拈_頭,我們提到用C編寫PHP功能函數(shù)的兩個主要的動機。第一個動機是用C實現(xiàn)一些算法來提高性能和擴展功能。前一個例子應該能夠指導你快速上手這種類型擴展的開發(fā)。第二個動機是包裹三方函數(shù)庫。我們將在下一步討論。
包裹第三方的擴展
本節(jié)中你將學到如何編寫更有用和更完善的擴展。該節(jié)的擴展包裹了一個C庫,展示了如何編寫一個含有多個互相依賴的PHP函數(shù)擴展。
動機
也許最常見的PHP擴展是那些包裹第三方C庫的擴展。這些擴展包括MySQL或Oracle的數(shù)據(jù)庫服務庫,libxml2的 XML技術庫,ImageMagick 或GD的圖形操縱庫。
在本節(jié)中,我們編寫一個擴展,同樣使用腳本來生成骨架擴展,因為這能節(jié)省許多工作量。這個擴展包裹了標準C函數(shù)fopen(), fclose(), fread(), fwrite()和 feof().
擴展使用一個被叫做資源的抽象數(shù)據(jù)類型,用于代表已打開的文件FILE*。你會注意到大多數(shù)處理比如數(shù)據(jù)庫連接、文件句柄等的PHP擴展使用了資源類型,這是因為引擎自己無法直接“理解”它們。我們計劃在PHP擴展中實現(xiàn)的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);
我們實現(xiàn)這些函數(shù),使它們在命名習慣和簡單性上符合PHP腳本。如果你曾經向PHP社區(qū)貢獻過代碼,你被期望遵循一些公共習俗,而不是跟隨C庫里的API。并不是所有的習俗都寫在PHP代碼樹的CODING_STANDARDS文件里。這即是說,此功能已經從PHP發(fā)展的很早階段即被包含在PHP中,并且與C庫API類似。PHP安裝已經支持fopen(), fclose()和更多的PHP函數(shù)。
以下是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() //接收一個資源句柄和讀入的總字節(jié)數(shù),返回讀入的字符串。
bool file_write(resource filehandle, string buffer)
file_write() //接收一個資源句柄和被寫入的字符串,返回真/假指示是否操作成功。
bool file_eof(resource filehandle)
file_eof() //接收一個資源句柄,返回真/假指示是否到達文件的尾部。
因此,我們的函數(shù)定義文件——保存為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./ 原代碼目錄執(zhí)行下面的命令:
. 代碼如下:
./ext_skel --extname=myfile --proto=myfile.de
然后,按照前一個例子的關于編譯新建立腳本的步驟操作。你會得到一些包含F(xiàn)ETCH_RESOURCE()宏行的編譯錯誤,這樣骨架腳本就無法順利完成編譯。為了讓骨架擴展順利通過編譯,把那些出錯行[3]注釋掉即可。
資源
資源是一個能容納任何信息的抽象數(shù)據(jù)結構。正如前面提到的,這個信息通常包括例如文件句柄、數(shù)據(jù)庫連接結構和其他一些復雜類型的數(shù)據(jù)。
使用資源的主要原因是因為:資源被一個集中的隊列所管理,該隊列可以在PHP開發(fā)人員沒有在腳本里面顯式地釋放時可以自動地被釋放。
舉個例子,考慮到編寫一個腳本,在腳本里調用mysql_connect()打開一個MySQL連接,可是當該數(shù)據(jù)庫連接資源不再使用時卻沒有調用mysql_close()。在PHP里,資源機制能夠檢測什么時候這個資源應當被釋放,然后在當前請求的結尾或通常情況下更早地釋放資源。這就為減少內存泄漏賦予了一個“防彈”機制。如果沒有這樣一個機制,經過幾次web請求后,web服務器也許會潛在地泄漏許多內存資源,從而導致服務器當機或出錯。
注冊資源類型
如何使用資源?Zend引擎讓使用資源變地非常容易。你要做的第一件事就是把資源注冊到引擎中去。使用這個API函數(shù):
int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, char *type_name, int module_number)
這個函數(shù)返回一個資源類型id,該id應當被作為全局變量保存在擴展里,以便在必要的時候傳遞給其他資源API。ld:該資源釋放時調用的函數(shù)。pld用于在不同請求中始終存在的永久資源,本章不會涉及。type_name是一個具有描述性類型名稱的字符串,module_number為引擎內部使用,當我們調用這個函數(shù)時,我們只需要傳遞一個已經定義好的module_number變量。
回到我們的例子中來:我們會添加下面的代碼到myfile.c原文件中。該文件包括了資源釋放函數(shù)的定義,此資源函數(shù)被傳遞給zend_register_list_destructors_ex()注冊函數(shù)(資源釋放函數(shù)應該提早添加到文件中,以便在調用zend_register_list_destructors_ex()時該函數(shù)已被定義):
. 代碼如下:
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){
/* If you have INI entries, uncomment these lines
ZEND_INIT_MODULE_GLOBALS(myfile, php_myfile_init_globals,NULL);
REGISTER_INI_ENTRIES();
*/
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()是一個先于模塊(擴展)的啟動函數(shù),是暴露給擴展的一部分API。下表提供可用函數(shù)簡要的說明。
函數(shù)聲明宏 |
語義 |
PHP_MINIT_FUNCTION() |
當PHP被裝載時,模塊啟動函數(shù)即被引擎調用。這使得引擎做一些例如資源類型,注冊INI變量等的一次初始化。 |
PHP_MSHUTDOWN_FUNCTION() |
當PHP完全關閉時,模塊關閉函數(shù)即被引擎調用。通常用于注銷INI條目 |
PHP_RINIT_FUNCTION() |
在每次PHP請求開始,請求前啟動函數(shù)被調用。通常用于管理請求前邏輯。 |
PHP_RSHUTDOWN_FUNCTION() |
在每次PHP請求結束后,請求前關閉函數(shù)被調用。經常應用在清理請求前啟動函數(shù)的邏輯。 |
PHP_MINFO_FUNCTION() |
調用phpinfo()時模塊信息函數(shù)被呼叫,從而打印出模塊信息。 |
新建和注冊新資源 我們準備實現(xiàn)file_open()函數(shù)。當我們打開文件得到一個FILE *,我們需要利用資源機制注冊它。下面的主要宏實現(xiàn)注冊功能:
. 代碼如下:
ZEND_REGISTER_RESOURCE(rsrc_result, rsrc_pointer, rsrc_type);
參考表格對宏參數(shù)的解釋
ZEND_REGISTER_RESOURCE 宏參數(shù)
宏參數(shù) |
參數(shù)類型 |
rsrc_result |
zval *, which should be set with the registered resource information. zval * 設置為已注冊資源信息 |
rsrc_pointer |
Pointer to our resource data. 資源數(shù)據(jù)指針 |
rsrc_type |
The resource id obtained when registering the resource type. 注冊資源類型時獲得的資源id |
文件函數(shù)
現(xiàn)在你知道了如何使用ZEND_REGISTER_RESOURCE()宏,并且準備好了開始編寫file_open()函數(shù)。還有一個主題我們需要講述。
當PHP運行在多線程服務器上,不能使用標準的C文件存取函數(shù)。這是因為在一個線程里正在運行的PHP腳本會改變當前工作目錄,因此另外一個線程里的腳本使用相對路徑則無法打開目標文件。為了阻止這種錯誤發(fā)生,PHP框架提供了稱作VCWD (virtual current working directory 虛擬當前工作目錄)宏,用來代替任何依賴當前工作目錄的存取函數(shù)。這些宏與被替代的函數(shù)具備同樣的功能,同時是被透明地處理。在某些沒有標準C函數(shù)庫平臺的情況下,VCWD框架則不會得到支持。例如,Win32下不存在chown(),就不會有相應的VCWD_CHOWN()宏被定義。
VCWD列表
標準C庫 |
VCWD宏 |
getcwd() |
VCWD_GETCWD() |
fopen() |
VCWD_FOPEN |
open() |
VCWD_OPEN() //用于兩個參數(shù)的版本 |
open() |
VCWD_OPEN_MODE() //用于三個參數(shù)的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函數(shù)
實現(xiàn)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);
}
你可能會注意到資源注冊宏的第一個參數(shù)return_value,可此地找不到它的定義。這個變量自動的被擴展框架定義為zval * 類型的函數(shù)返回值。先前討論的、能夠影響返回值的RETURN_LONG() 和RETVAL_BOOL()宏確實改變了return_value的值。因此很容易猜到程序注冊了我們取得的文件指針fp,同時設置return_value為該注冊資源。
訪問資源 需要使用下面的宏訪問資源(參看表對宏參數(shù)的解釋)
. 代碼如下:
ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id, default_id, resource_type_name, resource_type);
ZEND_FETCH_RESOURCE 宏參數(shù)
參數(shù) |
含義 |
rsrc |
資源值保存到的變量名。它應該和資源有相同類型。 |
rsrc_type |
rsrc的類型,用于在內部把資源轉換成正確的類型 |
passed_id |
尋找的資源值(例如zval **) |
default_id |
如果該值不為-1,就使用這個id。用于實現(xiàn)資源的默認值。 |
resource_type_name |
資源的一個簡短名稱,用于錯誤信息。 |
resource_type |
注冊資源的資源類型id |
使用這個宏,我們現(xiàn)在能夠實現(xiàn)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 eof also if there was an error */
RETURN_TRUE;
}
RETURN_FALSE;
}
刪除一個資源通常使用下面這個宏刪除一個資源:
. 代碼如下:
int zend_list_delete(int id)
傳遞給宏一個資源id,返回SUCCESS或者FAILURE。如果資源存在,優(yōu)先從Zend資源列隊中刪除,該過程中會調用該資源類型的已注冊資源清理函數(shù)。因此,在我們的例子中,不必取得文件指針,調用fclose()關閉文件,然后再刪除資源。直接把資源刪除掉即可。
使用這個宏,我們能夠實現(xiàn)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()從參數(shù)列表中取得資源的時候,得到的是zval的形式。為了獲得資源id,我們使用Z_RESVAL_P()宏得到id,然后把id傳遞給zend_list_delete()。
有一系列宏用于訪問存儲于zval值(參考表的宏列表)。盡管在大多數(shù)情況下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 |
聯(lián)合數(shù)組 |
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。它們的區(qū)別是在命名上,第一個沒有后綴,zval *有后綴_P(代表一個指針),最后一個 zval **有后綴_PP(代表兩個指針)。
現(xiàn)在,你有足夠的信息來獨立完成 file_read()和 file_write()函數(shù)。這里是一個可能的實現(xiàn):
. 代碼如下:
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;
}
測試擴展
你現(xiàn)在可以編寫一個測試腳本來檢測擴展是否工作正常。下面是一個示例腳本,該腳本打開文件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在下一節(jié)中討論)。因為PHP是為多線程環(huán)境而設計,所以不必定義全局變量。PHP提供了一個創(chuàng)建全局變量的機制,可以同時應用在線程和非線程環(huán)境中。我們應當始終利用這個機制,而不要自主地定義全局變量。用一個宏訪問這些全局變量,使用起來就像普通全局變量一樣。
用于生成myfile工程骨架文件的ext_skel腳本創(chuàng)建了必要的代碼來支持全局變量。通過檢查php_myfile.h文件,你應當發(fā)現(xiàn)類似下面的被注釋掉的一節(jié),
. 代碼如下:
ZEND_BEGIN_MODULE_GLOBALS(myfile)
int global_value;
char *global_string;
ZEND_END_MODULE_GLOBALS(myfile)
你可以把這一節(jié)的注釋去掉,同時添加任何其他全局變量于這兩個宏之間。文件后部的幾行,骨架腳本自動地定義一個MYFILE_G(v)宏。這個宏應當被用于所有的代碼,以便訪問這些全局變量。這就確保在多線程環(huán)境中,訪問的全局變量僅是一個線程的拷貝,而不需要互斥的操作。
為了使全局變量有效,最后需要做的是把myfile.c:
. 代碼如下:
ZEND_DECLARE_MODULE_GLOBALS(myfile)
注釋去掉。
你也許希望在每次PHP請求的開始初始化全局變量。另外,做為一個例子,全局變量已指向了一個已分配的內存,在每次PHP請求結束時需要釋放內存。為了達到這些目的,全局變量機制提供了一個特殊的宏,用于注冊全局變量的構造和析構函數(shù)(參考表對宏參數(shù)的說明):
. 代碼如下:
ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)
表 ZEND_INIT_MODULE_GLOBALS 宏參數(shù)
參數(shù) |
含義 |
module_name |
與傳遞給ZEND_BEGIN_MODULE_GLOBALS()宏相同的擴展名稱。 |
globals_ctor |
構造函數(shù)指針。在myfile擴展里,函數(shù)原形與void php_myfile_init_globals(zend_myfile_globals *myfile_globals)類似 |
globals_dtor |
析構函數(shù)指針。例如,php_myfile_init_globals(zend_myfile_globals *myfile_globals) |
你可以在myfile.c里看到如何使用構造函數(shù)和ZEND_INIT_MODULE_GLOBALS()宏的示例。
添加自定義INI指令
INI文件(php.ini)的實現(xiàn)使得PHP擴展注冊和監(jiān)聽各自的INI條目。如果這些INI條目由php.ini、Apache的htaccess或其他配置方法來賦值,注冊的INI變量總是更新到正確的值。整個INI框架有許多不同的選項以實現(xiàn)其靈活性。我們涉及一些基本的(也是個好的開端),借助本章的其他材料,我們就能夠應付日常開發(fā)工作的需要。
通過在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()其他宏也能夠使用,但這個宏是最常用的,可以滿足大多數(shù)需要(參看表對宏參數(shù)的說明):
. 代碼如下:
STD_PHP_INI_ENTRY(name, default_value, modifiable, on_modify, property_name, struct_type, struct_ptr)
STD_PHP_INI_ENTRY 宏參數(shù)表
參數(shù) |
含義 |
name |
INI條目名 |
default_value |
如果沒有在INI文件中指定,條目的默認值。默認值始終是一個字符串。 |
modifiable |
設定在何種環(huán)境下INI條目可以被更改的位域。可以的值是:
• PHP_INI_SYSTEM. 能夠在php.ini或http.conf等系統(tǒng)文件更改
• PHP_INI_PERDIR. 能夠在 .htaccess中更改
• PHP_INI_USER. 能夠被用戶腳本更改
• PHP_INI_ALL. 能夠在所有地方更改 |
on_modify |
處理INI條目更改的回調函數(shù)。你不需自己編寫處理程序,使用下面提供的函數(shù)。包括:
• 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)的值會變?yōu)?9。
. 代碼如下:
; php.ini – The following line sets the INI entry myfile.global_value to 99.myfile.global_value = 9
線程安全資源管理宏
現(xiàn)在,你肯定注意到以TSRM(線程安全資源管理器)開頭的宏隨處使用。這些宏提供給擴展擁有獨自的全局變量的可能,正如前面提到的。
當編寫PHP擴展時,無論是在多進程或多線程環(huán)境中,都是依靠這一機制訪問擴展自己的全局變量。如果使用全局變量訪問宏(例如MYFILE_G()宏),需要確保TSRM上下文信息出現(xiàn)在當前函數(shù)中?;谛阅艿脑颍琙end引擎試圖把這個上下文信息作為參數(shù)傳遞到更多的地方,包括PHP_FUNCTION()的定義。正因為這樣,在PHP_FUNCTION()內當編寫的代碼使用訪問宏(例如MYFILE_G()宏)時,不需要做任何特殊的聲明。然而,如果PHP函數(shù)調用其他需要訪問全局變量的C函數(shù),要么把上下文作為一個額外的參數(shù)傳遞給C函數(shù),要么提取上下文(要慢點)。
在需要訪問全局變量的代碼塊開頭使用TSRMLS_FETCH()來提取上下文。例如:
. 代碼如下:
void myfunc(){
TSRMLS_FETCH();
MYFILE_G(myglobal) = 2;
}
如果希望讓代碼更加優(yōu)化,更好的辦法是直接傳遞上下文給函數(shù)(正如前面敘述的,PHP_FUNCTION()范圍內自動可用)??梢允褂肨SRMLS_C(C表示調用Call)和TSRMLS_CC(CC邊式調用Call和逗號Comma)宏。前者應當用于僅當上下文作為一個單獨的參數(shù),后者應用于接受多個參數(shù)的函數(shù)。在后一種情況中,因為根據(jù)取名,逗號在上下文的前面,所以TSRMLS_CC不能是第一個函數(shù)參。
在函數(shù)原形中,可以分別使用TSRMLS_D和TSRMLS_DC宏聲名正在接收上下文。
下面是前一例子的重寫,利用了參數(shù)傳遞上下文。
. 代碼如下:
void myfunc(TSRMLS_D){
MYFILE_G(myglobal) = 2;
}
PHP_FUNCTION(my_php_function)
{
…
myfunc(TSRMLS_C);
…
}
~
總 結
現(xiàn)在,你已經學到了足夠的東西來創(chuàng)建自己的擴展。本章講述了一些重要的基礎來編寫和理解PHP擴展。Zend引擎提供的擴展API相當豐富,使你能夠開發(fā)面向對象的擴展。幾乎沒有文檔談幾許多高級特性。當然,依靠本章所學的基礎知識,你可以通過瀏覽現(xiàn)有的原碼學到很多。
更多關于信息可以在PHP手冊的擴展PHP章節(jié)http://www.php.net/manual/en/zend.php中找到。另外,你也可以考慮加入PHP開發(fā)者郵件列表internals@ lists.php.net,該郵件列表圍繞開發(fā)PHP 本身。你還可以查看一下新的擴展生成工具——PECL_Gen(http://pear.php.net/package/PECL_Gen),這個工具正在開發(fā)之中,比起本章使用的ext_skel有更多的特性。
此外你還可以關注風雪之隅, 會有更多相關知識更新.
詞匯表
binary safe |
二進制安全 |
context |
上下文 |
extensions |
擴展 |
entry |
條目 |
skeleton |
骨架 |
Thread-Safe Resource Manager TSRM |
線程安全資源管理器 |
[1] 可參考譯者寫的
[2] 譯者:可以使用phpcli程序在控制臺里執(zhí)行php文件。
[3] 譯者:可以查看到生成的FETCH_RESOURCE()宏參數(shù)是一些'???'。
該文章在 2012/9/27 10:10:02 編輯過