原文:http://devzone.zend.com/public/view/tag/ExtensionPart I: Introduction to PHP and Zend編寫擴展I - PHP和Zend起步http://devzone.zend.com/article/1021-Extension-Writing-Part-I-Introduction-to-PHP-and-ZendPart II: Parameters, Arrays, and ZVALs編寫擴展_II - 參數、數組和ZVALshttp://devzone.zend.com/article/1022-Extension-Writing-Part-II-Parameters-Arrays-and-ZVALsPart II: Parameters, Arrays, and ZVALs [continued]編寫擴展_II - 參數、數組和ZVALs[繼續]http://devzone.zend.com/article/1023-Extension-Writing-Part-II-Parameters-Arrays-and-ZVALs-continuedPart III: Resources編寫擴展_III - 資源http://devzone.zend.com/article/1024-Extension-Writing-Part-III-Resources
編寫擴展 I:PHP和Zend起步擴展 教程 by Sara Golemon | Monday, February 28, 2005
介紹擴展是什麼?生存周期內存分配建立構建環境Hello World構建你的擴展初始設置(INI)全局數值初始設置(INI)作為全局數值核對(代碼)完整性下一步是什麼?
介紹既然您正在閱讀本教程,那麼您或許對編寫PHP語言的擴展感興趣。如果不是...呃,或許你並不知道這一興趣,那麼我們結束的時候你就會發現它。本教程假定您基本熟悉PHP語言及其解釋器實現所用的語言:C.讓我們從指明為什麼你想要編寫PHP擴展開始。限於PHP語言本身的抽象程度,它不能直接訪問某些庫或特定於操作系統的調用。你想要通過某些不平常的方法定制PHP的行為。你有一些現成的PHP代碼,但是你知道它可以(運行)更快、(占用空間)更小,而且消耗更少的內存。你有一些不錯的代碼出售,買家可以使用它,但重要的是不能看到源代碼。這些都是非常正當的理由,但是,在創建擴展之前,你需要首先明白擴展是什麼?擴展是什麼?如果你用過PHP,那麼你肯定用到過擴展。除了少數例外,每個用戶空間的函數都被組織在不同的擴展中。這些函數中的很多夠成了standard擴展-總數超過400。PHP本身帶有86個擴展(原文寫就之時-譯注),平均每個含有大約30個函數。數學操作方面大約有2500個函數。似乎這還不夠, PECL倉庫另外提供了超過100個擴展,而且互聯網上可以找到更多。“除了擴展中的函數,還有什麼?”我聽到了你的疑問。 “擴展的裡面是什麼?PHP的‘核心’是什麼?”PHP的核心由兩部分組成。最底層是Zend引擎(ZE)。ZE把人類易讀的腳本解析成機器可讀的符號,然後在進程空間內執行這些符號。ZE也處理內存管理、變量作用域及調度程序調用。另一部分是PHP內核,它綁定了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由初始化其內核子系統開始。在接近啟動例程的末尾,它加載每個擴展的代碼並調用其模塊初始化例程(MINIT)。這使得每個擴展可以初始化內部變量、分配資源、注冊資源處理器,以及向ZE注冊自己的函數,以便於腳本調用這其中的函數時候ZE知道執行哪些代碼。接下來,PHP等待SAPI層請求要處理的頁面。對於CGI或CLI等SAPI,這將立刻發生且只發生一次。對於Apache、IIS或其他成熟的web服務器SAPI,每次遠程用戶請求頁面時都將發生,因此重復很多次,也可能並發。不管請求如何產生,PHP開始於要求ZE建立腳本的運行環境,然後調用每個擴展的請求初始化 (RINIT)函數。RINIT使得擴展有機會設定特定的環境變量,根據請求分配資源,或者執行其他任務,如審核。 session擴展中有個RINIT作用的典型示例,如果啟用了session.auto_start選項,RINIT將自動觸發用戶空間的session_start()函數以及預組裝$_SESSION變量。一旦請求被初始化了,ZE開始接管控制權,將PHP腳本翻譯成符號,最終形成操作碼並逐步運行之。如任一操作碼需要調用擴展的函數,ZE將會把參數綁定到該函數,並且臨時交出控制權直到函數運行結束。腳本運行結束後,PHP調用每個擴展的請求關閉(RSHUTDOWN)函數以執行最後的清理工作(如將session變量存入磁盤)。接下來,ZE執行清理過程(垃圾收集)-有效地對之前的請求期間用到的每個變量執行unset()。一旦完成,PHP繼續等待SAPI的其他文檔請求或者是關閉信號。對於CGI和CLI等SAPI,沒有“下一個請求”,所以SAPI立刻開始關閉。關閉期間,PHP再次遍歷每個擴展,調用其模塊關閉(MSHUTDOWN)函數,並最終關閉自己的內核子系統。這個過程乍聽起來很讓人氣餒,但是一旦你深入一個運轉的擴展,你會逐漸開始了解它。內存分配為了避免寫的不好的擴展丟失內存,ZE使用附加的標志來執行自己內部的內存管理器以標識持久性。持久分配的內存意味著比單次請求更持久。對比之下,對於在請求期間的非持久分配,不論是否調用釋放(內存)函數,都將在請求尾期被釋放。例如,用戶空間的變量被分配為非持久的,因為請求結束後它們就沒用了。然而,理論上,擴展可以依賴ZE在頁面請求結束時自動釋放非持久內存,但是不推薦這樣做。因為分配的內存將在很長時間保持為未回收狀態,與之相關聯的資源可能得不到適當的關閉,並且吃飯不擦嘴是壞習慣。稍後你會發現,事實上確保所有分配的數據都被正確清理很容易。讓我們簡單地比較傳統的內存分配函數(只應當在外部庫中使用)和PHP/ZE的持久的以及非持久的內存非配函數。傳統的 非持久的 持久的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()族包含一個‘持久’標志以允許它們實現對應的非持久函數的功能。 例如:emalloc(1234)與pemalloc(1234, 0)相同。** safe_emalloc()和(PHP 5中的)safe_pemalloc()執行附加檢測以防整數溢出。
建立構建環境既然你已經了解了一些PHP和Zend引擎的內部運行理論,我打賭你希望繼續深入並開始構建一些東西。在此之前,你需要收集一些必需的構建工具並設定適合於你的目的的環境。首先,你需要PHP本身及其所需要的構建工具集。如果你不熟悉從源碼構建PHP,我建議你看看http://www.php.net/install.unix。(為Windows開發PHP擴展將在以後的文章中提到)。然而,使用PHP的二進制分發包有些冒險,這些版本傾向於忽略./configure的兩個重要選項,它們在開發過程中很便利。第一個--enable-debug。這個選項將把附加的符號信息編譯進PHP的執行文件,以便如果發生段錯誤,你能從中得到一個內核轉儲文件,使用gdb追蹤並發現什麼地方以及為什麼會發生段錯誤。另一個選項依賴於你的PHP版本。在PHP 4.3中該選項名為--enable-experimental-zts,在PHP 5及以後的版本中為--enable-maintainer-zts。這個選項使PHP以為自己執行於多線程環境,並且使你能捕獲通常的程序錯誤,然而它們在非多線程環境中是無害的,卻使你的擴展不可安全用於多線程環境。一旦你已經使用這些額外的選項編譯了PHP並安裝於你的開發服務器(或者工作站)中,你就可以把你的第一個擴展加入其中了。Hello World什麼程序設計的介紹可以完全忽略必需的Hello World程序?此例中,你將制作的擴展導出一個簡單函數,它返回一個含有“Hello World”的字符串。用PHP的話你或許這樣做:<?phpfunction hello_world() {return 'Hello World';}?> 現在你將把它轉入PHP擴展。首先,我們在你的PHP源碼樹的目錄ext/中創建一個名為hello的目錄,並且chdir進入該目錄。事實上,這個目錄可以置於PHP源碼樹之中或之外的任何地方,但是我希望你把它放在這兒,以例示一個在以後的文章中出現的與此無關的概念。你需要在這兒創建3個文件:包含hello_world函數的源碼文件,包含引用的頭文件,PHP用它們加載你的擴展,以及phpize用來准備編譯你的擴展的配置文件。config.m4PHP_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 >= 20010901STANDARD_MODULE_HEADER,#endifPHP_HELLO_WORLD_EXTNAME,hello_functions,NULL,NULL,NULL,NULL,NULL,#if ZEND_MODULE_API_NO >= 20010901PHP_HELLO_WORLD_VERSION,#endifSTANDARD_MODULE_PROPERTIES};#ifdef COMPILE_DL_HELLOZEND_GET_MODULE(hello)#endifPHP_FUNCTION(hello_world){RETURN_STRING("Hello World", 1);} 在上面的示例擴展中,你所看到的代碼大多是黏合劑,作為將擴展引入PHP的協議語言並且在其間建立會話用於通信。只有最後四行才是你所認為“實際做事的代碼”,它們負責與用戶空間的腳本交互這一層次。這些代碼看起來確實非常像之前看到的PHP代碼,而且一看就懂:聲明一個名為hello_world的函數讓該函數返回字符串:“Hello World”....嗯....1? 那個1是怎麼回事兒?回憶一下,ZE包含一個復雜的內存管理層,它可以確保分配的資源在腳本退出時被釋放。然而,在內存管理領域,兩次釋放同一塊內存是絕對禁止的(big no-no)。這種被稱為二次釋放(double freeing)的做法,是引起段錯誤的一個常見因素,原因是它使調用程序試圖訪問不再擁有的內存。類似地,你不應該讓ZE去釋放一個靜態字符串緩沖區(如我們的示例擴展中的“Hello World”),因為它存在於程序空間,而不是被任何進程(process)擁有的數據塊。RETURN_STRING()可以假定傳入其中的任何字符串都需要被復制以便稍後可被安全地釋放;但是由於內部的函數給字符串動態地分配內存、填充並返回並不罕見,第二參數RETURN_STRING()允許我們指定是否需要拷貝字符串的副本。要進一步說明這個概念,下面的代碼片段與上面的對應版本等效:PHP_FUNCTION(hello_world){char *str;str = estrdup("Hello World");RETURN_STRING(str, 0);} 在這個版本中,你手工為最終將被傳回調用腳本的字符串“Hello World”分配內存,然後把這快內存“給予”RETURN_STRING(),用第二參數0指出它不需要制作自己的副本,可以擁有我們的。構建你的擴展本練習的最後一步是將你的擴展構建為可動態加載的模塊。如果你已經正確地拷貝了上面的代碼,只需要在ext/hello/中運行3個命令:$ phpize$ ./configure --enable-hello$ make每個命令都運行後,可在目錄ext/hello/modules/中看到文件hello.so。現在,你可像其他擴展一樣把它拷貝到你的擴展目錄(默認是/usr/local/lib/php/extensions/,檢查你的php.ini以確認),把extension=hello.so加入你的php.ini以使PHP啟動時加載它。 對於CGI/CLI,下次運行PHP就會生效;對於web服務器SAPI,如Apache,需要重新啟動web服務器。我們現在從命令行嘗試下:$ php -r 'echo hello_world();'如果一切正常,你會看到這個腳本輸出的Hello World,因為你的已加載的擴展中的函數hello_world()返回這個字符串,而且echo命令原樣輸出傳給它的內容(本例中是函數的結果)。可以同樣的方式返回其他標量,整數值用RETURN_LONG(),浮點值用 RETURN_DOUBLE(),true/false值用RETURN_BOOL(),RETURN_NULL()?你猜對了,是NULL。我們看下它們各自在實例中的應用,通過在文件hello.c中的function_entry結構中添加對應的幾行PHP_FE(),並且在文件結尾添加一些PHP_FUNCTION()。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中函數hello_world()的原型聲明旁邊加入這些函數的原型聲明,以便構建進程正確進行:PHP_FUNCTION(hello_world);PHP_FUNCTION(hello_long);PHP_FUNCTION(hello_double);PHP_FUNCTION(hello_bool);PHP_FUNCTION(hello_null); 由於你沒有改變文件config.m4,這次跳過phpize和./configure步驟直接跳到make在技術上是安全的。然而,此時我要你再次做完全部構建步驟,以確保構建良好。另外,你應該調用make clean all而不是簡單地在最後一步make,確保所有源文件被重建。重復一遍,迄今為止,根據你所做得改變的類型這些(步驟)不是必需的,但是安全比混淆要好。一旦模塊構建好了,再次把它拷貝到你的擴展目錄,替換舊版本。此時你可以再次調用PHP解釋器, 簡單地傳入腳本測試剛加入的函數。事實上,為什麼不現在就做呢?我會在這兒等待...完成了?好的。如果用了var_dump()而不是echo查看每個函數的輸出,你或許注意到了hello_bool()返回true。那就是RETURN_BOOL()中的值1表現的結果。和在PHP腳本中一樣,整數值0等於FALSE,而其他整數等於TRUE。僅僅是作為約定,擴展作者通常用1,鼓勵你也這麼做,但是不要感覺被它限制了。出於另外的可讀性目的,也可用宏RETURN_TRUE和RETURN_FALSE;再來一個hello_bool(),這次使用RETURN_TRUE:PHP_FUNCTION(hello_bool){RETURN_TRUE;} 注意這兒沒用括號。那樣的話,與其他的宏RETURN_*()相比,RETURN_TRUE和RETURN_FALSE的樣式有區別(are aberrations),所以確信不要被它誤導了(to get caught by this one)。大概你注意到了,上面的每個范例中,我們都沒傳入0或1以表明是否進行拷貝。這是因為,對於類似這些簡單的小型標量,不需要分配或釋放額外的內存(除了變量容器自身-我們將在第二部分作更深入的考查。)還有其他的三種返回類型:資源(就像mysql_connect(),fsockopen()和ftp_connect()返回的值的名字一樣,但是不限於此),數組(也被稱為HASH)和對象(由關鍵字new返回)。當我們深入地講解變量時,會在第二部分看到它們。初始設置(INI)Zend引擎提供了兩種管理INI值的途徑。現在我們來看簡單一些的,然後當你處理全局數據時再探究更完善但也更復雜的方式。假設你要在php.ini中為你的擴展定義一個值,hello.greeting,它保存將在hello_world()函數中用到的問候字符串。你需要向hello.c和php_hello.h中增加一些代碼,同時對hello_module_entry結構作一些關鍵性的改變。先在文件php_hello.h中靠近用戶空間函數的原型聲明處增加如下原型:PHP_MINIT_FUNCTION(hello);PHP_MSHUTDOWN_FUNCTION(hello);PHP_FUNCTION(hello_world);PHP_FUNCTION(hello_long);PHP_FUNCTION(hello_double);PHP_FUNCTION(hello_bool);PHP_FUNCTION(hello_null); 現在進入文件hello.c,去掉當前版本的hello_module_entry,用下面的列表替換它:zend_module_entry hello_module_entry = {#if ZEND_MODULE_API_NO >= 20010901STANDARD_MODULE_HEADER,#endifPHP_HELLO_WORLD_EXTNAME,hello_functions,PHP_MINIT(hello),PHP_MSHUTDOWN(hello),NULL,NULL,NULL,#if ZEND_MODULE_API_NO >= 20010901PHP_HELLO_WORLD_VERSION,#endifSTANDARD_MODULE_PROPERTIES};PHP_INI_BEGIN()PHP_INI_ENTRY("hello.greeting", "Hello World", PHP_INI_ALL, NULL)PHP_INI_END()PHP_MINIT_FUNCTION(hello){REGISTER_INI_ENTRIES();return SUCCESS;}PHP_MSHUTDOWN_FUNCTION(hello){UNREGISTER_INI_ENTRIES();return SUCCESS;} 現在,你只需要在文件hello.c頂部的那些#include旁邊增加一個#include,這樣可以獲得正確的支持INI的頭文件:#ifdef HAVE_CONFIG_H#include "config.h"#endif#include "php.h"#include "php_ini.h"#include "php_hello.h" 最後,你可修改函數hello_world讓它使用INI的值:PHP_FUNCTION(hello_world){RETURN_STRING(INI_STR("hello.greeting"), 1);} 注意,你將要拷貝從INI_STR()返回的值。這是因為,在進入PHP變量堆棧之前(as far as the PHP variable stack is concerned),它都是個靜態字符串。實際上,如果你試圖修改這個返回的字符串,PHP執行環境會變得不穩定,甚至崩潰。本節中的第一處改變引入了兩個你非常熟悉的函數:MINIT和MSHUTDOWN。正如稍早提到的,這些方法在SAPI初始啟動和最終關閉期間被各自調用。它們不會在請求期間和請求之間被調用。本例中它們用來將你的擴展中定義的條目向php.ini注冊。本系列後面的教程中,你也將看到如何使用MINIT和MSHUTDOWN函數注冊資源、對象和流處理器。函數hello_world()中使用INI_STR()取得hello.greeting條目的當前字符串值。也存在其他類似函數用於取得其他類型的值,長整型、雙精度浮點型和布爾型,如下面表格中所示;同時也提供另外的ORIG版本,它們提供在php.ini文件中的INI(的原始)設定(在被 .htaccess或ini_set()指令改變之前)(原文:provides the value of the referenced INI setting as it was set in php.ini-譯注)。當前值 原始值 類型INI_STR(name) INI_ORIG_STR(name) char * (NULL terminated)INI_INT(name) INI_ORIG_INT(name) signed longINI_FLT(name) INI_ORIG_FLT(name) signed doubleINI_BOOL(name) INI_ORIG_BOOL(name) zend_bool傳入PHP_INI_ENTRY()的第一個參數含有在php.ini文件中用到的名字字符串。為了避免命名空間沖突,你應該使用同函數一樣的約定,即是,將你的擴展的名字作為所有值的前綴,就像你對hello.greeting做的一樣。僅僅是作為約定,一個句點被用來分隔擴展的名字和更具說明性的初始設定名字。第二個參數是初始值(默認值?-譯注),而且,不管它是不是數字值,都要使用char*類型的字符串。這主要是依據如下事實:.ini文件中的原值就是文本-連同其他的一切作為一個文本文件存儲。你在後面的腳本中所用到的INI_INT()、INI_FLT()或INI_BOOL()會進行類型轉換。傳入的第三個值是訪問模式修飾符。這是個位掩碼字段,它決定該INI值在何時和何處是可修改的。對於其中的一些,如register_globals,它只是不允許在腳本中用ini_set()改變該值,因為這個設定只在請求啟動期間(在腳本能夠運行之前)有意義。其他的,如allow_url_fopen,是管理(員才可進行的)設定,你不會希望共享主機環境的用戶去修改它,不論是通過ini_set()還是.htaccess的指令。該參數的典型值可能是PHP_INI_ALL,表明該值可在任何地方被修改。然後還有PHP_INI_SYSTEM|PHP_INI_PERDIR,表明該設定可在php.ini文件中修改,或者通過.htaccess文件中的Apache指令修改,但是不能用ini_set()修改。或者,也可用PHP_INI_SYSTEM,表示該值只能在php.ini文件中修改,而不是任何其他地方。我們現在忽略第四個參數,只是提一下,它允許在初始設定發生改變時-例如使用ini_set()-觸發一個方法回調。這使得當設定改變時,擴展可以執行更精確的控制,或是根據新的設定觸發一個相關的行為。全局數值擴展經常需要在一個特定的請求中由始至終跟蹤一個值,而且要把它與可能同時發生的其他請求分開。在非多線程的SAPI中很簡單:只是在源文件中聲明一個全局變量並在需要時訪問它。問題是,由於PHP被設計為可在多線程web服務器(如Apache 2和IIS)中運行,它需要保持各線程使用的全局數值的獨立。通過使用TSRM (Thread Safe Resource Management,線程安全的資源管理器) 抽象層-有時稱為ZTS(Zend Thread Safety,Zend線程安全),PHP將其極大地簡化了。實際上,此時你已經用到了部分TSRM,只是沒有意識到。(不要探尋的太辛苦;隨著本系列的進行,你將到處看到它的身影。)如同任意的全局作用域,創建一個線程安全的作用域的第一步是聲明它。鑒於本例的目標,你將會聲明一個值為0的long型全局數值。每次hello_long()被調用,都將該值增1並返回。在php_hello.h文件中的#define PHP_HELLO_H語句後面加入下面的代碼段:#ifdef ZTS#include "TSRM.h"#endifZEND_BEGIN_MODULE_GLOBALS(hello)long counter;ZEND_END_MODULE_GLOBALS(hello)#ifdef ZTS#define HELLO_G(v) TSRMG(hello_globals_id, zend_hello_globals *, v)#else#define HELLO_G(v) (hello_globals.v)#endif 這次也會使用RINIT方法,所以你需要在頭文件中聲明它的原型:PHP_MINIT_FUNCTION(hello);PHP_MSHUTDOWN_FUNCTION(hello);PHP_RINIT_FUNCTION(hello); 現在我們回到文件hello.c中並緊接著包含代碼塊後面加入下面的代碼:#ifdef HAVE_CONFIG_H#include "config.h"#endif#include "php.h"#include "php_ini.h"#include "php_hello.h"ZEND_DECLARE_MODULE_GLOBALS(hello) 改變hello_module_entry,加入PHP_RINIT(hello):zend_module_entry hello_module_entry = {#if ZEND_MODULE_API_NO >= 20010901STANDARD_MODULE_HEADER,#endifPHP_HELLO_WORLD_EXTNAME,hello_functions,PHP_MINIT(hello),PHP_MSHUTDOWN(hello),PHP_RINIT(hello),NULL,NULL,#if ZEND_MODULE_API_NO >= 20010901PHP_HELLO_WORLD_VERSION,#endifSTANDARD_MODULE_PROPERTIES}; 而且修改你的MINIT函數,附帶著另外兩個函數,它們在請求啟動時執行初始化:static void php_hello_init_globals(zend_hello_globals *hello_globals){}PHP_RINIT_FUNCTION(hello){HELLO_G(counter) = 0;return SUCCESS;}PHP_MINIT_FUNCTION(hello){ZEND_INIT_MODULE_GLOBALS(hello, php_hello_init_globals, NULL);REGISTER_INI_ENTRIES();return SUCCESS;} 最後,你可修改hello_long()函數使用這個值:PHP_FUNCTION(hello_long){HELLO_G(counter)++;RETURN_LONG(HELLO_G(counter));} 在你加入php_hello.h的代碼中,你用到了兩個宏-ZEND_BEGIN_MODULE_GLOBALS()和ZEND_END_MODULE_GLOBALS()-用來創建一個名為zend_hello_globals的結構,它包含一個long型的變量。然後有條件地將HELLO_G()定義為從線程池中取得數值,或者從全局作用域中得到-如果你編譯的目標是非多線程環境。在hello.c中,你用ZEND_DECLARE_MODULE_GLOBALS()宏來例示zend_hello_globals結構,或者是真的全局(如果此次構建是非線程安全的),或者是本線程的資源池的一個成員。作為擴展作者,我們不需要擔心它們的區別,因為Zend引擎為我們打理好這個事情。最後,你在 MINIT中用ZEND_INIT_MODULE_GLOBALS()分配一個線程安全的資源id-現在還不用考慮它是什麼。你可能已經注意到了,php_hello_init_globals()實際上什麼也沒做,卻得多聲明個RINIT將變量counter初始化為0。為什麼?關鍵在於這兩個函數何時被調用。php_hello_init_globals()只在開始一個新的進程或線程時被調用;然而, 每個進程都能處理多個請求,所以用這個函數將變量counter初始化為0將只在第一個頁面請求時運行。向同一進程發出的後續頁面請求將仍會得到以前存儲在這兒的counter變量的值,因此不會從0開始計數。要為每個頁面請求把counter變量初始化為0,我們實現RINIT函數, 正如之前看到的,它在每個頁面請求之前被調用。此時我們包含php_hello_init_globals()函數是因為稍後你將會用到它,而且在ZEND_INIT_MODULE_GLOBALS()中為這個初始化函數傳入NULL將導致在非多線程的平台產生段錯誤。初始設置(INI)作為全局數值回想一下,一個用PHP_INI_ENTRY()聲明的php.ini值會作為字符串被解析,並按需用INI_INT()、INI_FLT()和INI_BOOL()轉為其他格式。對於某些設定,那麼做使得在腳本的執行過程中,當讀取這些值時反復做大量不需要的重復工作。幸運的是,可以讓ZE將INI值存儲為特定的數據類型,並只在它的值被改變時執行類型轉換。我們通過聲明另一個INI值來嘗試下,這次是個布爾值,用來指示變量counter是遞增還是遞減。開始吧,先把php_hello.h中的MODULE_GLOBALS塊改成下面的代碼:ZEND_BEGIN_MODULE_GLOBALS(hello)long counter;zend_bool direction;ZEND_END_MODULE_GLOBALS(hello) 接下來,修改PHP_INI_BEGIN()塊,聲明INI值,像這樣:PHP_INI_BEGIN()PHP_INI_ENTRY("hello.greeting", "Hello World", PHP_INI_ALL, NULL)STD_PHP_INI_ENTRY("hello.direction", "1", PHP_INI_ALL, OnUpdateBool, direction, zend_hello_globals, hello_globals)PHP_INI_END() 現在用下面的代碼初始化init_globals方法中的設定:static void php_hello_init_globals(zend_hello_globals *hello_globals){hello_globals->direction = 1;} 並且最後,在hello_long()中應用這個初始設定來決定是遞增還是遞減:PHP_FUNCTION(hello_long){if (HELLO_G(direction)) {HELLO_G(counter)++;} else {HELLO_G(counter)--;}RETURN_LONG(HELLO_G(counter));} 就是這些。在INI_ENTRY部分指定的OnUpdateBool方法會自動地把php.ini、.htaccess或者腳本中通過ini_set()提供的值轉換為適當的TRUE/FALSE值,這樣你就可以在腳本中直接訪問它們。STD_PHP_INI_ENTRY的最後三個參數告訴PHP去改變哪個全局變量,我們的擴展的全局(作用域)的結構是什麼樣子,以及持有這些變量的全局作用域的名字是什麼。核對(代碼)完整性迄今為止,我們的三個文件應該類似於下面的列表。(為了可讀性,一些項目被移動和重新組織了。)config.m4PHP_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#ifdef ZTS#include "TSRM.h"#endifZEND_BEGIN_MODULE_GLOBALS(hello)long counter;zend_bool direction;ZEND_END_MODULE_GLOBALS(hello)#ifdef ZTS#define HELLO_G(v) TSRMG(hello_globals_id, zend_hello_globals *, v)#else#define HELLO_G(v) (hello_globals.v)#endif#define PHP_HELLO_WORLD_VERSION "1.0"#define PHP_HELLO_WORLD_EXTNAME "hello"PHP_MINIT_FUNCTION(hello);PHP_MSHUTDOWN_FUNCTION(hello);PHP_RINIT_FUNCTION(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 hello.c#ifdef HAVE_CONFIG_H#include "config.h"#endif#include "php.h"#include "php_ini.h"#include "php_hello.h"ZEND_DECLARE_MODULE_GLOBALS(hello)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}};zend_module_entry hello_module_entry = {#if ZEND_MODULE_API_NO >= 20010901STANDARD_MODULE_HEADER,#endifPHP_HELLO_WORLD_EXTNAME,hello_functions,PHP_MINIT(hello),PHP_MSHUTDOWN(hello),PHP_RINIT(hello),NULL,NULL,#if ZEND_MODULE_API_NO >= 20010901PHP_HELLO_WORLD_VERSION,#endifSTANDARD_MODULE_PROPERTIES};#ifdef COMPILE_DL_HELLOZEND_GET_MODULE(hello)#endifPHP_INI_BEGIN()PHP_INI_ENTRY("hello.greeting", "Hello World", PHP_INI_ALL, NULL)STD_PHP_INI_ENTRY("hello.direction", "1", PHP_INI_ALL, OnUpdateBool, direction, zend_hello_globals, hello_globals)PHP_INI_END()static void php_hello_init_globals(zend_hello_globals *hello_globals){hello_globals->direction = 1;}PHP_RINIT_FUNCTION(hello){HELLO_G(counter) = 0;return SUCCESS;}PHP_MINIT_FUNCTION(hello){ZEND_INIT_MODULE_GLOBALS(hello, php_hello_init_globals, NULL);REGISTER_INI_ENTRIES();return SUCCESS;}PHP_MSHUTDOWN_FUNCTION(hello){UNREGISTER_INI_ENTRIES();return SUCCESS;}PHP_FUNCTION(hello_world){RETURN_STRING("Hello World", 1);}PHP_FUNCTION(hello_long){if (HELLO_G(direction)) {HELLO_G(counter)++;} else {HELLO_G(counter)--;}RETURN_LONG(HELLO_G(counter));}PHP_FUNCTION(hello_double){RETURN_DOUBLE(3.1415926535);}PHP_FUNCTION(hello_bool){RETURN_BOOL(1);}PHP_FUNCTION(hello_null){RETURN_NULL();}
下一步是什麼?本教程探究了一個簡單PHP擴展的結構,包括導出函數、返回值、聲明初始設置(INI)以及在(客戶端)請求期間跟蹤其內部狀態。下一部分,我們將探究PHP變量的內部結構,以及在腳本環境中如何存儲、跟蹤和維護它們。在函數被調用時,我們將使用zend_parse_parameters接收來自於程序的參數,以及探究如何返回更加復雜的結果,包括數組、對象和本教程提到的資源等類型。