摘要&引言
PHP是當前應用非常廣泛的一門語言,從國外的Facebook、Twitter到國內的淘寶、騰訊、百度再到互連網上林林總總的各種大中小型網站都能見到它的身影。PHP的成功,應該說很大程度上依賴於其開放的擴充API機制和豐富的向外延展群組件(PHP Extension),正是這些向外延展群組件使得PHP從各種資料庫操作到XML、JSON、加密、檔案處理、圖形處理、Socket等領域無所不能。有時候開發人員可能需要開發自己的PHP擴充,當前PHP5的擴充機制是基於Zend API的,Zend API提供了豐富的介面和宏定義,加上一些工具 + 生產力,使得PHP擴充開發起來難度並不算特別大。本文將介紹關於PHP向外延展群組件開發的基本知識,並通過一個執行個體展示開發PHP擴充的基本過程。
PHP向外延展群組件的開發過程在Unix和Windows環境下有所不同,但基本是互連的,本文將基於Unix環境(具體使用Linux)。閱讀本文需要簡單瞭解Unix環境、PHP和C語言的一些基礎知識,只要簡單瞭解就行,我會盡量不涉及太過具體的作業系統和語言特性,並在必要的地方加以解釋,以便讀者閱讀。
本文的具體開發環境為Ubuntu 10.04 + PHP 5.3.3。
下載PHP原始碼
要開發PHP擴充,第一步要下載PHP原始碼,因為裡面有開發擴充需要的工具。我下載的是PHP最新版本5.3.3,格式為tar.bz2壓縮包。下載地址為:http://cn.php.net/get/php-5.3.3.tar.bz2/from/a/mirror。
下載後,將原始碼移動到合適的目錄並解壓。解壓命令為:
複製代碼 代碼如下:tar -jxvf 源碼包名稱
若下載的是tar.gz壓縮包,解壓命令為
複製代碼 代碼如下:tar -zxvf 源碼包名稱
解壓後,在原始碼目錄中有個ext目錄,這裡便是和PHP擴充有關的目錄。進入目錄後用ls查看,可以看到許多已經存在的擴充。下圖是在我的環境下查看的結果:
其中藍色的均是擴充包目錄,其中可以看到我們很熟悉的mysql、iconv和gd等等。而ext_skel是Unix環境下用於自動產生PHP擴充架構的指令碼工具,後面我們馬上會用到,ext_skel_win32.php是windows下對應的指令碼。
開發自己的PHP擴充——say_hello
下面我們開發一個PHP擴充:say_hello。這個擴充很簡單,只是接受一個字串參數,然後輸出“Hello xxx!”。這個例子只是為了介紹PHP向外延展群組件的開發流程,不承擔實際功能。
產生向外延展群組件架構
PHP的向外延展群組件開發目錄和檔案是有固定組織圖的,你可以隨便進入一個已有向外延展群組件目錄,查看其所有檔案,我想你一定眼花繚亂了。當然你可以選擇手工完成架構的搭建,不過我相信你更希望有什麼東西來幫你完成。上文提到的ext_skel指令碼就是用來自動構建擴充包架構的工具。ext_skel的完整命令為:
ext_skel --extname=module [--proto=file] [--stubs=file] [--xml[=file]] [--skel=dir] [--full-xml] [--no-help]
作為初學者,我們不必瞭解所有命令參數,實際上,大多數情況下只需要提供第一個參數就可以了,也就是擴充模組的名字。因此,我們在ext目錄中鍵入如下命令:
./ext_skel --extname=say_hello
(如果你希望詳細瞭解ext_skel的各項命令參數, 請參考這裡)
這時再用ls查看,會發現多了一個“say_hello”目錄,進入這個目錄,會發現ext_skel已經為我們建立好了say_hello的基本架構,如下圖:
如果你懶得弄清楚PHP擴充包目錄結構的全部內容,那麼裡面有三個檔案你必須注意:
config.m4:這是Unix環境下的Build System設定檔,後面將會通過它組建組態和安裝。
php_say_hello.h:這個檔案是擴充模組的標頭檔。遵循C語言一貫的作風,這個裡面可以放置一些自訂的結構體、全域變數等等。
say_hello.c:這個就是擴充模組的主程式檔案了,最終的擴充模組各個函數入口都在這裡。當然,你可以將所有程式碼都塞到這裡面,也可以遵循模組化思想,將各個功能模組放到不同檔案中。
下面的內容主要圍繞這三個檔案展開。
Unix Build System配置
開發PHP向外延展群組件的第一步不是寫實現代碼,而是要先配置好Build System選項。由於我們是在Linux下開發,所以這裡的配置主要與config.m4有關。
關於Build System配置這一塊,要是寫起來能寫一大堆,而且與Unix系統很多東西相關,就算我有興趣寫估計大家也沒興趣看,所以這裡我們從略,只揀關鍵地方說一下,關於config.m4更多細節可以參考這裡。
開啟產生的config.m4檔案,內容大致如下:
複製代碼 代碼如下:
dnl $Id$
dnl config.m4 for extension say_hello
dnl Comments in this file start with the string "dnl".
dnl Remove where necessary. This file will not work
dnl without editing.
dnl If your extension references something external, use with:
dnl PHP_ARG_WITH(say_hello, for say_hello support,
dnl Make sure that the comment is aligned:
dnl [ --with-say_hello Include say_hello support])
dnl Otherwise use enable:
dnl PHP_ARG_ENABLE(say_hello, whether to enable say_hello support,
dnl Make sure that the comment is aligned:
dnl [ --enable-say_hello Enable say_hello support])
if test "$PHP_SAY_HELLO" != "no"; then
dnl Write more examples of tests here...
dnl # --with-say_hello -> check with-path
dnl SEARCH_PATH="/usr/local /usr" # you might want to change this
dnl SEARCH_FOR="/include/say_hello.h" # you most likely want to change this
dnl if test -r $PHP_SAY_HELLO/$SEARCH_FOR; then # path given as parameter
dnl SAY_HELLO_DIR=$PHP_SAY_HELLO
dnl else # search default path list
dnl AC_MSG_CHECKING([for say_hello files in default path])
dnl for i in $SEARCH_PATH ; do
dnl if test -r $i/$SEARCH_FOR; then
dnl SAY_HELLO_DIR=$i
dnl AC_MSG_RESULT(found in $i)
dnl fi
dnl done
dnl fi
dnl
dnl if test -z "$SAY_HELLO_DIR"; then
dnl AC_MSG_RESULT([not found])
dnl AC_MSG_ERROR([Please reinstall the say_hello distribution])
dnl fi
dnl # --with-say_hello -> add include path
dnl PHP_ADD_INCLUDE($SAY_HELLO_DIR/include)
dnl # --with-say_hello -> check for lib and symbol presence
dnl LIBNAME=say_hello # you may want to change this
dnl LIBSYMBOL=say_hello # you most likely want to change this
dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,
dnl [
dnl PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $SAY_HELLO_DIR/lib, SAY_HELLO_SHARED_LIBADD)
dnl AC_DEFINE(HAVE_SAY_HELLOLIB,1,[ ])
dnl ],[
dnl AC_MSG_ERROR([wrong say_hello lib version or lib not found])
dnl ],[
dnl -L$SAY_HELLO_DIR/lib -lm
dnl ])
dnl
dnl PHP_SUBST(SAY_HELLO_SHARED_LIBADD)
PHP_NEW_EXTENSION(say_hello, say_hello.c, $ext_shared)
fi
這個結構體可能看起來會讓人有點頭疼,不過我還是要解釋一下裡面的內容。因為這就是PHP Extension的原型,如果不搞清楚,就沒法開發PHP Extension了。當然,我就不一一對每個欄位進行解釋了,只揀關鍵的、這篇文章會用到的欄位說,因為許多欄位並不需要我們手工填寫,而是可以使用某些預定義的宏填充。
第7個欄位“name”,這個欄位是此PHP Extension的名字,在本例中就是“say_hello”。
第8個欄位“functions”,這個將存放我們在此擴充中定義的函數的引用,具體結構不再分析,有興趣的朋友可以閱讀_zend_function_entry的原始碼。具體編寫代碼時這裡會有相應的宏。
第9-12個欄位分別是四個函數指標,這四個函數會在相應時機被調用,分別是“擴充模組載入時”、“擴充模組卸載時”、“每個請求開始時”和“每個請求結束時”。這四個函數可以看成是一種攔截機制,主要用於相應時機的資源分派、釋放等相關操作。
第13個欄位“info_func”也是一個函數指標,這個指標指向的函數會在執行phpinfo()時被調用,用於顯示自訂模組資訊。
第14個欄位“version”是模組的版本。
(關於zend_module_entry更詳盡的介紹請參考這裡)
介紹完以上欄位,我們可以看看“say_hello.c”中自動產生的“say_hello_module_entry”架構代碼了。
複製代碼 代碼如下:
/* {{{ say_hello_module_entry
*/
zend_module_entry say_hello_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
"say_hello",
say_hello_functions,
PHP_MINIT(say_hello),
PHP_MSHUTDOWN(say_hello),
PHP_RINIT(say_hello), /* Replace with NULL if there"s nothing to do at request start */
PHP_RSHUTDOWN(say_hello), /* Replace with NULL if there"s nothing to do at request end */
PHP_MINFO(say_hello),
#if ZEND_MODULE_API_NO >= 20010901
"0.1", /* Replace with version number for your extension */
#endif
STANDARD_MODULE_PROPERTIES
};
/* }}} */
首先,宏“STANDARD_MODULE_HEADER”會產生前6個欄位,“STANDARD_MODULE_PROPERTIES ”會產生“version”後的欄位,所以現在我們還不用操心。而我們關心的幾個欄位,也都填寫好或由宏產生好了,並且在“say_hello.c”的相應位置也產生了幾個函數的架構。這裡要注意,幾個宏的參數均為“say_hello”,但這並不表示幾個函數的名字全為“say_hello”,C語言中也不可能存在函數名重載機制。實際上,在開發PHP Extension的過程中,幾乎處處都要用到Zend裡預定義的各種宏,從全域變數到函數的定義甚至傳回值,都不能按照“裸寫”的方式來編寫C語言,這是因為PHP的運行機制可能會導致命名衝突等問題,而這些宏會將函數等元素變換成一個內部名稱,但這些對程式員都是透明的(除非你去閱讀那些宏的代碼),我們通過各種宏進行編程,而宏則為我們處理很多內部的東西。
寫到這裡,我們的任務就明了了:第一,如果需要在相應時機處理一些東西,那麼需要填充各個攔截函數內容;第二,編寫say_hello的功能函數,並將引用添加到say_hello_functions中。
編寫phpinfo()回呼函數
因為say_hello擴充在各個生命週期階段並不需要做操作,所以我們只編寫info_func的內容,上文說過,這個函數將在phpinfo()執行時被自動調用,用於顯示擴充的資訊。編寫這個函數會用到四個函數:
php_info_print_table_start()——開始phpinfo表格。無參數。
php_info_print_table_header()——輸出表格頭。第一個參數是整形,指明頭的列數,然後後面的參數是與列數等量的(char*)型別參數用於指定顯示的文字。
php_info_print_table_row()——輸出表格內容。第一個參數是整形,指明這一行的列數,然後後面的參數是與列數等量的(char*)型別參數用於指定顯示的文字。
php_info_print_table_end()——結束phpinfo表格。無參數。