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,改用我下面的方法來加載吧:
  1. if(!extension_loaded("hello")) {
  2.         dl_local("hello.so");
  3. }
  4. function dl_local( $extensionFile ) {
  5.         //make sure that we are ABLE to load libraries
  6.         if( !(bool)ini_get"enable_dl" ) || (bool)ini_get"safe_mode" ) ) {
  7.                 die"dh_local(): Loading extensions is not permitted./n" );
  8.         }
  9.         //check to make sure the file exists
  10.         if( !file_exists(dirname(__FILE__) . "/"$extensionFile ) ) {
  11.                 die"dl_local(): File '$extensionFile' does not exist./n" );
  12.         }
  13.         //check the file permissions
  14.         if( !is_executable(dirname(__FILE__) . "/"$extensionFile ) ) {
  15.                 die"dl_local(): File '$extensionFile' is not executable./n" );
  16.         }
  17.         //we figure out the path
  18.         $currentDir = dirname(__FILE__) . "/";
  19.         $currentExtPath = ini_get"extension_dir" );
  20.         $subDirs = preg_match_all( "////" , $currentExtPath , $matches );
  21.         unset( $matches );
  22.         //lets make sure we extracted a valid extension path
  23.         if( !(bool)$subDirs ) {
  24.                 die"dl_local(): Could not determine a valid extension path [extension_dir]./n" );
  25.         }
  26.         $extPathLastChar = strlen$currentExtPath ) - 1;
  27.         if$extPathLastChar == strrpos$currentExtPath , "/" ) ) {
  28.                 $subDirs--;
  29.         }
  30.         $backDirStr = ""
  31.         for$i = 1; $i <= $subDirs$i++ ) {
  32.                 $backDirStr .= "..";
  33.                 if$i != $subDirs ) {
  34.                   $backDirStr .= "/";
  35.                 }
  36.         }
  37.         //construct the final path to load
  38.         $finalExtPath = $backDirStr . $currentDir . $extensionFile;
  39.         //now we execute dl() to actually load the module
  40.         if( !dl( $finalExtPath ) ) {
  41.                 die();
  42.         }
  43.         //if the module was loaded correctly, we must bow grab the module name
  44.         $loadedExtensions = get_loaded_extensions();
  45.         $thisExtName = $loadedExtensions[ sizeof( $loadedExtensions ) - 1 ];
  46.         //lastly, we return the extension name
  47.         return $thisExtName;
  48. }//end dl_local()
這樣的好處是你的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裏面添加了一行信息,表示你這個模塊中有這個函數了。

那麼都是一樣的函數原型,如何區分返回值與參數呢?
我給一個例子:
  1. PHP_FUNCTION(hello_strdiff)
  2. {
  3.     char *r1 = NULL, *r2 = NULL;
  4.     int n = 0, m = 0;
  5.     if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &r1, &n, &r2, &m) == FAILURE) {
  6.         return;
  7.     }
  8.     while(n && m && *r1 == *r2) {
  9.         r1++;
  10.         r2++;
  11.         n--;
  12.         m--;
  13.     }
  14.     if(n == 0) RETURN_LONG(m);
  15.     if(m == 0) RETURN_LONG(n);
  16.     int d[n+1][m+1];
  17.     int cost;
  18.     int i,j;
  19.     for(i = 0; i <= n; i++) d[i][0] = i;
  20.     for(j = 0; j <= m; j++) d[0][j] = j;
  21.     for(i = 1; i <= n; i++) {
  22.         for(j = 1; j <= m; j++) {
  23.             if(r1[i-1] == r2[j-1]) cost = 0;
  24.             else cost = 1;
  25.             int a = MIN(d[i-1][j]+1,d[i][j-1]+1);
  26.             a = MIN(a, d[i-1][j-1]+cost);
  27.             d[i][j] = a;
  28.         }
  29.     }
  30.     RETURN_LONG(d[n][m]);
  31. }
這是一個求兩個字符串差異度的算法,輸入參數兩個字符串,返回整型。
參數的傳遞看這裏
zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &r1, &n, &r2, &m)
把這個當成是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*
如果想實現可選參數的話,例如一個字符串,一個浮點,再加一個可選的bool型,可以用"sd|b"來表示。
和scanf有一點不同的是,對於字符串,你要提供兩個變量來存儲,一個是char *,存字符串的地址,一個int,來存字符串的長度。這樣有必要的時候,你可以安全的處理二進制數據。

那麼返回值怎麼辦呢?
使用下面一組宏來表示:
RETURN_STRING
RETURN_LONG
RETURN_DOUBLE
RETURN_BOOL
RETURN_NULL
注意RETURN_STRING有兩個參數
當你需要複製一份字符串時使用
RETURN_STRING("Hello World", 1);
否則使用
RETURN_STRING(str, 0);
這裏涉及到了模塊中內存的分配,當你申請的內存需要php程序中去釋放的話,請參照如下表
Traditional Non-Persistent Persistent
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)
一般我們使用Non-Persistent中列出的這些好了。

基本上就是這樣,可以開始寫一個php的擴展了。
從我目前的應用來看,能操縱字符串就夠用了,所以我就只能介紹這麼多了,如果要詳細一點的呢,例如php數組怎麼處理,可以參考

更好的文章:http://www.toplee.com/blog/56.html#pp1

更詳細的呢,可以參考php手冊中的《Zend API:深入 PHP 內核》一章
不過這些資料都是英文的。
 
 php擴展基礎

本節沒有介紹關於腳本引擎基本構造的一些知識,而是直接進入擴展的編碼講解中,因此不要擔心你無法立刻獲得對擴展整體把握的感覺。假設你正在開發一個網站,需要一個把字符串重複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腳本生成的骨架結構:

 

/* {{{ 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函數週圍包含了一些註釋,這些註釋用於自動生成代碼文檔和viEmacs等編輯器的代碼摺疊。函數自身的定義使用了宏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

邏輯型(10

r

zval *

資源(文件指針,數據庫連接等)

a

zval *

聯合數組

o

zval *

任何類型的對象

O

zval *

指定類型的對象。需要提供目標對象的類類型

z

zval *

無任何操作的zval 

 

爲了容易地理解最後幾個選項的含義,你需要知道zvalZend引擎的值容器[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_lenn得到同樣的值:

 

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等於3n等於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)

資源句柄。

 

發佈了16 篇原創文章 · 獲贊 6 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章