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()php
快速開始html
本節沒有介紹關於腳本引擎基本構造的一些知識,而是直接進入擴展的編碼講解中,所以不要擔憂你沒法馬上得到對擴展總體把握的感受。假設你正在開發一個網站,須要一個把字符串重複n次的函數。下面是用PHP寫的例子:mysql
function self_concat($string, $n)程序員
{web
$result = "";算法
for ($i = 0; $i < $n; $i++) {sql
$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腳本生成的骨架結構:
/* {{{ 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函數週圍包含了一些註釋,這些註釋用於自動生成代碼文檔和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引擎的值容器。不管這個變量是布爾型,字符串型或者其餘任何類型,其信息總會包含在一個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()的編碼:
/* {{{ 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);
}
/* }}} */
如今要作的就是從新編譯一下PHP,這樣就完成了第一個PHP函數。
讓我門檢查函數是否真的工做。在最新編譯過的PHP樹下執行下面的腳本:
<?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()宏行的編譯錯誤,這樣骨架腳本就沒法順利完成編譯。爲了讓骨架擴展順利經過編譯,把那些出錯行註釋掉便可。
資源 資源是一個能容納任何信息的抽象數據結構。正如前面提到的,這個信息一般包括例如文件句柄、數據庫鏈接結構和其餘一些複雜類型的數據。
使用資源的主要緣由是由於:資源被一個集中的隊列所管理,該隊列能夠在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)
{
/* 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()是一個先於模塊(擴展)的啓動函數,是暴露給擴展的一部分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 eof also if there was an error */
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有更多的特性。