翻譯:taft at wjl.cn
校對:laruence at yahoo.com.cn
最后更新日期:2009/04/29

簡 介

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寫的例子:

  1. function self_concat($string, $n){
  2.     $result = "";
  3. for($i = 0; $i < $n; $i++){
  4.     $result .= $string;
  5. }
  6.     return $result;
  7. }
  8.  
  9. self_concat("One", 3) returns "OneOneOne".
  10.  
  11. self_concat("One", 1) returns "One".

假設由于一些奇怪的原因,你需要時常調用這個函數,而且還要傳給函數很長的字符串和大值n。這意味著在腳本里有相當巨大的字符串連接量和內存重新分配過程,以至顯著地降低腳本執行速度。如果有一個函數能夠更快地分配大量且足夠的內存來存放結果字符串,然后把$string重復n次,就不需要在每次循環迭代中分配內存。

為擴展建立函數的第一步是寫一個函數定義文件,該函數定義文件定義了擴展對外提供的函數原形。該例中,定義函數只有一行函數原形self_concat() :

  1. 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,運行下面的命令來建立擴展骨架

  1. ./ext_skel --extname=myfunctions --proto=myfunctions.def

這個命令在ext/目錄下建立了一個myfunctions/目錄。你要做的第一件事情也許就是編譯該骨架,以便編寫和測試實際的C代碼。編譯擴展有兩種方法:

  • 作為一個可裝載模塊或者DSO(動態共享對象)
  • 靜態編譯到PHP
PHP擴展開發導圖

PHP擴展開發導圖

因為第二種方法比較容易上手,所以本章采用靜態編譯。如果你對編譯可裝載擴展模塊感興趣,可以閱讀PHP原代碼根目錄下的README.SELF-CONTAINED_EXTENSIONS文件。為了使擴展能夠被編譯,需要修改擴展目錄ext/myfunctions/下的config.m4文件。擴展沒有包裹任何外部的C庫,你需要添加支持–enable-myfunctions配置開關到PHP編譯系統里(–with-extension 開關用于那些需要用戶指定相關C庫路徑的擴展)。可以去掉自動生成的下面兩行的注釋來開啟這個配置。

  1. ./ext_skel --extname=myfunctions --proto=myfunctions.def
  2. PHP_ARG_ENABLE(myfunctions, whether to enable myfunctions support,
  3.  
  4. [ --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的擴展開發后,可以把后者去掉。

  1. <?php
  2. print confirm_myfunctions_compiled("myextension");
  3. ?>

運行這個腳本會出現類似下面的輸出:

  1. "Congratulations! You have successfully modified ext/myfunctions
  2.  
  3. config.m4. Module myfunctions is now compiled into PHP."

另外,ext_skel腳本生成一個叫myfunctions.php的腳本,你也可以利用它來驗證擴展是否被成功地編譯到PHP。它會列出該擴展所支持的所有函數。

現在你學會如何編譯擴展了,該是真正地研究self_concat()函數的時候了。
下面就是ext_skel腳本生成的骨架結構:

  1. /* {{{ proto string self_concat(string str, int n)
  2.  
  3. */
  4.  
  5. PHP_FUNCTION(self_concat)
  6.  
  7. }
  8.  
  9. char *str = NULL;
  10.  
  11. int argc = ZEND_NUM_ARGS();
  12.  
  13. int str_len;
  14.  
  15. long n;
  16.  
  17. if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
  18.  
  19. return;
  20.  
  21. php_error(E_WARNING, "self_concat: not yet implemented");
  22.  
  23. }
  24.  
  25. /* }}} */

 

自動生成的PHP函數周圍包含了一些注釋,這些注釋用于自動生成代碼文檔和vi、Emacs等編輯器的代碼折疊。函數自身的定義使用了宏PHP_FUNCTION(),該宏可以生成一個適合于Zend引擎的函數原型。邏輯本身分成語義各部分,取得調用函數的參數和邏輯本身。

為了獲得函數傳遞的參數,可以使用zend_parse_parameters()API函數。下面是該函數的原型:

  1. 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, 以便我們能更好地理解接下來的代碼。

  1. typedef union _zval{
  2. long lval;
  3. double dval;
  4. struct {
  5. char *val;
  6. int len;
  7. }str;
  8.  
  9. HashTable *ht;
  10. zend_object_value obj;
  11.  
  12. }zval;

 

在我們的例子中,我們用基本類型調用zend_parse_parameters(),以本地C類型的方式取得函數參數的值,而不是用zval容器。

為了讓zend_parse_parameters()能夠改變傳遞給它的參數的值,并返回這個改變值,需要傳遞一個引用。仔細查看一下self_concat():

  1. if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
  2. 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得到同樣的值:

  1. self_concat("321", 5);
  2.  
  3. self_concat(321, "5");
  4.  
  5. self_concat("321", "5");
  6.  
  7. str points to the string "321", str_len equals 3, and n equals 5.
  8.  
  9. 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()的編碼:

  1. /* {{{ proto string self_concat(string str, int n)
  2.  
  3. */
  4.  
  5. PHP_FUNCTION(self_concat)
  6.  
  7. }
  8.  
  9. char *str = NULL;
  10.  
  11. int argc = ZEND_NUM_ARGS();
  12.  
  13. int str_len;
  14.  
  15. long n;
  16.  
  17. char *result; /* Points to resulting string */
  18.  
  19. char *ptr; /* Points at the next location we want to copy to */
  20.  
  21. int result_length; /* Length of resulting string */
  22.  
  23. if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
  24.  
  25. return;
  26.  
  27. /* Calculate length of result */
  28.  
  29. result_length = (str_len * n);
  30.  
  31. /* Allocate memory for result */
  32.  
  33. result = (char *) emalloc(result_length + 1);
  34.  
  35. /* Point at the beginning of the result */
  36.  
  37. ptr = result;
  38.  
  39. while (n--) {
  40.  
  41. /* Copy str to the result */
  42.  
  43. memcpy(ptr, str, str_len);
  44.  
  45. /* Increment ptr to point at the next position we want to write to */
  46.  
  47. ptr += str_len;
  48.  
  49. }
  50.  
  51. /* Null terminate the result. Always null-terminate your strings
  52.  
  53. even if they are binary strings */
  54.  
  55. *ptr = '\0';
  56.  
  57. /* Return result to the scripting engine without duplicating it*/
  58.  
  59. RETURN_STRINGL(result, result_length, 0);
  60.  
  61. }
  62.  
  63. /* }}} */

現在要做的就是重新編譯一下PHP,這樣就完成了第一個PHP函數。

讓我門檢查函數是否真的工作。在最新編譯過的PHP樹下執行[2]下面的腳本:

  1. <?php
  2. for ($i = 1; $i <= 3; $i++){
  3.     print self_concat("ThisIsUseless", $i);
  4.     print "\n";
  5. }
  6. ?>

你應該得到下面的結果:

  1. ThisIsUseless
  2.  
  3. ThisIsUselessThisIsUseless
  4.  
  5. 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()時該函數已被定義):

  1. static void myfile_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC){
  2. FILE *fp = (FILE *) rsrc->ptr;
  3. fclose(fp);
  4. }

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

  1. PHP_MINIT_FUNCTION(myfile){
  2. /* If you have INI entries, uncomment these lines
  3. ZEND_INIT_MODULE_GLOBALS(myfile, php_myfile_init_globals,NULL);
  4.  
  5. REGISTER_INI_ENTRIES();
  6. */
  7.  
  8. le_myfile = zend_register_list_destructors_ex(myfile_dtor,NULL,"standard-c-file", module_number);
  9.  
  10. return SUCCESS;
  11. }

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 *,我們需要利用資源機制注冊它。下面的主要宏實現注冊功能:

  1. 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()應該非常簡單,看起來像下面的樣子:

  1. PHP_FUNCTION(file_open){
  2. char *filename = NULL;
  3. char *mode = NULL;
  4. int argc = ZEND_NUM_ARGS();
  5. int filename_len;
  6. int mode_len;
  7. FILE *fp;
  8.  
  9. if (zend_parse_parameters(argc TSRMLS_CC, "ss", &filename,&filename_len, &mode, &mode_len) == FAILURE) {
  10. return;
  11. }
  12.  
  13. fp = VCWD_FOPEN(filename, mode);
  14.  
  15. if (fp == NULL) {
  16. RETURN_FALSE;
  17. }
  18.  
  19. ZEND_REGISTER_RESOURCE(return_value, fp, le_myfile);
  20. }

 

你可能會注意到資源注冊宏的第一個參數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():

  1. PHP_FUNCTION(file_eof){
  2. int argc = ZEND_NUM_ARGS();
  3. zval *filehandle = NULL;
  4. FILE *fp;
  5.  
  6. if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) ==FAILURE) {
  7. return;
  8. }
  9.  
  10. ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-c-file",le_myfile);
  11.  
  12. if (fp == NULL){
  13. RETURN_FALSE;
  14. }
  15.  
  16. if (feof(fp) <= 0) {
  17. /* Return eof also if there was an error */
  18. RETURN_TRUE;
  19. }
  20.  
  21. RETURN_FALSE;
  22. }

 

 

刪除一個資源

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

int zend_list_delete(int id)

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

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

  1. PHP_FUNCTION(file_close){
  2. int argc = ZEND_NUM_ARGS();
  3. zval *filehandle = NULL;
  4.  
  5. if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) == FAILURE) {
  6. return;
  7. }
  8.  
  9. if (zend_list_delete(Z_RESVAL_P(filehandle)) == FAILURE) {
  10. RETURN_FALSE;
  11. }
  12.  
  13. RETURN_TRUE;
  14. }

 

你肯定會問自己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()函數。這里是一個可能的實現:

  1. PHP_FUNCTION(file_read){
  2. int argc = ZEND_NUM_ARGS();
  3. long size;
  4. zval *filehandle = NULL;
  5. FILE *fp;
  6. char *result;
  7. size_t bytes_read;
  8.  
  9. if (zend_parse_parameters(argc TSRMLS_CC, "rl", &filehandle,&size) == FAILURE) {
  10. return;
  11. }
  12.  
  13. ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-cfile", le_myfile);
  14.  
  15. result = (char *) emalloc(size+1);
  16.  
  17. bytes_read = fread(result, 1, size, fp);
  18.  
  19. result[bytes_read] = '\0';
  20.  
  21. RETURN_STRING(result, 0);
  22. }
  23.  
  24. PHP_FUNCTION(file_write){
  25. char *buffer = NULL;
  26. int argc = ZEND_NUM_ARGS();
  27. int buffer_len;
  28. zval *filehandle = NULL;
  29. FILE *fp;
  30.  
  31. if (zend_parse_parameters(argc TSRMLS_CC, "rs", &filehandle,&buffer, &buffer_len) == FAILURE) {
  32. return;
  33. }
  34.  
  35. ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-cfile", le_myfile);
  36.  
  37. if (fwrite(buffer, 1, buffer_len, fp) != buffer_len) {
  38. RETURN_FALSE;
  39. }
  40.  
  41. RETURN_TRUE;
  42. }

測試擴展

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

  1. <?php
  2. $fp_in = file_open("test.txt", "r") or die("Unable to open input file\n");
  3.  
  4. $fp_out = file_open("test.txt.new", "w") or die("Unable to open output file\n");
  5.  
  6. while (!file_eof($fp_in)) {
  7.     $str = file_read($fp_in, 1024);
  8.     print($str);
  9.     file_write($fp_out, $str);
  10. }
  11.  
  12. file_close($fp_in);
  13. file_close($fp_out);
  14. ?>

全局變量

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

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

  1. ZEND_BEGIN_MODULE_GLOBALS(myfile)
  2.  
  3. int global_value;
  4. char *global_string;
  5.  
  6. 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中的注冊過程應當如下:

  1. PHP_INI_BEGIN()
  2.  
  3. STD_PHP_INI_ENTRY("myfile.global_value", "42", PHP_INI_ALL, OnUpdateInt, global_value, zend_myfile_globals, myfile_globals)
  4.  
  5. STD_PHP_INI_ENTRY("myfile.global_string", "foobar", PHP_INI_ALL, OnUpdateString, global_string, zend_myfile_globals, myfile_globals)
  6.  
  7. 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()來提取上下文。例如:

  1. void myfunc(){
  2. TSRMLS_FETCH();
  3.  
  4. MYFILE_G(myglobal) = 2;
  5. }

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

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

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

  1. void myfunc(TSRMLS_D){
  2. MYFILE_G(myglobal) = 2;
  3. }
  4.  
  5. PHP_FUNCTION(my_php_function)
  6. {
  7. myfunc(TSRMLS_C);
  8. }

 

總 結

現在,你已經學到了足夠的東西來創建自己的擴展。本章講述了一些重要的基礎來編寫和理解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 at wjl.cn / laruence at yahoo.com.cn
http://www.laruence.com

——————————————————————————–
[1] 可參考譯者寫的
[2] 譯者:可以使用phpcli程序在控制臺里執行php文件。
[3] 譯者:可以查看到生成的FETCH_RESOURCE()宏參數是一些’???’。