http://devzone.zend.com/node/view/id/1021 簡介
文中所用的教程需要您對PHP和PHP的解譯器(用C語言開發)有初步的瞭解.
首先來確認為什麼我們需要寫出一個新的PHP擴充.
1. 由於不同語言之間的差別,導致這些以這些語言開發的共用庫不能夠由PHP直接調用,但我們確實很需要這些共用庫所已有的功能
2. 某些情況下,需要使PHP自身具有一些非常規的特性
3.我們已經獲得了一些php代碼,但是我們想讓它啟動並執行更快,更精緻並且耗費的資源更少
4.你已經開發除了很重要的代碼需要出售,但只想讓購買方擁有可執行邏輯而不是原始碼。
或許還有其它更多的原因,但是在能夠創造出一個擴充之前,我們首先需要瞭解什麼是PHP擴充。
什麼是擴充
只要你在使用PHP,你就已經在使用php擴充了。除了少數情況外,所有PHP中所有使用者可以調用的方法都被分到不同的php擴充中實現的。這些方法中的大部分都是php標準擴充的一部分。PHP源碼內建了85個擴充,平均每個擴充包含30個方法。對於數學計算,大約有2500個方法。但是這仍然是不夠的,PECL庫提供了額外的超過100個以上的擴充.
“如果所有的方法都是由擴充實現,那擴充隸屬於什麼,PHP的核心(core)又是什嗎?",有人會這麼問.
PHP的core由兩個獨立的部分構成。處於最底層的是Zend引擎, 它負責把程式員寫的程式轉變為機器可以識別的標識符,然後運行這些符號。此外,它還完成記憶體管理、變數範圍和指定方法調用等工作。另外的一部分就是PHP Core了。它負責處理於SAPI(Server Application Programming Interface, 通常指的是運行環境-Apache,IIS,CLI,CGI等)層的互動和綁定. 此外也提供了對一些如safe_mode和open_basedir的 檢查,以及使用者空間使用fopen(),fread()和fwrite()方法與檔案系統和網路I/O相關的流的處理。
當SAPI啟動時,例如通過 /usr/local/apache/bin/apachectl start這樣的方式,PHP就會初始化它的core子系統。當這一步驟完成後,它就開始逐個載入所有的擴充的代碼並調用每一個擴充的Module Initialization 方法。該方法給了擴充初始化內部變數、分配資源、註冊資源控制代碼和向ZE註冊方法的機會,保證當php指令碼調用某個方法時,ZE可以知道需要執行什麼樣的代碼。
然後,PHP等待SAPI層獲得一個請求以便執行它。在CGI或者CLI模式下,PHP可以立即獲得請求而且僅僅發生一次。在Apache、IIS或者其它完整的Web Server的SAPI模式下,由於請求是由用戶端發起,因此這種等待-執行的方式可以重複任意多次,而且多種情況下是並發的執行。然而不論在何種模式下,PHP調用ZE建立起執行請求所需要的運行環境,然後調用每一個擴充的Request Initialization (RINIT)方法。這個方法給擴充提供了建立特定環境變數、分配特定於請求的資源或者執行其它動作的機會。RINIT方法的一個主要的sample就是session擴充的使用。當session.auto_start設定為enable時,RINIT會自動出發使用者空間的session_start()方法並產生$_SESSION變數。
當這種特定於請求的初始化完成後,ZE就接管了程式控制權。翻譯php代碼到機器可識別的符號,再對這些符號進行編碼以便執行。當這些編碼需要調用擴充定義的方法時,ZE將Binder 方法名和參數,並暫時放棄控制權直到方法調用結束。
當指令碼執行完成時,PHP會調用擴充的Request Shutdown (RSHUTDOWN)方法,使擴充有機會能夠清理在此次調用中使用的資源。然後,ZE將執行清理進程(也就是垃圾收集),該進程會非常有效對這次調用使用的每一個變數使用unset()方法。
當上述幾步都完成後,PHP就等待SAPI傳遞給它下一個請求或者一個停止的訊號。在CGI和CLI模式下是不存在“下一個”的概念的,因此SAPI會立即執行shutdown。在shutdown過程中,PHP會調用每一個擴充的Module Shutdown (MSHUTDOWN)方法,然後最終關閉它自己的core子系統。
上述的啟動-執行-關閉的流程似乎很複雜,但當你開始寫一個擴充時就會慢慢的熟悉了,比較容易理解。
Memory Allocation
為了避免記憶體泄露,ZE提供了自己的記憶體管理方式:永久分配(persistent allocation )和非永久分配(non-persistent allocation)。永久分配指的是所分配的記憶體的聲明周期長於單個請求的聲明周期,非永久分配指的是當單個請求結束時,不論free方法是否被調用,記憶體也就隨之被回收。如使用者自訂的變數就屬於非永久分配,因為在請求完成時這些變數也就不在有任何使用的價值。
儘管在理論上,擴充可以依賴ZE在請求被完成時自動釋放掉非永久變數所佔有的記憶體,但在實際操作中並不建議這麼做。所分配的記憶體會長時間的處於未回收狀態,和這些記憶體關聯的已經開啟的資源也可能不能正確的關閉,並且這麼做簡直就像拉完大便(make a mess)後而不進行清理。 我們即將看到,事實上在擴充裡是很容易做到所有分配的資料被合適的清理掉。
比較一下傳統的記憶體配置和PHP/ZE所使用的永久記憶體配置和非永久記憶體配置所使用的方法:
| 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) |
* pemalloc()方法包含一個'persistent'標識,可以允許pemalloc()方法當作非永久記憶體配置方法使用
例如: emalloc(1234) 與 pemalloc(1234,1)是完全相同的。
**safe_emalloc()和(PHP5中)safe_pemalloc()會做額外檢查來防止整數溢出。
建立擴充的開發環境
進行PHP的擴充開發有兩種方式。第一種是從PHP源碼進行,第二種是從PHP提供的獨立的工具進行。第二種比較簡單,但缺點是只能做成擴充的形式。第一種可以把新做的擴充直接編譯到PHP的最後的二進位包中,缺點是少了靈活性,當需要進行調整時就不得不重新編譯整個PHP源碼。因此我們採用第2種方式進行,這種方式也是絕大多數情況下所使用的方式。
Hello World
讓我們從最經典的“Hello World”開始。我們需要完成的是這麼一個擴充:它包含了一個方法,當從php指令碼調用該方法時可以輸出一個字串“Hello World.” 在php指令碼層面上,我們可以使用如下的實現方式: <?php
function hello_world() {
return 'Hello World';
}
?>
現在我們把這個方法的實現做到PHP擴充中去。首先建立一個hello的目錄,隨便哪裡都可以。為了建立擴充,我們需要建立3個檔案:包含hello_world()方法的source檔案,包含PHP調用該擴充所需要資源的header檔案和phpize工具所需要的configureation檔案.
config.m4
PHP_ARG_ENABLE(hello, whether to enable Hello World support,
[ --enable-hello Enable Hello World support])
if test "$PHP_HELLO" = "yes"; then
AC_DEFINE(HAVE_HELLO, 1, [Whether you have Hello World])
PHP_NEW_EXTENSION(hello, hello.c, $ext_shared)
fi
php_hello.h
#ifndef PHP_HELLO_H
#define PHP_HELLO_H 1
#define PHP_HELLO_WORLD_VERSION "1.0"
#define PHP_HELLO_WORLD_EXTNAME "hello"
PHP_FUNCTION(hello_world);
extern zend_module_entry hello_module_entry;
#define phpext_hello_ptr &hello_module_entry
#endif
hello.c
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "php_hello.h"
static function_entry hello_functions[] = ...{
PHP_FE(hello_world, NULL)
...{NULL, NULL, NULL}
};
zend_module_entry hello_module_entry = ...{
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
PHP_HELLO_WORLD_EXTNAME,
hello_functions,
NULL,
NULL,
NULL,
NULL,
NULL,
#if ZEND_MODULE_API_NO >= 20010901
PHP_HELLO_WORLD_VERSION,
#endif
STANDARD_MODULE_PROPERTIES
};
#ifdef COMPILE_DL_HELLO
ZEND_GET_MODULE(hello)
#endif
PHP_FUNCTION(hello_world)
...{
RETURN_STRING("Hello World", 1);
}
如同我們看到的那樣,上述的大部分代碼是膠水-----對PHP說明這個擴充並建立起擴充與PHP之間的通訊。只有最後4行看起來像是調用方法時真正進行處理的邏輯。的確,從這個層次來看這幾行代碼和我們前面提到的PHP代碼看起來很像,直觀上也非常容易理解:
1. 聲明一個名稱為hello_world的方法
2. 這個方法返回一個值為"hello World"的字串
3. 但是, 1 是什麼意思?
前面我們講過,ZE提供了自己的記憶體管理機制,保證當php指令碼執行結束時所有的資源都能夠被合適的釋放掉。對記憶體管理層面而言,對同一塊記憶體進行兩次釋放是禁忌之事。這種稱為double freeing,也通常是導致 segmentation faults的原因。因為它使程式去訪問已經不屬於程式自己的記憶體空間。所以,我們不想允許ZE去釋放一個靜態字串所對應的記憶體空間,因為這個字串位於整個程式空間,並不屬於進程的資料區塊中。【待用資料成員不能被free掉,因為進程要求這些待用資料在進程的聲明周期內都存在】。RETURN_STRING方法可以假設傳給它的字串都需要被複製一份以便以後能夠安全的被釋放掉,但在方法內部給字串分配記憶體,複製然後返回是很常見的操作。RETURN_STRING()允許我們指定當調用該方法時是否需要字串的copy. 上面的4行代碼與下面的代碼是等價的:PHP_FUNCTION(hello_world)
{
char *str;
str = estrdup("Hello World");
RETURN_STRING(str, 0);
}
在這個實現中,我們手工為"Hello World"建立了記憶體空間,並且這個記憶體空間最終傳遞迴調用它的php指令碼,然後把這個記憶體交給了RETURN_STRING(),使用0標識表明RETURN_STRING()不用建立該字串的copy,使用我們已有的就可以了。
建立擴充
有了上述三個檔案,最後一部就是bulid這三個檔案以建立一個可動態載入的擴充了。只需要在hello目錄下執行三個命令就OK了。$ phpize
$ ./configure --enable-hello
$ make
正確執行完畢後,在hello/modules/下即可看到一個hello.so的檔案,它就是一個新的PHP擴充。複製這個so檔案到php的擴充目錄下,並在php.ini中增加一行 extension=hello.so , 以觸發當php啟動運行時對該擴充的自動載入。對於APACHE,IIS之類Web Server,需要重新啟動Web Server,對於CGI或者CLI則不用重啟。
現在,運行$ php -r 'echo hello_world();'
如果一切正常,我們就能在終端上看到輸出的 Hello World了。
使用類似的方式也可以獲得標量的返回結果。如RETURN_LONG()返回整形, RETURN_DOUBLE()返回浮點數,RETURN_BOOL()返回true/false, RETURN_NULL()返回NULL等。實現如下:static function_entry hello_functions[] = {
PHP_FE(hello_world, NULL)
PHP_FE(hello_long, NULL)
PHP_FE(hello_double, NULL)
PHP_FE(hello_bool, NULL)
PHP_FE(hello_null, NULL)
{NULL, NULL, NULL}
};
PHP_FUNCTION(hello_long)
{
RETURN_LONG(42);
}
PHP_FUNCTION(hello_double)
{
RETURN_DOUBLE(3.1415926535);
}
PHP_FUNCTION(hello_bool)
{
RETURN_BOOL(1);
}
PHP_FUNCTION(hello_null)
{
RETURN_NULL();
}
同時,我們需要在標頭檔php_hello.h中添加上述方法的聲明,以便編譯能夠正確完成。
php_hello.h
#ifndef PHP_HELLO_H
#define PHP_HELLO_H 1
#define PHP_HELLO_WORLD_VERSION "1.0"
#define PHP_HELLO_WORLD_EXTNAME "hello"
PHP_FUNCTION(hello_world);
PHP_FUNCTION(hello_long);
PHP_FUNCTION(hello_double);
PHP_FUNCTION(hello_bool);
PHP_FUNCTION(hello_null);
extern zend_module_entry hello_module_entry;
#define phpext_hello_ptr &hello_module_entry
#endif
然後繼續使用 make 即可完成新版本的編譯了,很簡單。