1 前言 這篇文章是我個人的學習筆記,我把這篇文章送給所有喜歡PHP語言,喜歡PHP擴展開發的同行、同學們。 2 前期准備 閒話不和大家撤了,如果您想學習php的擴展開發,相信您對php基礎知識已經有了一定的了解。PHP擴展時用C語言編寫的,如果您還不知道C語言 裡面指針式什麼東西,建議您先移步他出,好好學習一下C語言,大家都在罵譚浩強的《C語言程序設計》,但是我仍然建議您好好看看這本書。 做PHP開發得先准備一下環境吧。說起來容易,對於一些新手來說恐怕也是一場噩夢。建議您安裝UBUNTU LINUX,網上有很多的linux安裝教程,相信我不用多說了。 我們假設您已經安裝了linux,我用的debain,不過ubuntu的同學不用擔心,因為ubuntu是debain變話而來的,所謂萬變不離其宗,相信您使用ubuntu可以達到同樣的效果。 2.1 開發環境搭建 首先,我們安裝一下php的開發環境:PHP+APACHE2+MYSQL UBUNTU下的安裝很簡單,簡單到您只需要執行下面的命令就可以了: sudo apt-get install apache2 sudo apt-get install php5 sudo apt-get install libapache2-mod-php5 sudo apt-get install mysql-server sudo apt-get install libapache2-mod-auth-mysql sudo apt-get install php5-mysql sudo /etc/init.d/apache2 restart 2.2 源碼部署 進行php擴展開發我們還需要下載一份php的源代碼,我們以PHP5.3為例進行php擴張開發講解,下面是一些下載地址,為了防止下載鏈接失效,我會不斷更新鏈接,當然PHP版本不會變化^_^ http://cn2.php.net/get/php-5.3.19.tar.gz/from/this/mirror 你也可以到www.php.net上面,點擊download下載源碼。 假設您已經或得到了源碼,下面我們將源代碼解壓縮 tar xzvf php-5.3.19 如果這個命令不能正確解壓縮的話請使用tar xvf php-5.3.19試試,再不行的話用rar解壓縮吧,我相信你總有辦法將這個文件解壓縮~ 多大的事 - _ - 我們的php擴展是在linux上面開發的,如果您想在win上面開發,這片文章恐怕不太適合您了。但是技術這種東西都是觸類旁通,希望您在win上面部署好了環境再來看看我的文章。 3 一切都是從HelloWorld開始的 還記得第一次寫代碼,是用C語言打印出來了HelloWorld,那天我興奮激動了一天。希望你你看完本頁內容後自己也寫一個helloworld的php擴展。 第一個擴展程序,改怎麼寫呢? 其實很簡單~~~ 開始吧~~ 上次我們說,我們第一步需要卸載php的源代碼,你下載好了嗎? 建議:建議大家在開發時,只安裝一個php,其實在一個系統中你可以安裝多個php,但是為了避免前期很多不必要的解釋,讀者最好安裝一個。等你對php娴熟後,多個php就不在話下了。 3.1 進入擴展目錄 cd php-5.3.19/ext ext文件夾存放了是php擴展的源代碼,同樣的,我們編寫擴展也需要這裡面進行 大家可以看看ext文件夾下各個文件夾的名字:mysql、xml、zip、sqlite...這些都是php的擴張 3.2 建立擴展開發框架 ./ext_skel --extname=helloworld 執行上面的命令建立擴張開發的框架,這是你會發現ext文件夾下面多了一個叫helloworld的文件夾,同時命令行也輸出了一些文本, 我們先把這些東西放在這裡,不做解釋,稍後大家在看看輸出的命令是什麼意思。 記者這幅圖奧,我們下面會分析 3.3 進入php源碼的根目錄, 編輯文件 vi ext/helloworld/config.m4 去掉這個文件中的幾行注釋 所謂的取消注釋,就是講16~18行前面的dnl去掉而已(不同版本的php源碼行數可能不一樣,請大家注意) 注釋取消後,保存文件並且退出 3.4 在php根目錄執行命令./buildconf --force 這一步以後的文章中進行解釋 3.5 在php源碼的根目錄編譯php程序,注意命令為 ./configure--with-helloworld 3.6 進入我們的擴張目錄helloworld,執行命令 phpize 3.7 在helloworld目錄編譯我們的擴展./configure --with-php-config=/usr/local/bin/php-config(使用你自己環境的php-config)--enable-helloworld 3.8 執行make命令 make 我在執行make的時候,系統報錯,錯誤信息為 /home/work/src/php-5.3.19/ext/helloworld/helloworld.c:43: error:'PHP_FE_END' undeclared here (not in a function) make: *** [helloworld.lo] Error 1 此時,請大家講helloworld.c中的第43行中PHP_FE_END換成{NULL,NULL, NULL} 替換完成後,請大家執行make命令重新編譯 3.9 安裝我們的擴展 有些同學可能知道,php的程序可以使用makeinstall進行安裝,但是為了更好的理解php的工作原理, 我們采用手動安裝php擴展的方式進行安裝,其實php的安裝非常簡單 3.9.1 找到php的擴展安裝目錄 work@baidu:~$ php -r "phpinfo();" | grep extension_dir extension_dir => /usr/lib/php5/20090626+lfs =>/usr/lib/php5/20090626+lfs 我們需要將我們的擴張安裝到/usr/lib/php5/20090626+lfs目錄下面, 在helloworld目錄下面執行命令 sudo cpmodules/helloworld.so /usr/lib/php5/20090626+lfs 目錄/usr/lib/php5/20090626+lfs需要根據自己的實際情況作出改動 3.9.2 在php.ini中打開我們的擴展 同樣,請執行下面的命令 work@baidu:~$ php -r "phpinfo();" | grep"php.ini" Configuration File (php.ini) Path => /etc/php5/cli Loaded Configuration File => /etc/php5/cli/php.ini 可以看出php.ini的路徑為/etc/php5/cli/php.ini sudo vi /etc/php5/cli/php.ini 在文件的最後一行添加代碼 extension=helloworld.so 3.10 驗證我們的擴展時否安裝成功 你可以通過執行下面的命令進行驗證 php -r "phpinfo();" | grep helloworld 如果你安裝成功的話,應該看到下面的信息 root@baidu:/usr/lib/php5/20090626+lfs# php -r "phpinfo();" |grep helloworld helloworld helloworld support => enabled 至此,php擴展安裝完成 也許你會問,我該怎麼打印helloworld呢,別急,下面接著就將helloworld程序了。 3.11 進入擴展helloworld目錄,編輯文件php_helloworld.h,在最後一行添加函數 PHP_FUNCTION(fun_helloworld); 3.12 在helloworld.c中實現我們的函數 將fun_helloworld函數加入到helloworld_functions[]中 3.13 編譯擴展 make 3.14 按照step 9-2 重新安裝我們的擴展 3.15 驗證擴展函數 php -r "echo fun_helloworld();" 如果一切順利的話,你就可以看到我們的問候語句了Hello World ! 3.16 寫在後面的話 也許你按照我的步驟走到最後,你看到了我們的問候語hello world,也許你沒有,但是我已經盡力的把開發的過程描述的盡量詳細,請大家勿要罵我。其實做程序開發的過程就是一個不斷解決問題的過程,如果你在我上面的步驟中遇到了問題,那麼我要恭喜你,因為你得到一個提高自己能力的機會。在解決問題的過程中,百度、谷歌是我們最好的幫手,雖然你也可以通過其他教程或者官方文檔解決問題,但是百度、谷歌對於新手來說卻是最快解決問題的方式。 說了這麼多,無非是想告訴大家:不要害怕在學習的過程中遇到問題,因為這些問題才是我們進步的基石。 4 SAPI簡介 要想寫好php擴展,了解一下SAPI肯定是少不了的。也許你看完本節的內容你還不能很好的掌握SAPI,不過沒有關系,這並不會影響到你後續的php擴展開發,但是我建議讀者還是細細地看完這一節,如果有問題的話可以給我聯系。 SAPI(ServerApplication Programming Interface)指是PHP具體應用編程接口。PHP腳本的執行方式有很多,常見的就是把PHP放在web服務器上面執行、在命令行直接通過php test.php的方式執行,其實php腳本也可以嵌入在其他程序中執行。 目前常見的sapi實現方式有apache模塊,fpm,cgi,cli等,而這些實現可以分為三類:單進程SAPI、多進程SAPI、多線程SAPI。 4.1 單進程SAPI CLI/CGI模式的PHP屬於單進程SAPI實現方式(或者叫做實現模型)。這類的請求在處理一次後就關閉。它的生命周期如下: 請記住:每次請求都會執行上面所有的過程。 給大家解釋一下:假如你使用命令行方式運行php文件或者通過web服務器以cgi方式訪問一個php文件,每一次請求你都會經歷下面的過程: 1. 執行每一個擴展的MINIT方法 2. 執行每個擴展的RINIT方法 3. 執行你要訪問的php文件 4. 執行每個擴展的RSHUTDOWN方法 5. 執行清理工作(針對你要執行的php文件而言) 6. 執行每個擴展的MSHUTDOWN方法 7. php執行結束 看到MINIT, RINIT,RSHUTDWON, MSSHUTDOWN了嗎,是不是有點面熟啊? 4.2 多進程SAPI 如果PHP編譯成為apache的一個模塊處理PHP請求,那麼apache一般會采用多進程的模式,apache啟動後會創建多個子進程,每個子進程在整個生命周期中,可能會處理多次請求。只有在apache關閉,這些子進程才會關閉。這種模式的SAPI生命周期如下: 解釋:上圖頂部紅色部分在apache fork出來這個子進程後就會自行,而中間藍顏色表示部分每個請求時執行一次,一次請求只能在被一個子進程處理,上圖底部紅色部分是子進程結束的時候執行的。對於每個進程而言,MINIT函數只會執行一次,但是每次請求都會執行RINIT RSHUTDOWN函數,進程結束時執行MSHUTDOWN函數。 4.3 多線程SAPI 多線程模式和多進程中的某個進程類似,不同的是整個進程的聲明周期內會並行重復請求開始->請求結束,它的生命周期如下圖: 我們依次講解一下apache的對請求的處理流程、php文件的處理流程、php擴展執行的流程。如果您想深入了解擴展,並且向在擴展開發方面有所成就的話,那麼我建議您詳細的看一下這一節,也許你看不懂,但是沒有關系,以後我們還會詳細講解。 5 php擴展調用php.ini配置 本節中我們讀取一下php.ini文件中的配置。其實,讀取php.ini配置的方式挺多,本次我們只講一種,如果有興趣的話,大家可以各自研究一下。 廢話少說,我們更改一下之前的講解方式,本次我們直接上代碼。(其實是因為php擴展讀取配置文件太簡單了) (假設我們建立了一個ini_read的擴展,代碼的改動部分我們用黃顏色標出 5.1 更改配置 在php.ini中增加配置ini_read.helloworld=hellohellohello 5.2 頭文件改動 php_ini_read.h的改動如下:(改動的地方我用黃顏色) #ifndef PHP_INI_READ_H #define PHP_INI_READ_H externzend_module_entry ini_read_module_entry; #define phpext_ini_read_ptr &ini_read_module_entry #ifdef PHP_WIN32 # define PHP_INI_READ_API __declspec(dllexport) #elif defined(__GNUC__) &&__GNUC__ >= 4 # define PHP_INI_READ_API __attribute__((visibility("default"))) #else # define PHP_INI_READ_API #endif #ifdef ZTS #include "TSRM.h" #endif PHP_MINIT_FUNCTION(ini_read); PHP_MSHUTDOWN_FUNCTION(ini_read); PHP_RINIT_FUNCTION(ini_read); PHP_RSHUTDOWN_FUNCTION(ini_read); PHP_MINFO_FUNCTION(ini_read); /* Declare any global variables you may need between the BEGIN and END macros here: ZEND_BEGIN_MODULE_GLOBALS(ini_read) long global_value; char *global_string; ZEND_END_MODULE_GLOBALS(ini_read) */ /* In every utility function you addthat needs to use variables in php_ini_read_globals, call TSRMLS_FETCH(); after declaring other variables used by that function, or better yet, pass in TSRMLS_CC after the last function argument and declare your utility function with TSRMLS_DC after the last declared argument. Always refer to the globals in your function as INI_READ_G(variable). You are encouraged to rename these macros something shorter, see examples in any other php module directory. */ #ifdef ZTS #define INI_READ_G(v)TSRMG(ini_read_globals_id, zend_ini_read_globals *, v) #else #define INI_READ_G(v)(ini_read_globals.v) #endif #endif /* PHP_INI_READ_H */ /* * Local variables: * tab-width: 4 * c-basic-offset: 4 * End: * vim600: noet sw=4 ts=4 fdm=marker * vim<600: noet sw=4 ts=4 */ PHP_FUNCTION(helloworld); 5.3 源文件改動 ini_read.c的改動如下 #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "php.h" #include "php_ini.h" #include"ext/standard/info.h" #include "php_ini_read.h" /* If you declare any globals inphp_ini_read.h uncomment this: ZEND_DECLARE_MODULE_GLOBALS(ini_read) */ /* True global resources - no need forthread safety here */ static int le_ini_read; /* {{{ ini_read_functions[] * * Every user visible function must have anentry in ini_read_functions[]. */ constzend_function_entry ini_read_functions[]= { PHP_FE(helloworld, NULL) /* Fortesting, remove later. */ {NULL, NULL, NULL} }; /* }}} */ /* {{{ ini_read_module_entry */ zend_module_entry ini_read_module_entry={ #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif "ini_read", ini_read_functions, PHP_MINIT(ini_read), PHP_MSHUTDOWN(ini_read), NULL, NULL, NULL, #if ZEND_MODULE_API_NO >= 20010901 "0.1",/* Replace with version number for yourextension */ #endif STANDARD_MODULE_PROPERTIES }; /* }}} */ #ifdef COMPILE_DL_INI_READ ZEND_GET_MODULE(ini_read) #endif /* {{{ PHP_INI */ PHP_INI_BEGIN() PHP_INI_ENTRY("ini_read.helloworld", "foobar",PHP_INI_ALL, NULL) PHP_INI_END() /* }}} */ /* {{{ php_ini_read_init_globals */ /* Uncomment this function if you haveINI entries static voidphp_ini_read_init_globals(zend_ini_read_globals *ini_read_globals) { ini_read_globals->global_value = 0; ini_read_globals->global_string = NULL; } */ /* }}} */ /* {{{ PHP_MINIT_FUNCTION */ PHP_MINIT_FUNCTION(ini_read) { REGISTER_INI_ENTRIES(); return SUCCESS; } /* }}} */ /* {{{ PHP_MSHUTDOWN_FUNCTION */ PHP_MSHUTDOWN_FUNCTION(ini_read) { UNREGISTER_INI_ENTRIES(); return SUCCESS; } /* }}} */ /* Remove if there's nothing to do atrequest start */ /* {{{ PHP_RINIT_FUNCTION */ PHP_RINIT_FUNCTION(ini_read) { return SUCCESS; } /* }}} */ /* Remove if there's nothing to do atrequest end */ /* {{{ PHP_RSHUTDOWN_FUNCTION */ PHP_RSHUTDOWN_FUNCTION(ini_read) { return SUCCESS; } /* }}} */ /* {{{ PHP_MINFO_FUNCTION */ PHP_MINFO_FUNCTION(ini_read) { php_info_print_table_start(); php_info_print_table_header(2,"ini_read support","enabled"); php_info_print_table_end(); /* Remove comments if you have entries in php.ini DISPLAY_INI_ENTRIES(); */ } /* }}} */ /* }}} */ /* The previous line is meant for vimand emacs, so it can correctly fold and unfold functions in source code. See the corresponding marks just before function definition, where the functions purpose is also documented.Please follow this convention for the convenience of others editing your code. */ /* * Local variables: * tab-width: 4 * c-basic-offset: 4 * End: * vim600: noet sw=4 ts=4 fdm=marker * vim<600: noet sw=4 ts=4 */ PHP_FUNCTION(helloworld) { RETURN_STRING(INI_STR("ini_read.helloworld"),1); } 5.4 結果驗證 上面步驟執行完了後,根據之前安裝擴張的方式安裝一下我們的擴展吧。 如果你安裝完了擴展,那麼運行下面的命令您就可以看到輸出了 php -r "echohelloworld();" 此時的輸出應該為hellohellohello 6 全局變量 6.1 拋磚引玉 將這一節的目的是想交給大家,如和聲明一個變量。使得這個變量針對每次請求獨立,也就是說,同一次請求我們訪問的變量是同一個,不同的請求我們使用的變量不是同一個。 說道這裡我先拋出一個問題:既然要實現上面的要求,那麼我們該怎麼辦呢?我應該在哪裡聲明我的全局變量呢? 還記得SAPI簡介那一張嗎?SAPI的實現有三種方式,單進程,多進程,多線程,但是對於每一次而言,都必須執行的幾個過程為RINIT RSHUTDOWN….說道這裡你意識到了嗎。我們是不是在RINIT過程的時候初始化我們的全局常量,這樣每次請求的時候這個變量的值都會變成默認值。還沒有看明白?看看下面的流程你也許就懂了。 1. 首先在.h文件中聲明全局變量 2. 在RINIT過程時初始化這個變量 3. 調用變量 6.2 實現方式 我們還是直接上源代碼:我把一些無用的注釋去掉了,希望大家別介意 頭文件php-iamnew.h #ifndef PHP_IAMNEW_H #define PHP_IAMNEW_H extern zend_module_entry iamnew_module_entry; #define phpext_iamnew_ptr &iamnew_module_entry #ifdef PHP_WIN32 # definePHP_IAMNEW_API __declspec(dllexport) #elif defined(__GNUC__) && __GNUC__ >= 4 # definePHP_IAMNEW_API __attribute__ ((visibility("default"))) #else # definePHP_IAMNEW_API #endif #ifdef ZTS #include "TSRM.h" #endif PHP_MINIT_FUNCTION(iamnew); PHP_MSHUTDOWN_FUNCTION(iamnew); PHP_RINIT_FUNCTION(iamnew); PHP_RSHUTDOWN_FUNCTION(iamnew); PHP_MINFO_FUNCTION(iamnew); PHP_FUNCTION(confirm_iamnew_compiled); /* For testing, remove later. */ ZEND_BEGIN_MODULE_GLOBALS(iamnew) long counter; ZEND_END_MODULE_GLOBALS(iamnew) #ifdef ZTS #define IAMNEW_G(v) TSRMG(iamnew_globals_id,zend_iamnew_globals *, v) #else #define IAMNEW_G(v) (iamnew_globals.v) #endif #endif PHP_FUNCTION(test_global_value); 源文件iamnew.c #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "php.h" #include "php_ini.h" #include "ext/standard/info.h" #include "php_iamnew.h" ZEND_DECLARE_MODULE_GLOBALS(iamnew) //聲明全局變量 static int le_iamnew; const zend_function_entry iamnew_functions[]= { PHP_FE(test_global_value,NULL) {NULL,NULL,NULL} //此處修改以後不再解釋 }; zend_module_entry iamnew_module_entry ={ #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif "iamnew", iamnew_functions, PHP_MINIT(iamnew), PHP_MSHUTDOWN(iamnew), PHP_RINIT(iamnew), PHP_RSHUTDOWN(iamnew), PHP_MINFO(iamnew), #if ZEND_MODULE_API_NO >= 20010901 "0.1", #endif STANDARD_MODULE_PROPERTIES }; #ifdef COMPILE_DL_IAMNEW ZEND_GET_MODULE(iamnew) #endif // 這個函數之前是被注釋的,去掉注釋,並且函數內容為空即可 static void php_iamnew_init_globals(zend_iamnew_globals*iamnew_globals) { } PHP_MINIT_FUNCTION(iamnew) { ZEND_INIT_MODULE_GLOBALS(iamnew, php_iamnew_init_globals,NULL); return SUCCESS; } PHP_MSHUTDOWN_FUNCTION(iamnew) { return SUCCESS; } PHP_RINIT_FUNCTION(iamnew) { IAMNEW_G(counter)= 0; //初始化 return SUCCESS; } PHP_RSHUTDOWN_FUNCTION(iamnew) { return SUCCESS; } PHP_MINFO_FUNCTION(iamnew) { php_info_print_table_start(); php_info_print_table_header(2,"iamnew support","enabled"); php_info_print_table_end(); } // 增加測試函數 PHP_FUNCTION(test_global_value) { IAMNEW_G(counter)++; RETURN_LONG(IAMNEW_G(counter)); } 6.3 結果驗證 修改完成後,編譯安裝我們的擴展,執行下面的命令進行測試 php -r "echotest_global_value(); test_global_value();" 以後我們可能不在對這些簡單的結果進行驗證了,但是為了學習效果,建議大家自己驗證一下。 6.4 實現原理 6.4.1 知識點1 我們先看一下頭文件中聲明全局變量的兩個宏 ZEND_BEGIN_MODULE_GLOBALS ZEND_END_MODULE_GLOBALS 我們看一下這兩個宏的展開內容: #define ZEND_BEGIN_MODULE_GLOBALS(module_name) \ typedef struct _zend_##module_name##_globals { #define ZEND_END_MODULE_GLOBALS(module_name) \ }zend_##module_name##_globals; 從展開信息中我們可以看到,這兩個宏僅僅是定了一個叫做zend_##module##_globals的結構體,而ZEND_BEGIN_MODULE_GLOBALS和ZEND_END_MODULE_GLOBALS就是結構體zend_##module##_globals的成員變量。 6.4.2 知識點2 我們再來看一下ZEND_DECLARE_MODULE_GLOBALS這句話是 #define ZEND_DECLARE_MODULE_GLOBALS(module_name) \ ts_rsrc_idmodule_name##_globals_id; 其實ts_rsrc_id就是int類型,查看ts_rsrc_id的定義就可以知道:typedef int ts_rsrc_id; 6.4.3 知識點3 在一個宏聲明ZEND_INIT_MODULE_GLOBALS,查看一下這個宏的定義 #define ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor,globals_dtor) \ ts_allocate_id(&module_name##_globals_id,sizeof(zend_##module_name##_globals), (ts_allocate_ctor) globals_ctor,(ts_allocate_dtor) globals_dtor); 從定義來看,ZEND_INIT_MODULE_GLOBALS給變量module_name##_globals_id分配了一個id,這個id是一個線程安全的資源id。而module_name##_globals_id不就是ZEND_DECLARE_MODULE_GLOBALS分配的變量嗎! globals_ctor是一個回調函數指針,這裡不再多說。 6.4.4 知識點 我們再來看一下IAMNEW_G這個宏的定義 #ifdef ZTS //是否線程安全 #defineIAMNEW_G(v) TSRMG(iamnew_globals_id,zend_iamnew_globals *, v) #else #defineIAMNEW_G(v) (iamnew_globals.v) #endif ---------------------------------------------------- TSRMG的定義為: #define TSRMG(id, type, element) (((type) (*((void ***)tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(id)])->element) tsrm_ls的定義為: void ***tsrm_ls; 從宏整體的定義可以看出IAMNEW_G是取得變量的值。 其實IAMNEW_G就是取的全局變量的值。 如果你對TSRMG非常感興趣的話,也可以看看這個宏具體的實現方式,不多對於新手來說的確有點困難。 7 使用php.ini裡面的配置初始化全局變量 前面我們講到了如何讀取php.ini裡面的配置,也講到了如何初始化全局變量,這一節的任務也很明了,就是如何使用php.ini裡面的配置初始化全局變量。這一節我們作為課堂任務來進行處理,不在單獨解釋。 8 參數接收 前面我們講到了php擴展開發的大體架構,本節我們介紹擴展如何接收php腳本中傳入的參數。 8.1 普通參數接收 任務:寫一個擴展,輸出php腳本傳入的參數。例如php –r “echo hello(‘param test.’);” 將要輸出param test. 首先我們建立一個paramtest的擴展,建議、編譯、安裝、測試過程不再贅述。 首先,我們需要建立一個函數叫做hello,建立的過程和之前是一樣的,也不多說。 我們具體看一下,hello函數的實現,paramtest.c中hello函數的實現如下: PHP_FUNCTION(hello) { char* str_hello; int int_hello_str_length; if(zend_parse_parameters(ZEND_NUM_ARGS()TSRMLS_CC,"s", &str_hello,&int_hello_str_length)==FAILURE) { RETURN_NULL(); } php_printf("%s",str_hello); RETURN_TRUE; } 我們先看一下zend_parse_parameters的函數定義:ZEND_NUM_ARGS() TSRMLS_CC其實是zend_parse_parameters的兩個參數,具體可以查找這兩個宏的定義,這兩個宏主要是傳入參數信息和保證線程安全。參數”s”其實是格式化字符串,也就是說這個參數告訴ZEND編譯器,我可以接收的參數類型是什麼樣子的。Zend_parse_parameters的其他參數負責具體接收php腳本中函數變量的值。 請注意:我們在php腳本的函數中傳入了一個參數,但是在zend_parse_parameters中卻需要兩個參數進行接收,給大家解釋一下原因:我們在php腳本中傳入的參數是字符串,對於C語言來說,PHP腳本傳入字符串的長度是無法直接用函數strlen來進行獲取的。原因是因為,我們在php腳本中可以傳入\0,但是\0在C語言中是字符串結尾的意思。 在zend_parse_parameters中,格式化字符創(本例中是”s”)有很多,如果你想要接受多個參數的話,只要在格式化參數中加入相應的類型標示符,在參數中添加接受變量就可以了。例如,如果你的php函數需要傳入兩個變量,第一個變量為問候字符串,第二個參數為bool,表示是否輸出,那麼zend_parse_parameters函數可以這樣寫:zend_parse_parameters(ZEND_NUM_ARGS()TSRMLS_CC,"sb", &str_hello, &len,&is_output)。 其中,格式化字符串可以列表如下: PHP變量類型 代碼 C擴展變量類型 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* 上表中有很多zval類型,這個類型我們下一節進行單獨介紹。 另外一個需要注意的地方是,我們使用php_printf在C函數中進行輸出,你能想到原因嗎?前面我們已經講到php既可以作為腳本在命令行運行,也可以通過web服務器,以單進程、多進程、多線程方式運行,如果我們在web服務器中將我們的信息輸出到stdout中,會導致信息無法輸出或者輸出錯誤。給大家舉個例子,我們可以再apache+php環境中寫一個腳本,通過浏覽器訪問這個腳本,如果你使用printf輸出字符串,那麼浏覽器將無法看到你的輸出信息。但是如果你使用php_printf輸出的話,浏覽器就能顯示你輸出的信息。 8.2 可選參數接收 我們已經學會了如何接收普通參數,大家都知道php中還有可選參數,那麼我們改怎樣接收可選參數呢?(如果你不知道php可以使用可選參數的話,建議你先溫習一下php的基礎知識。) 這個很簡單,我們直接上代碼: PHP_FUNCTION(hello) { char*str_hello; int int_hello_str_length; zend_bool is_output =0; if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"s|b",&str_hello, &int_hello_str_length,&is_output)== FAILURE) { RETURN_NULL(); } if(is_output) { php_printf("%s", str_hello); } RETURN_TRUE; } 很簡單吧,如果你有可選參數,那麼在格式化字符串中加入|就可以了,|後面的就是可選參數,其他的和普通變量的接收是一樣的。 9 zval結構分析 9.1 初識zval 我們先來看一下zval的定義: typedef union_zvalue_value { long lval; /* long value */ double dval; /* double value */ struct { char *val; int len; } str; HashTable *ht; /* hash table value */ zend_object_value obj; } zvalue_value; struct _zval_struct { /* Variable information*/ zvalue_value value; /* value */ zend_uint refcount__gc; zend_uchar type; /*active type */ zend_uchar is_ref__gc; }; typedef struct_zval_struct zval; 9.2 zval的創建和使用 我們先來看一段代碼: PHP_FUNCTION(hello) { zval* t; ALLOC_INIT_ZVAL(t); Z_TYPE_P(t)= IS_LONG; Z_LVAL_P(t)= 1234567890; zval_ptr_dtor(&t); } ALLOC_INIT_ZVAL宏用來給t分配內存,並且將t初始化為一個空變量,Z_TYPE_P用來給zval變量指定變量的類型,Z_LVAL_P用來給變量賦值,zval_ptr_dtor用來清理變量空間。 我們可以使用上面的代碼使用變量外,我們還可以使用宏ZVAL_LONG來快速的定義變量和給變量賦值。也就是說上面的代碼我們可以使用下面的代碼來代替。 PHP_FUNCTION(hello) { zval* t; ZVAL_LONG(t, 1234567890); } 我們可以使用下面的宏來快速的定義和使用zval變量: ZVAL_RESOURCE、 ZVAL_BOOL、ZVAL_NULL、ZVAL_LONG、ZVAL_DOUBLE ZVAL_STRING、ZVAL_STRINGL、ZVAL_EMPTY_STRING、ZVAL_ZVAL 上面的宏很簡單,如果大家有什麼不明白的地方,大家可以去看看源代碼。 10 函數返回值 終於講完zval了,前面我們講到了函數的定義和使用,但是我們沒有講函數的返回值。因為C擴展中,函數的返回值類型為zval的,所以我們把這一節放在了這裡進行講解。 本節任務,我們寫一個簡單的計算器,完成加減乘除運算,要求:編寫函數calculate(num1, num2, opt),我們希望完成num1 opt num2= ?的運算。我們看一下完成上面任務的代碼: PHP_FUNCTION(calculate) { int num1; int num2; char* opt; int opt_len; if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"lls", &num1,&num2,&opt,&opt_len)==FAILURE) { php_printf("param error. example: calculate(123, 456, '+')\n"); RETURN_NULL(); } if(1!= opt_len) { php_printf("param error. example: calculate(123, 456, '+')\n"); RETURN_NULL(); } switch (opt[0]) { case '+': return_value->type= IS_LONG; return_value->value.lval= num1 + num2; break; case '-': return_value->type= IS_LONG; return_value->value.lval= num1 - num2; break; case '*': return_value->type= IS_LONG; return_value->value.lval= num1 * num2; break; case '/': return_value->type= IS_DOUBLE; return_value->value.lval= num1 *1.0 / num2; break; default: return_value->type= IS_LONG; return_value->value.lval= 0; break; } } 看到上面代碼,不知道大家有沒有疑惑,return_value是怎麼來的? return_value是你宏PHP_FUNCTION宏中定義的,PHP_FUNCTION會聲明這個變量,並且將這個變量賦值為NULL,我們來看一下宏定義: #define ZEND_NAMED_FUNCTION(name) void name(INTERNAL_FUNCTION_PARAMETERS) #define ZEND_FUNCTION(name) ZEND_NAMED_FUNCTION(ZEND_FN(name)) #define INTERNAL_FUNCTION_PARAMETERSint ht, zval *return_value, \ zval **return_value_ptr, zval *this_ptr, intreturn_value_used TSRMLS_DC 從上面的宏定義來看,return_value就是zval的一個指針,php用這個變量來指向函數的返回值。其實我們也有一個宏可以進行簡單的返回,宏定義如下,RETVAL_*(v),*表示的就是各種變量類型,v表示變量的值,例如RETVAL_LONG(34),將返回一個long類型的數值,其值為34。 00:14啦,說點題外話,小時候非常喜歡過年,但是長大後發現自己害怕了。不知道你有沒有同感? 不說啦,大家有問題給我發郵件[email protected],或者聯系我qq947847775. 現在太晚啦,大家晚安。 11 數組使用以及HashTable介紹 本節我們講一下php的數組,在php中,數組使用HashTable實現的。本節中我們先詳細的介紹一下HashTable,然後再講講如何使用HastTable 11.1 變長結構體 所謂的變長結構體,其實是我們C語言結構體的一種特殊用法,並沒有什麼新奇之處。我們先來看一下變長結構體的一種通用定義方法。 typedef struct bucket { int n; char key[30]; char value[1]; }Bucket; 我們定義了一個結構體Bucket,我們希望用這個結構體存放學生的個人簡介。其中key用來存在學生的姓名,value用來存放學生的簡介。大家可能很好奇,我們的value聲明了長度為1. 1個char能存多少信息呀? 其實,對於變長結構體,我們在使用的使用不能直接定義變量,例如:Bucket bucket; 您要是這樣使用,value肯定存儲不了多少信息。對於變長結構體,我們在使用的時候需要先聲明一個變長結構體的指針,然後通過malloc函數分配函數空間,我們需要用到的空間長度是多少,我們就可以malloc多少。通用的使用方法如下: Bucket*pBucket; pBucket =malloc(sizeof(Bucket)+ n *sizeof(char)); 其中n就是你要使用value的長度。如果這樣使用的話,value指向的字符串不久變長了嗎! 11.2 Hashtable簡介 我們先看一下HashTable的定義 struct _hashtable; typedef struct bucket { ulong h;//當元素是數字索引時使用 uint nKeyLength;//當使用字符串索引時,這個變量表示索引的長度,索引(字符串)保存在最後一個元素aKey void *pData;//用來指向保存的數據,如果保存的數據是指針的話,pDataPtr就指向這個數據,pData指向pDataPtr void *pDataPtr; struct bucket *pListNext;//上一個元素 struct bucket *pListLast;//下一個元素 struct bucket *pNext;//指向下一個bucket的指針 struct bucket *pLast;//指向上一個bucket的指針 char arKey[1];//必須放在最後,主要是為了實現變長結構體 }Bucket; typedef struct _hashtable { uint nTableSize; //哈希表的大小 uint nTableMask; //數值上等於nTableSize - 1 uint nNumOfElements; //記錄了當前HashTable中保存的記錄數 ulongnNextFreeElement; //指向下一個空閒的Bucket Bucket *pInternalPointer; //這個變量用於數組反轉 Bucket *pListHead; //指向Bucket的頭 Bucket *pListTail; //指向Bucket的尾 Bucket **arBuckets; dtor_func_tpDestructor; //函數指針,數組增刪改查時自動調用,用於某些清理操作 zend_bool persistent; //是否持久 unsigned char nApplyCount; zend_boolbApplyProtection; //和nApplyCount一起起作用,防止數組遍歷時無限遞歸 #if ZEND_DEBUG int inconsistent; #endif }HashTable; 希望大家能好好看看上面的定義,有些東西我將出來反而會說不明白,不如大家看看代碼淺顯明了。PHP的數組,其實是一個帶有頭結點的雙向鏈表,其中HashTable是頭,Bucket存儲具體的結點信息。 11.3 HashTable內部函數分析 11.3.1 宏HASH_PROTECT_RECURSION #defineHASH_PROTECT_RECURSION(ht) \ if ((ht)->bApplyProtection) { \ if ((ht)->nApplyCount++ >= 3){ \ zend_error(E_ERROR, "Nestinglevel too deep - recursive dependency?"); \ } \ } 這個宏主要用來防止循環引用。 11.3.2 宏ZEND_HASH_IF_FULL_DO_RESIZE #defineZEND_HASH_IF_FULL_DO_RESIZE(ht) \ if ((ht)->nNumOfElements >(ht)->nTableSize) { \ zend_hash_do_resize(ht); \ } 這個宏的作用是檢查目前HashTable中的元素個數是否大於了總的HashTable的大小,如果個數大於了HashTable的大小,那麼我們就重新分配空間。我們看一下zend_hash_do_resize static intzend_hash_do_resize(HashTable*ht) { Bucket **t; IS_CONSISTENT(ht); if ((ht->nTableSize<< 1) >0) { /* Let's double the table size */ t = (Bucket**) perealloc_recoverable(ht->arBuckets, (ht->nTableSize<< 1) * sizeof(Bucket*), ht->persistent); if (t){ HANDLE_BLOCK_INTERRUPTIONS(); ht->arBuckets = t; ht->nTableSize = (ht->nTableSize<< 1); ht->nTableMask = ht->nTableSize- 1; zend_hash_rehash(ht); HANDLE_UNBLOCK_INTERRUPTIONS(); return SUCCESS; } return FAILURE; } return SUCCESS; } 從上面的代碼中我們可以看出,HashTable在分配空間的時候,新分配的空間等於原有空間的2倍。 11.3.3 函數 _zend_hash_init 這個函數是用來初始化HashTable的,我們先看一下代碼: ZEND_API int_zend_hash_init(HashTable*ht, uint nSize,hash_func_t pHashFunction,dtor_func_t pDestructor,zend_bool persistent ZEND_FILE_LINE_DC) { uint i = 3; //HashTable的大小默認無2的3次方 Bucket **tmp; SET_INCONSISTENT(HT_OK); if (nSize>= 0x80000000){ ht->nTableSize = 0x80000000; } else{ while ((1U << i)< nSize){ i++; } ht->nTableSize = 1 << i; } ht->nTableMask = ht->nTableSize-1; ht->pDestructor = pDestructor; ht->arBuckets =NULL; ht->pListHead =NULL; ht->pListTail =NULL; ht->nNumOfElements = 0; ht->nNextFreeElement = 0; ht->pInternalPointer = NULL; ht->persistent = persistent; ht->nApplyCount =0; ht->bApplyProtection = 1; /* Uses ecalloc() so that Bucket* == NULL */ if (persistent){ tmp = (Bucket **)calloc(ht->nTableSize,sizeof(Bucket*)); if (!tmp){ return FAILURE; } ht->arBuckets = tmp; } else{ tmp = (Bucket **)ecalloc_rel(ht->nTableSize,sizeof(Bucket*)); if (tmp){ ht->arBuckets = tmp; } } return SUCCESS; } 可以看出,HashTable的大小被初始化為2的n次方,另外我們看到有兩種內存方式,一種是calloc,一種是ecalloc_rel,這兩中內存分配方式我們細講了,有興趣的話大家可以自己查一查。 11.3.4 函數_zend_hash_add_or_update 這個函數向HashTable中添加或者修改元素信息 ZEND_API int_zend_hash_add_or_update(HashTable*ht,const char *arKey, uintnKeyLength,void *pData, uintnDataSize,void **pDest,int flag ZEND_FILE_LINE_DC) { ulong h; uint nIndex; Bucket *p; IS_CONSISTENT(ht); if (nKeyLength<= 0){ #if ZEND_DEBUG ZEND_PUTS("zend_hash_update: Can't put inempty key\n"); #endif return FAILURE; } h = zend_inline_hash_func(arKey, nKeyLength); nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; while (p!= NULL){ if ((p->h== h)&& (p->nKeyLength==nKeyLength)){ if (!memcmp(p->arKey, arKey,nKeyLength)){ if (flag &HASH_ADD){ return FAILURE; } HANDLE_BLOCK_INTERRUPTIONS(); #if ZEND_DEBUG if (p->pData == pData){ ZEND_PUTS("Fatalerror in zend_hash_update: p->pData == pData\n"); HANDLE_UNBLOCK_INTERRUPTIONS(); return FAILURE; } #endif if (ht->pDestructor){ ht->pDestructor(p->pData); } UPDATE_DATA(ht, p, pData,nDataSize); if (pDest) { *pDest = p->pData; } HANDLE_UNBLOCK_INTERRUPTIONS(); return SUCCESS; } } p = p->pNext; } p = (Bucket*)pemalloc(sizeof(Bucket)- 1 +nKeyLength, ht->persistent); if (!p){ return FAILURE; } memcpy(p->arKey, arKey, nKeyLength); p->nKeyLength = nKeyLength; INIT_DATA(ht, p, pData, nDataSize); p->h = h; CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]); if (pDest){ *pDest = p->pData; } HANDLE_BLOCK_INTERRUPTIONS(); CONNECT_TO_GLOBAL_DLLIST(p, ht); ht->arBuckets[nIndex]= p; HANDLE_UNBLOCK_INTERRUPTIONS(); ht->nNumOfElements++; ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* Ifthe Hash table is full, resize it */ return SUCCESS; } 11.3.5 宏CONNECT_TO_BUCKET_DLLIST #define CONNECT_TO_BUCKET_DLLIST(element, list_head) \ (element)->pNext= (list_head); \ (element)->pLast= NULL; \ if((element)->pNext) { \ (element)->pNext->pLast= (element); \ } 這個宏是將bucket加入到bucket鏈表中 11.3.6 其他函數或者宏定義 我們只是簡單的介紹一下HashTable,如果你想細致的了解HashTable的話,建議您看看php的源代碼,關於HashTable的代碼在Zend/zend_hash.h 和Zend/zend_hash.c中。 zend_hash_add_empty_element 給函數增加一個空元素 zend_hash_del_key_or_index 根據索引刪除元素 zend_hash_reverse_apply 反向遍歷HashTable zend_hash_copy 拷貝 _zend_hash_merge 合並 zend_hash_find 字符串索引方式查找 zend_hash_index_find 數值索引方法查找 zend_hash_quick_find 上面兩個函數的封裝 zend_hash_exists 是否存在索引 zend_hash_index_exists 是否存在索引 zend_hash_quick_exists 上面兩個方法的封裝