## 擴展開發前言
擴展作用:重新定義PHP行為對PHP進行HACK,提供內部函數或內部類,提升執行性能等
---
### PHP擴展分類
PHP中的擴展分為兩類:PHP擴展、Zend擴展,對內核而言這兩個分別稱之為:模塊(module)、擴展(extension),我們主要介紹是PHP擴展,也就是模塊.
#### 加載區別:
* PHP擴展(又名PHP“模塊”)使用“extension = test.so”行加載到INI文件中
* Zend擴展使用“zend_extension = test.so”行加載到INI文件中
Zend擴展比PHP擴展更復雜,因為它們有更多的鉤子,而且更接近Zend引擎及其虛擬機(整個PHP源代碼中最復雜的部分).
Zend擴展例子如:OPCache,XDebug,phpdbg,Zend擴展通常用來處理兩種任務:調試器和剖析器.如果您的目標是“只是”向PHP 添加一些新概念(函數,類,常量等),那么您將使用PHP擴展,但如果需要更改PHP的當前行為,可能Zend擴展將會更好.
## PHP擴展生命周期

```code
/* Zend擴展結構 -- zend_extension.h */
struct _zend_extension {
... /* 擴展基礎信息 */
startup_func_t startup; // STARTUP() */
shutdown_func_t shutdown; // SHUTDOWN() 模塊關閉 */
activate_func_t activate; // ACTIVE() 請求啟動 */ */
deactivate_func_t deactivate; // DEACTIVATE() 請求關閉 */
message_handler_func_t message_handler; // MESSAGE_HANDLER() 在擴展注冊后調用 */
op_array_handler_func_t op_array_handler; //在腳本編譯后(zend compilation)后調用的鉤子函數 */
statement_handler_func_t statement_handler; /* */
fcall_begin_handler_func_t fcall_begin_handler; /* 在處理opcode時調用 */
fcall_end_handler_func_t fcall_end_handler; /* */
op_array_ctor_func_t op_array_ctor; /* 構造OPArray時調用 */
op_array_dtor_func_t op_array_dtor; /* 銷毀OPArray時調用 */
int (*api_no_check)(int api_no); /* API_NO_CHECK() 用來檢測擴展是否兼容 */
int (*build_id_check)(const char* build_id); /* BUILD_ID_CHECK() */
...
DL_HANDLE handle; /* dlopen()返回句柄 */
int resource_number; /* 用于管理該擴展名的內部編號 */
};
/* php擴展(模塊)結構 -- zend_modules.h */
struct _zend_module_entry {
...
int (*module_startup_func)(INIT_FUNC_ARGS); /* MINIT() 模塊初始化回調函數,通過PHP_MINIT_FUNCTION()或ZEND_MINIT_FUNCTION()宏完成定義 */
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); /* MSHUTDOWN() 模塊關閉階段回調的函數.,通過PHP_MSHUTDOWN_FUNCTION()或ZEND_MSHUTDOWN_FUNCTION()定義, */
int (*request_startup_func)(INIT_FUNC_ARGS); /* RINIT() 請求開始前回調函數,通過PHP_RINIT_FUNCTION()或ZEND_RINIT_FUNCTION()宏定義. */
int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); /* RSHUTDOWN() 請求結束時回調函數,通過PHP_RSHUTDOWN_FUNCTION()或ZEND_RSHUTDOWN_FUNCTION()宏定義 */
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); /* PHPINFO() php_info展示的擴展信息處理函數,調用phpinfo()時觸發此函數 */
...
void (*globals_ctor)(void *global); /* GINIT() This funtion is called to initialize a module's globals before any module_startup_func. */
void (*globals_dtor)(void *global); /* GSHUTDOWN() This funtion is called to deallocate a module's globals after any module_shutdown_func. */
int (*post_deactivate_func)(void); /* PRSHUTDOWN() 晚于RSHUTDOWN調用, post-RSHUTDOWN function */
...
};
```
module_startup/module_shutdown:可用來注冊/銷毀類,全局變量,INI配置,常量
request_startup/request_shutdown:可用來注冊/銷毀特定于每個請求的變量
混合擴展可以注冊為zend擴展或php模塊后,在啟動函數(startup或minit)中再注冊另一個結構.
```c
#include "php.h"
#include "Zend/zend_extensions.h"
#include "php_pib.h"
#define PRINT(what) fprintf(stderr, what "\n");
/* Declared as static, thus private */
static zend_module_entry pib_module_entry = {
STANDARD_MODULE_HEADER,
"pib",
NULL, /* Function entries */
PHP_MINIT(pib), /* Module init */
PHP_MSHUTDOWN(pib), /* Module shutdown */
PHP_RINIT(pib), /* Request init */
PHP_RSHUTDOWN(pib), /* Request shutdown */
NULL, /* Module information */
"0.1", /* Replace with version number for your extension */
STANDARD_MODULE_PROPERTIES
};
/* This line should stay commented
ZEND_GET_MODULE(pib)
*/
zend_extension_version_info extension_version_info = {
ZEND_EXTENSION_API_NO,
ZEND_EXTENSION_BUILD_ID
};
zend_extension zend_extension_entry = {
"pib-zend-extension",
"1.0",
"PHPInternalsBook Authors",
"http://www.phpinternalsbook.com",
"Our Copyright",
pib_zend_extension_startup,
pib_zend_extension_shutdown,
pib_zend_extension_activate,
pib_zend_extension_deactivate,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
STANDARD_ZEND_EXTENSION_PROPERTIES
};
static void pib_zend_extension_activate(void)
{
PRINT("Zend extension new request starting up");
}
static void pib_zend_extension_deactivate(void)
{
PRINT("Zend extension current request is shutting down");
}
static int pib_zend_extension_startup(zend_extension *ext)
{
PRINT("Zend extension is starting up");
/* When the Zend extension part will startup(), make it register
a PHP extension by calling ourselves zend_startup_module() */
return zend_startup_module(&pib_module_entry);
}
static void pib_zend_extension_shutdown(zend_extension *ext)
{
PRINT("Zend extension is shutting down");
}
static PHP_MINIT_FUNCTION(pib)
{
PRINT("PHP extension is starting up");
return SUCCESS;
}
static PHP_MSHUTDOWN_FUNCTION(pib)
{
PRINT("PHP extension is shutting down");
return SUCCESS;
}
static PHP_RINIT_FUNCTION(pib)
{
PRINT("PHP extension new request starting up");
return SUCCESS;
}
static PHP_RSHUTDOWN_FUNCTION(pib)
{
PRINT("PHP extension current request is shutting down");
return SUCCESS;
}
```
#### zend引擎可修改的全局變量函數指針
```c
/* AST, Zend/zend_ast.h: */
void (*zend_ast_process_t)(zend_ast *ast)
/* Compiler, Zend/zend_compile.h: */
zend_op_array *(*zend_compile_file)(zend_file_handle *file_handle, int type)
zend_op_array *(*zend_compile_string)(zval *source_string, char *filename)
/* Executor, Zend/zend_execute.h: */
void (*zend_execute_ex)(zend_execute_data *execute_data)
void (*zend_execute_internal)(zend_execute_data *execute_data, zval *return_value)
/* GC, Zend/zend_gc.h: */
int (*gc_collect_cycles)(void)
/* TSRM, TSRM/TSRM.h: */
void (*tsrm_thread_begin_func_t)(THREAD_T thread_id)
void (*tsrm_thread_end_func_t)(THREAD_T thread_id)
/* Error, Zend/zend.h: */
void (*zend_error_cb)(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args)
/* Exceptions, Zend/zend_exceptions.h: */
void (*zend_throw_exception_hook)(zval *ex)
/* Lifetime, Zend/zend.h: */
void (*zend_on_timeout)(int seconds)
void (*zend_interrupt_function)(zend_execute_data *execute_data)
void (*zend_ticks_function)(int ticks)
```
## 編碼規則
php-src/CODING_STANDARDS
* 用來區別函數行為的數值,除了0,1外,盡量定義為常量.
* 用PHP封裝好的函數(emalloc(), efree(), estrdup())分配內存.
* 變量名和函數名為小寫字母加下劃線組成,盡量短但不要用縮寫.
* 如果是同一父集下的函數,使用parent_*加前綴的命名方法,如(file_get_contents,file_put_contents).
* 類名和類中的方法使用駝峰命名法命名,類名首字母大寫,類的方法首字母小寫(如 FooBar->getData() ).
* 不要使用`// ...`風格的注釋,使用`/* ... */`格式的注釋。
* 使用tab縮進(四個空格的空間).
* 變量聲明和語句塊之間留一個空行, 邏輯語句塊之間也要有空行, 函數與函數之間留一到兩個空行.
* 預處理語句(例如 #if)必須寫在第一列,如果要縮進預處理語句也要把 # 號放在一行的開始, 緊接著是任意數量的空格
## 編譯安裝PHP
```shell
make clean
./configure --prefix=/home/ll/workspace/C/php-7-1-8-install --disable-all --enable-cli --enable-debug
make -j4
make install
```
#### TODO::擴展的加載,和鉤子函數的調用過程
## 編寫PHP擴展(模塊)的步驟:
1. 通過ext目錄下ext_skel腳本生成擴展的基本框架:./ext_skel --extname;
2. 修改config.m4配置:設置編譯配置參數、設置擴展的源文件、依賴庫/函數檢查等等;
3. 編寫擴展要實現的功能:按照PHP擴展的格式以及PHP提供的API編寫功能;
4. 生成configure:擴展編寫完成后執行phpize腳本生成configure及其它配置文件;
5. 編譯&安裝:./configure、make、make install,然后將擴展的.so路徑添加到php.ini中.
``
## PHP擴展(模塊)生成和編譯
骨架生成器腳本位于php-src/ext/ext_skel中,其使用的模板存儲在 php-src/ext/skeleton
### 基本用法
```code
> php-src/ext/ext_skel
./ext_skel --extname=module [--proto=file] [--stubs=file] [--xml[=file]]
[--skel=dir] [--full-xml] [--no-help]
--extname=module module為你的擴展名稱 (module is the name of your extension)
--proto=file 從file原型文件創建一組PHP函數,方便開發基于庫的擴展.(file contains prototypes of functions to create)
--stubs=file generate only function stubs in file
--xml generate xml documentation to be added to phpdoc-cvs
--skel=dir 用于指定用一套修改過的框架文件來工作.(path to the skeleton directory)
--full-xml generate xml documentation for a self-contained extension
(not yet implemented)
--no-help 指定此參數會造成 ext_skel 會在生成的文件里省略很多有用的注釋。do not try to be nice and create comments in the code
and helper functions to test if the module compiled
> php-src/ext/ext_skel --extname=pib
> cd php-src/ext
> tree pib/
pib/
├── config.m4 //UNIX 構建系統配置
├── config.w32 //Windows 構建系統配置
├── CREDITS //擴展描述文件,包含擴展名,開發者信息。默認生成時只帶有擴展名
├── EXPERIMENTAL //實驗功能說明
├── php_pib.h //包含附加的宏、原型和全局量
├── pib.c //擴展源文件
├── pib.php //測試文件
└── tests //測試腳本目錄
└── 001.phpt //測試腳本。測試方法:php ../../run-tests.php ./pib/001.phpt (run-tests.php文件存在php源碼根目錄)
```
#### proto原型文件
原型文件有點類似 C 頭文件,根據其中申明的函數,生成函數骨架代碼和其他相關代碼。如 pib.proto,內容為 `string pib_hello_world (string name)`
原型文件的格式,類似于 C 頭文件中的函數申明的方式,返回值、函數名、形參類型、形參名。 參數用 () 包裹,多個參數以 , 分隔,函數申明末尾不需要以 ; 結尾,一行一個函數聲明。
原型文件的生成依賴于 awk 腳本 ext/skeleton/create_stubs ,由其中 convert 函數可知,其支持的參數類型有
```code
int,long
bool,boolean
double,float
string
array,object,mixed
resource,handle
```
## 修改config.m4
config.m4是擴展的編譯配置文件,它被include到configure.in文件中,最終被autoconf編譯為configure,編寫擴展時我們只需要在config.m4中修改配置即可.
首先修改 config.m4 ,去掉 PHP_ARG_ENABLE 和 --enable-pib 這兩行前面的 dnl,dnl 是注釋符號。修改后如下
```code
PHP_ARG_ENABLE(pib, whether to enable pib support,
dnl Make sure that the comment is aligned:
[ --enable-pib Enable pib support])
```
PHP_ARG_ENABLE函數第一參數表示擴展名,第二個和第三個在生成configure時的一些提示信息。--enable-extname表示不依賴第三方庫,而--with-extname表示需要第三方庫。
PHP_NEW_EXTENSION(pib, pib.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)函數聲明了這個擴展的名稱、需要的源文件名、此擴展的編譯形式。
如果是多個文件的話,就在第二個參數后加空格再填寫你的源文件地址,需要換行的話記得反斜杠\。
### config.m4常用宏
PHP在acinclude.m4中基于autoconf/automake的宏封裝了很多可以直接使用的宏,下面介紹幾個比較常用的宏:
* `PHP_ARG_WITH(arg_name,check message,help info)`: 定義一個--with-feature[=arg]這樣的編譯參數,調用的是autoconf的AC_ARG_WITH,這個宏有5個參數,常用的是前三個,分別表示:參數名、執行./configure是展示信息、執行--help時展示信息,第4個參數為默認值,如果不定義默認為"no",通過這個宏定義的參數可以在config.m4中通過$PHP_參數名(大寫)訪問.比如:PHP_ARG_WITH(aaa, aaa-configure, help aa)后面通過$PHP_AAA就可以讀取到--with-aaa=xxx設置的值了
* `PHP_ARG_ENABLE(arg_name,check message,help info)`: 定義一個--enable-feature[=arg]或--disable-feature參數,--disable-feature等價于--enable-feature=no,這個宏與PHP_ARG_WITH類似,通常情況下如果配置的參數需要額外的arg值會使用PHP_ARG_WITH,而如果不需要arg值,只用于開關配置則會使用PHP_ARG_ENABLE.
* `AC_MSG_CHECKING()/AC_MSG_RESULT()/AC_MSG_ERROR()`: ./configure時輸出結果,其中error將會中斷configure執行.
* `AC_DEFINE(variable, value, [description])`: 定義一個宏,比如:AC_DEFINE(IS_DEBUG, 1, []),執行autoheader時將在頭文件中生成:#define IS_DEBUG 1.
* `PHP_ADD_INCLUDE(path)`: 添加include路徑,即:gcc -Iinclude_dir,#include "file";將先在通過-I指定的目錄下查找,擴展引用了外部庫或者擴展下分了多個目錄的情況下會用到這個宏.
* `PHP_CHECK_LIBRARY(library, function [, action-found [, action-not-found [, extra-libs]]])`: 檢查依賴的庫中是否存在需要的function,action-found為存在時執行的動作,action-not-found為不存在時執行的動作,比如擴展里使用到線程pthread,檢查pthread_create(),如果沒找到則終止./configure執行:
```code
PHP_ADD_INCLUDE(pthread, pthread_create, [], [
AC_MSG_ERROR([not find pthread_create() in lib pthread])
])
```
* `AC_CHECK_FUNC(function, [action-if-found], [action-if-not-found])`: 檢查函數是否存在. (8)PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $XXX_DIR/$PHP_LIBDIR, XXX_SHARED_LIBADD): 添加鏈接庫.
* `PHP_NEW_EXTENSION(extname, sources [, shared [, sapi_class [, extra-cflags [, cxx [, zend_ext]]]]])`: 注冊一個擴展,添加擴展源文件,確定此擴展是動態庫還是靜態庫,每個擴展的config.m4中都需要通過這個宏完成擴展的編譯配置.
更多autoconf及PHP封裝的宏大家可以在用到的時候再自行檢索,同時ext目錄下有大量的示例可供參考.
[參考鏈接](https://secure.php.net/manual/en/internals2.buildsys.configunix.php)
## 編譯安裝測試擴展
```shell
cd ext/pib
../../../php-7-1-8-install/bin/phpize
./configure --with-php-config=../../../php-7-1-8-install/bin/php-config
make
cd ../../sapi/cli
./php -d extension='../../ext/pib/modules/pib.so' -r "echo confirm_pib_compiled('hello world');"
```
在擴展目錄(ext/pib/)中,phpize會根據config.m4生一個configure文件.
執行./configure后會生makefiles文件,再執行make . 然后擴展中modules目錄下會發現一個pib.so文件 .
make install 會將擴展復制到PHP安裝路徑的擴展目錄當中 。
測試會輸出Congratulations! You have successfully modified ext/pib/config.m4. Module hello world is now compiled into PHP.
### 發布前執行
`phpize --clean`
## 參考資料:
http://www.phpinternalsbook.com/index.html
https://github.com/pangudashu/php7-internal/
https://secure.php.net/manual/en/internals2.structure.php
http://www.laruence.com/2009/04/28/719.html
https://andot.gitbooks.io/bped/c02s03.html
https://github.com/lxy254069025/php-extension-book
http://php.net/manual/zh/internals2.buildsys.configunix.php
http://www.php-internals.com/book/?p=chapt11/11-02-00-extension-hello-world