## 7.3 擴展的構成及編譯
### 7.3.1 擴展的構成
擴展首先需要創建一個`zend_module_entry`結構,這個變量必須是全局變量,且變量名必須是:`擴展名稱_module_entry`,內核通過這個結構得到這個擴展都提供了哪些功能,換句話說,一個擴展可以只包含一個`zend_module_entry`結構,相當于定義了一個什么功能都沒有的擴展。
```c
//zend_modules.h
struct _zend_module_entry {
unsigned short size; //sizeof(zend_module_entry)
unsigned int zend_api; //ZEND_MODULE_API_NO
unsigned char zend_debug; //是否開啟debug
unsigned char zts; //是否開啟線程安全
const struct _zend_ini_entry *ini_entry;
const struct _zend_module_dep *deps;
const char *name; //擴展名稱,不能重復
const struct _zend_function_entry *functions; //擴展提供的內部函數列表
int (*module_startup_func)(INIT_FUNC_ARGS); //擴展初始化回調函數,PHP_MINIT_FUNCTION或ZEND_MINIT_FUNCTION定義的函數
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); //擴展關閉時回調函數
int (*request_startup_func)(INIT_FUNC_ARGS); //請求開始前回調函數
int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); //請求結束時回調函數
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); //php_info展示的擴展信息處理函數
const char *version; //版本
...
unsigned char type;
void *handle;
int module_number; //擴展的唯一編號
const char *build_id;
};
```
這個結構包含很多成員,但并不是所有的都需要自己定義,經常用到的主要有下面幾個:
* __name:__ 擴展名稱,不能重復
* __functions:__ 擴展定義的內部函數entry
* __module_startup_func:__ PHP在模塊初始化時回調的hook函數,可以使擴展介入module startup階段
* __module_shutdown_func:__ 在模塊關閉階段回調的函數
* __request_startup_func:__ 在請求初始化階段回調的函數
* __request_shutdown_func:__ 在請求結束階段回調的函數
* __info_func:__ php_info()函數時調用,用于展示一些配置、運行信息
* __version:__ 擴展版本
除了上面這些需要手動設置的成員,其它部分可以通過`STANDARD_MODULE_HEADER`、`STANDARD_MODULE_PROPERTIES`宏統一設置,擴展提供的內部函數及四個執行階段的鉤子函數是擴展最常用到的部分,幾乎所有的擴展都是基于這兩部分實現的。有了這個結構還需要提供一個接口來獲取這個結構變量,這個接口是統一的,擴展中通過`ZEND_GET_MODULE(extension_name)`完成這個接口的定義:
```
//zend_API.h
#define ZEND_GET_MODULE(name) \
BEGIN_EXTERN_C()\
ZEND_DLEXPORT zend_module_entry *get_module(void) { return &name##_module_entry; }\
END_EXTERN_C()
```
展開后可以看到,實際就是定義了一個get_module()函數,返回擴展zend_module_entry結構的地址,這就是為什么這個結構的變量名必須是`擴展名稱_module_entry`這種格式的原因。
有了擴展的zend_module_entry結構以及獲取這個結構的接口一個合格的擴展就編寫完成了,只是這個擴展目前還什么都干不了:
```c
#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
zend_module_entry mytest_module_entry = {
STANDARD_MODULE_HEADER,
"mytest",
NULL, //mytest_functions,
NULL, //PHP_MINIT(mytest),
NULL, //PHP_MSHUTDOWN(mytest),
NULL, //PHP_RINIT(mytest),
NULL, //PHP_RSHUTDOWN(mytest),
NULL, //PHP_MINFO(mytest),
"1.0.0",
STANDARD_MODULE_PROPERTIES
};
ZEND_GET_MODULE(mytest)
```
編譯、安裝后執行`php -m`就可以看到my_test這個擴展了。
### 7.3.2 編譯工具
PHP提供了幾個腳本工具用于簡化擴展的實現:ext_skel、phpize、php-config,后面兩個腳本主要配合autoconf、automake生成Makefile。在介紹這幾個工具之前,我們先看下PHP安裝后的目錄結構,因為很多腳本、配置都放置在安裝后的目錄中,比如PHP的安裝路徑為:/usr/local/php7,則此目錄的主要結構:
```c
|---php7
| |---bin //php編譯生成的二進制程序目錄
| |---php //cli模式的php
| |---phpize
| |---php-config
| |---...
| |---etc //一些sapi的配置
| |---include //php源碼的頭文件
| |---php
| |---main //PHP中的頭文件
| |---Zend //Zend頭文件
| |---TSRM //TSRM頭文件
| |---ext //擴展頭文件
| |---sapi //SAPI頭文件
| |---include
| |---lib //依賴的so庫
| |---php
| |---extensions //擴展so保存目錄
| |---build //編譯時的工具、m4配置等,編寫擴展是會用到
| |---acinclude.m4 //PHP自定義的autoconf宏
| |---libtool.m4 //libtool定義的autoconf宏,acinclude.m4、libtool.m4會被合成aclocal.m4
| |---phpize.m4 //PHP核心configure.in配置
| |---...
| |---...
| |---php
| |---sbin //SAPI編譯生成的二進制程序,php-fpm會放在這
| |---var //log、run日志
```
#### 7.3.2.1 ext_skel
這個腳本位于PHP源碼/ext目錄下,它的作用是用來生成擴展的基本骨架,幫助開發者快速生成一個規范的擴展結構,可以通過以下命令生成一個擴展結構:
```c
./ext_skel --extname=擴展名稱
```
執行完以后會在ext目錄下新生成一個擴展目錄,比如extname是mytest,則將生成以下文件:
```c
|---mytest
| |---config.m4 //autoconf規則的編譯配置文件
| |---config.w32 //windows環境的配置
| |---CREDITS
| |---EXPERIMENTAL
| |---include //依賴庫的include頭文件,可以不用
| |---mytest.c //擴展源碼
| |---php_mytest.h //頭文件
| |---mytest.php //用于在PHP中測試擴展是否可用,可以不用
| |---tests //測試用例,執行make test時將執行、驗證這些用例
| |---001.phpt
```
這個腳本主要生成了編譯需要的配置以及擴展的基本結構,初步生成的這個擴展可以成功的編譯、安裝、使用,實際開發中我們可以使用這個腳本生成一個基本結構,然后根據具體的需要逐步完善。
### 7.3.2.2 php-config
這個腳本為PHP源碼中的/script/php-config.in,PHP安裝后被移到安裝路徑的/bin目錄下,并重命名為php-config,這個腳本主要是獲取PHP的安裝信息的,主要有:
* __PHP安裝路徑__
* __PHP版本__
* __PHP源碼的頭文件目錄:__ main、Zend、ext、TSRM中的頭文件,編寫擴展時會用到這些頭文件,這些頭文件保存在PHP安裝位置/include/php目錄下
* __LDFLAGS:__ 外部庫路徑,比如:`-L/usr/bib -L/usr/local/lib`
* __依賴的外部庫:__ 告訴編譯器要鏈接哪些文件,`-lcrypt -lresolv -lcrypt`等等
* __擴展存放目錄:__ 擴展.so保存位置,安裝擴展make install時將安裝到此路徑下
* __編譯的SAPI:__ 如cli、fpm、cgi等
* __PHP編譯參數:__ 執行./configure時帶的參數
* ...
這個腳本在編譯擴展時會用到,執行`./configure --with-php-config=xxx`生成Makefile時作為參數傳入即可,它的作用是提供給configure.in獲取上面幾個配置,生成Makefile。
#### 7.3.2.3 phpize
這個腳本主要是操作復雜的autoconf/automake/autoheader/autolocal等系列命令,用于生成configure文件,GNU auto系列的工具眾多,這里簡單介紹下基本的使用:
__(1)autoscan:__ 在源碼目錄下掃描,生成configure.scan,然后把這個文件重名為為configure.in,可以在這個文件里對依賴的文件、庫進行檢查以及配置一些編譯參數等。
__(2)aclocal:__ automake中有很多宏可以在configure.in或其它.m4配置中使用,這些宏必須定義在aclocal.m4中,否則將無法被autoconf識別,aclocal可以根據configure.in自動生成aclocal.m4,另外,autoconf提供的特性不可能滿足所有的需求,所以autoconf還支持自定義宏,用戶可以在acinclude.m4中定義自己的宏,然后在執行aclocal生成aclocal.m4時也會將acinclude.m4加載進去。
__(3)autoheader:__ 它可以根據configure.in、aclocal.m4生成一個C語言"define"聲明的頭文件模板(config.h.in)供configure執行時使用,比如很多程序會通過configure提供一些enable/disable的參數,然后根據不同的參數決定是否開啟某些選項,這種就可以根據編譯參數的值生成一個define宏,比如:`--enabled-xxx`生成`#define ENABLED_XXX 1`,否則默認生成`#define ENABLED_XXX 0`,代碼里直接使用這個宏即可。比如configure.in文件內容如下:
```sh
AC_PREREQ([2.63])
AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])
AC_CONFIG_HEADERS([config.h])
AC_ARG_ENABLE(xxx, "--enable-xxx if enable xxx",[
AC_DEFINE([ENABLED_XXX], [1], [enabled xxx])
],
[
AC_DEFINE([ENABLED_XXX], [0], [disabled xxx])
])
AC_OUTPUT
```
執行autoheader后將生成一個config.h.in的文件,里面包含`#undef ENABLED_XXX`,最終執行`./configure --enable-xxx`后將生成一個config.h文件,包含`#define ENABLED_XXX 1`。
__(4)autoconf:__ 將configure.in中的宏展開生成configure、config.h,此過程會用到aclocal.m4中定義的宏。
__(5)automake:__ 將Makefile.am中定義的結構建立Makefile.in,然后configure腳本將生成的Makefile.in文件轉換為Makefile。
各步驟之間的轉化關系如下圖:

編寫PHP擴展時并不需要操作上面全部的步驟,PHP提供了兩個編輯好的配置:configure.in、acinclude.m4,這兩個配置是從PHP安裝路徑/lib/php/build目錄下的phpize.m4、acinclude.m4復制生成的,其中configure.in中定義了一些PHP內核相關的配置檢查項,另外這個文件會include每個擴展各自的配置:config.m4,所以編寫擴展時我們只需要在config.m4中定義擴展自己的配置就可以了,不需要關心依賴的PHP內核相關的配置,在擴展所在目錄下執行phpize就可以生成擴展的configure、config.h文件了。
configure.in(phpize.m4):
```sh
AC_PREREQ(2.59)
AC_INIT(config.m4)
...
#--with-php-config參數
PHP_ARG_WITH(php-config,,
[ --with-php-config=PATH Path to php-config [php-config]], php-config, no)
PHP_CONFIG=$PHP_PHP_CONFIG
...
#加載擴展配置
sinclude(config.m4)
...
AC_CONFIG_HEADER(config.h)
AC_OUTPUT()
```
__phpize中的主要操作:__
__(1)phpize_check_configm4:__ 檢查擴展的config.m4是否存在。
__(2)phpize_check_build_files:__ 檢查php安裝路徑下的lib/php/build/,這個目錄下包含PHP自定義的autoconf宏文件acinclude.m4以及libtool;檢查擴展所在目錄。
__(3)phpize_print_api_numbers:__ 輸出PHP Api Version、Zend Module Api No、Zend Extension Api No信息。
```sh
phpize_get_api_numbers()
{
# extracting API NOs:
PHP_API_VERSION=`grep '#define PHP_API_VERSION' $includedir/main/php.h|$SED 's/#define PHP_API_VERSION//'`
ZEND_MODULE_API_NO=`grep '#define ZEND_MODULE_API_NO' $includedir/Zend/zend_modules.h|$SED 's/#define ZEND_MODULE_API_NO//'`
ZEND_EXTENSION_API_NO=`grep '#define ZEND_EXTENSION_API_NO' $includedir/Zend/zend_extensions.h|$SED 's/#define ZEND_EXTENSION_API_NO//'`
}
```
__(4)phpize_copy_files:__ 將PHP安裝位置/lib/php/build目錄下的mkdep.awk scan_makefile_in.awk shtool libtool.m4四個文件拷到擴展的build目錄下,然后將acinclude.m4 Makefile.global config.sub config.guess ltmain.sh run-tests*.php文件拷到擴展根目錄,最后將acinclude.m4、build/libtool.m4合并到擴展目錄下的aclocal.m4文件中。
```sh
phpize_copy_files()
{
test -d build || mkdir build
(cd "$phpdir" && cp $FILES_BUILD "$builddir"/build)
(cd "$phpdir" && cp $FILES "$builddir")
#acinclude.m4、libtool.m4合并到aclocal.m4
(cd "$builddir" && cat acinclude.m4 ./build/libtool.m4 > aclocal.m4)
}
```
__(5)phpize_replace_prefix:__ 將PHP安裝位置/lib/php/build/phpize.m4拷貝到擴展目錄下,將文件中的prefix替換為PHP安裝路徑,然后重命名為configure.in。
```sh
phpize_replace_prefix()
{
$SED \
-e "s#/usr/local/php7#$prefix#" \
< "$phpdir/phpize.m4" > configure.in
}
```
__(6)phpize_check_shtool:__ 檢查/build/shtool。
__(7)phpize_check_autotools:__ 檢查autoconf、autoheader。
__(8)phpize_autotools__ 執行autoconf生成configure,然后再執行autoheader生成config.h。
### 7.3.3 編寫擴展的基本步驟
編寫一個PHP擴展主要分為以下幾步:
* 通過ext目錄下ext_skel腳本生成擴展的基本框架:`./ext_skel --extname`;
* 修改config.m4配置:設置編譯配置參數、設置擴展的源文件、依賴庫/函數檢查等等;
* 編寫擴展要實現的功能:按照PHP擴展的格式以及PHP提供的API編寫功能;
* 生成configure:擴展編寫完成后執行phpize腳本生成configure及其它配置文件;
* 編譯&安裝:./configure、make、make install,然后將擴展的.so路徑添加到php.ini中。
最后就可以在PHP中使用這個擴展了。
### 7.3.4 config.m4
config.m4是擴展的編譯配置文件,它被include到configure.in文件中,最終被autoconf編譯為configure,編寫擴展時我們只需要在config.m4中修改配置即可,一個簡單的擴展配置只需要包含以下內容:
```c
PHP_ARG_WITH(擴展名稱, for mytest support,
Make sure that the comment is aligned:
[ --with-擴展名稱 Include xxx support])
if test "$PHP_擴展名稱" != "no"; then
PHP_NEW_EXTENSION(擴展名稱, 源碼文件列表, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi
```
PHP在acinclude.m4中基于autoconf/automake的宏封裝了很多可以直接使用的宏,下面介紹幾個比較常用的宏:
__(1)PHP_ARG_WITH(arg_name,check message,help info):__ 定義一個`--with-feature[=arg]`這樣的編譯參數,調用的是autoconf的AC_ARG_WITH,這個宏有5個參數,常用的是前三個,分別表示:參數名、執行./configure是展示信息、執行--help時展示信息,第4個參數為默認值,如果不定義默認為"no",通過這個宏定義的參數可以在config.m4中通過`$PHP_參數名(大寫)`訪問,比如:
```sh
PHP_ARG_WITH(aaa, aaa-configure, help aa)
#后面通過$PHP_AAA就可以讀取到--with-aaa=xxx設置的值了
```
__(2)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。
__(3)AC_MSG_CHECKING()/AC_MSG_RESULT()/AC_MSG_ERROR():__ ./configure時輸出結果,其中error將會中斷configure執行。
__(4)AC_DEFINE(variable, value, [description]):__ 定義一個宏,比如:`AC_DEFINE(IS_DEBUG, 1, [])`,執行autoheader時將在頭文件中生成:`#define IS_DEBUG 1`。
__(5)PHP_ADD_INCLUDE(path):__ 添加include路徑,即:`gcc -Iinclude_dir`,`#include "file";`將先在通過-I指定的目錄下查找,擴展引用了外部庫或者擴展下分了多個目錄的情況下會用到這個宏。
__(6)PHP_CHECK_LIBRARY(library, function [, action-found [, action-not-found [, extra-libs]]]):__ 檢查依賴的庫中是否存在需要的function,action-found為存在時執行的動作,action-not-found為不存在時執行的動作,比如擴展里使用到線程pthread,檢查pthread_create(),如果沒找到則終止./configure執行:
```sh
PHP_ADD_INCLUDE(pthread, pthread_create, [], [
AC_MSG_ERROR([not find pthread_create() in lib pthread])
])
```
__(7)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):__ 添加鏈接庫。
__(9)PHP_NEW_EXTENSION(extname, sources [, shared [, sapi_class [, extra-cflags [, cxx [, zend_ext]]]]]):__ 注冊一個擴展,添加擴展源文件,確定此擴展是動態庫還是靜態庫,每個擴展的config.m4中都需要通過這個宏完成擴展的編譯配置。
更多autoconf及PHP封裝的宏大家可以在用到的時候再自行檢索,同時ext目錄下有大量的示例可供參考。
- 目錄
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 第3章 Zend虛擬機
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.2 函數實現
- 3.3.2 執行流程
- 3.3.3 函數的執行流程
- 3.3.4 全局execute_data和opline
- 3.4 面向對象實現
- 3.4.1 類
- 3.4.2 對象
- 3.4.3 繼承
- 3.4.4 動態屬性
- 3.4.5 魔術方法
- 3.4.6 類的自動加載
- 3.5 運行時緩存
- 3.6 Opcache
- 3.6.1 opcode緩存
- 3.6.2 opcode優化
- 3.6.3 JIT
- 第4章 PHP基礎語法實現
- 4.1 類型轉換
- 4.2 選擇結構
- 4.3 循環結構
- 4.4 中斷及跳轉
- 4.5 include/require
- 4.6 異常處理
- 第5章 內存管理
- 5.1 Zend內存池
- 5.2 垃圾回收
- 第6章 線程安全
- 6.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 6.1 什么是線程安全
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.6 函數
- 7.7 zval的操作
- 7.8 常量
- 7.9 面向對象
- 7.9.1 內部類注冊
- 7.9.2 定義成員屬性
- 7.9.3 定義成員方法
- 7.9.4 定義常量
- 7.9.5 類的實例化
- 7.10 資源類型
- 7.11 經典擴展解析
- 7.8.1 Yaf
- 7.8.2 Redis
- 第8章 命名空間
- 8.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- 附錄1:break/continue按標簽中斷語法實現
- 附錄2:defer推遲函數調用語法的實現
- 8.1 概述