php调用C代码的方法详解和zend_parse_parameters函数详解
來源:http://my.oschina.net/Customs/blog/490873
http://blog.csdn.net/super_ufo/article/details/3863731
?
?
php調用C代碼的方法詳解
在php程序中需要用到C代碼,應該是下面兩種情況:
1 已有C代碼,在php程序中想直接用 2 由于php的性能問題,需要用C來實現部分功能針對第一種情況,最合適的方法是用system調用,把現有C代碼寫成一個獨立的程序。參數通過命令行或者標準輸入傳入,結果從標準輸出讀出。其次,稍麻煩一點的方法是C代碼寫成一個daemon,php程序用socket來和它進行通訊。
重點講講第二種情況,雖然沿用system調用的方法也可以,但是想想你的目的是優化性能,那么頻繁的起這么多進程,當然會讓性能下降。而寫daemon的方法固然可行,可是繁瑣了很多。
我的簡單測試,同樣一個算法,用C來寫比用php效率能提高500倍。而用php擴展的方式,也能提高90多倍(其中的性能損失在了參數傳遞上了吧,我猜)。
所以有些時候php擴展就是我們的最佳選擇了。
這里我著重介紹一下用C寫php擴展的方法,而且不需要重新編譯php。
首先,找到一個php的源碼,php4或者php5版本的都可以,與你目標平臺的php版本沒有關系。
在源碼的ext目錄下可以找到名為ext_skel的腳本(windows平臺使用ext_skel_win32.php) 在這個目錄下執行./ext_skel?--extname=hello(我用hello作為例子) 這時生成了一個目錄 hello,目錄下有幾個文件,你只需要關心這三個:config.m4?hello.c php_hello.h
把這個目錄拷備到任何你希望的地方,cd進去,依次執行 (安裝phpize等工具 yum -y install php-devel ) phpize ./configure make 什么也沒發生,對吧? 這是因為漏了一步,打開config.m4,找到下面 dnl If your extension references something external, use with:
... dnl Otherwise use enable:
... 這是讓你選擇你的擴展使用with還是enable,我們用with吧。把with那一部分取消注釋。 如果你和我一樣使用vim編輯器,你就會很容易發現dnl三個字母原來是表示注釋的呀(這是因為vim默認帶了各種文件格式的語法著色包)
我們修改了config.m4后,繼續 phpize ./configure make 這時,modules下面會生成hello.so和hello.la文件。一個是動態庫,一個是靜態庫。
你的php擴展已經做好了,盡管它還沒有實現你要的功能,我先說說怎么使用這個擴展吧!ext_skel為你生成了一個hello.php里面有調用示例,但是那個例子需要你把hello.so拷貝到php的擴展目錄中去,我們只想實現自己的功能,不想打造山寨版php,改用我下面的方法來加載吧:
隨后一個讓人關心的問題是,如何添加函數、實現參數傳遞和返回值
添加函數步驟如下: php_hello.h: PHP_FUNCTION(confirm_hello_compiled);// 括號里面填寫函數名
hello.c zend_function_entry hello_functions[] = { ?? ?PHP_FE(confirm_hello_compiled, ?NULL) ? ? ? /* 這里添加一行 */ ?? ?{NULL, NULL, NULL} ?/* Must be the last line in hello_functions[] */ }; PHP_FUNCTION(confirm_hello_compiled)? {// 這里寫函數體 } 要實現的函數原型其實都一個樣,用宏PHP_FUNCTION來包裝了一下,另外呢,在hello_functions里面添加了一行信息,表示你這個模塊中有這個函數了。
那么都是一樣的函數原型,如何區分返回值與參數呢? 我給一個例子:
把這個當成是scanf來理解好了。 類型說明見下表:
| Boolean | b | zend_bool |
| Long | l | long |
| Double | d | double |
| String | s | char*, int |
| Resource | r | zval* |
| Array | a | zval* |
| Object | o | zval* |
| zval | z | zval* |
那么返回值怎么辦呢? 使用下面一組宏來表示: RETURN_STRING
RETURN_LONG
RETURN_DOUBLE
RETURN_BOOL
RETURN_NULL
注意RETURN_STRING有兩個參數 當你需要復制一份字符串時使用 RETURN_STRING("Hello World", 1);
否則使用 RETURN_STRING(str, 0);
這里涉及到了模塊中內存的分配,當你申請的內存需要php程序中去釋放的話,請參照如下表
| malloc(count) calloc(count, num) | emalloc(count) ecalloc(count, num) | pemalloc(count, 1)* pecalloc(count, num, 1) |
| strdup(str) strndup(str, len) | estrdup(str) estrndup(str, len) | pestrdup(str, 1) pemalloc() & memcpy() |
| free(ptr) | efree(ptr) | pefree(ptr, 1) |
| realloc(ptr, newsize) | erealloc(ptr, newsize) | perealloc(ptr, newsize, 1) |
| malloc(count * num + extr)** | safe_emalloc(count, num, extr) | safe_pemalloc(count, num, extr) |
基本上就是這樣,可以開始寫一個php的擴展了。 從我目前的應用來看,能操縱字符串就夠用了,所以我就只能介紹這么多了,如果要詳細一點的呢,例如php數組怎么處理,可以參考 http://devzone.zend.com/node/view/id/1022 翻譯:http://blog.csdn.net/alexdream/archive/2008/03/24/2213344.aspx
更好的文章:http://www.toplee.com/blog/56.html#pp1
本節沒有介紹關于腳本引擎基本構造的一些知識,而是直接進入擴展的編碼講解中,因此不要擔心你無法立刻獲得對擴展整體把握的感覺。假設你正在開發一個網站,需要一個把字符串重復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輸出信息,可以檢查新的配置選項是否被包含到配置文件中?,F在,打開你喜好的配置選項開關和--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");
}
/*?}}}?*/
? ? ? ? ? ? zend_parse_parameters 詳解自動生成的PHP函數周圍包含了一些注釋,這些注釋用于自動生成代碼文檔和vi、Emacs等編輯器的代碼折疊。函數自身的定義使用了宏PHP_FUNCTION(),該宏可以生成一個適合于Zend引擎的函數原型。邏輯本身分成語義各部分,取得調用函數的參數和邏輯本身。
???????為了獲得函數傳遞的參數,可以使用zend_parse_parameters()API函數。下面是該函數的原型:
zend_parse_parameters(int?num_args?TSRMLS_DC,?char?*type_spec,?…);
?
第一個參數是傳遞給函數的參數個數。通常的做法是傳給它ZEND_NUM_ARGS()。(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) | 資源句柄。 |
? ?
總結
以上是生活随笔為你收集整理的php调用C代码的方法详解和zend_parse_parameters函数详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 十六字令三首(说一说十六字令三首的简介)
- 下一篇: 云南新华电脑学校,学航空轨道真的可以当空