PHP擴展開發(fā)總結(jié)

使用PHP擴展的原因:
①如果應用注重效率,使用非常復雜的算法,推薦使用PHP擴展。
②有些系統(tǒng)調(diào)用PHP不能直接訪問(如Linux的fork()函數(shù)創(chuàng)建進程),需要編寫成PHP擴展。
③應用不想暴露關(guān)鍵代碼,可以創(chuàng)建擴展使用。
準備工作
一:了解PHP源碼目錄

網(wǎng)上下載下來PHP 5.4版本源代碼,目錄結(jié)構(gòu)如下:

php-5.4.30
  |____build    --和編譯有關(guān)的目錄,里面包括wk,awk和sh腳本用于編譯處理,其中m4文件是linux下編譯程序自動生成的文件,可以使用buildconf命令操作具體的配置文件。
  |____ext      --擴展庫代碼,例如Mysql,gd,zlib,xml,iconv 等我們熟悉的擴展庫,ext_skel是linux下擴展生成腳本,windows下使用ext_skel_win32.php。
  |____main     --主目錄,包含PHP的主要宏定義文件,php.h包含絕大部分PHP宏及PHP API定義。
  |____netware  --網(wǎng)絡目錄,只有sendmail_nw.h和start.c,分別定義SOCK通信所需要的頭文件和具體實現(xiàn)。
  |____pear     --擴展包目錄,PHP Extension and Application Repository。
  |____sapi     --各種服務器的接口調(diào)用,如Apache,IIS等。
  |____scripts  --linux下的腳本目錄。
  |____tests    --測試腳本目錄,主要是phpt腳本,由--TEST--,--POST--,--FILE--,--EXPECT--組成,需要初始化可添加--INI--部分。
  |____TSRM     --線程安全資源管理器,Thread Safe Resource Manager保證在單線程和多線程模型下的線程安全和代碼一致性。
  |____win32    --Windows下編譯PHP 有關(guān)的腳本。
  |____Zend     --包含Zend引擎的所有文件,包括PHP的生命周期,內(nèi)存管理,變量定義和賦值以及函數(shù)宏定義等等。
二:自動構(gòu)建工具

本篇針對Linux環(huán)境下創(chuàng)建PHP擴展,使用擴展自動構(gòu)建工具為ext_skel,Windows下使用ext_skel_win32.php,構(gòu)建方式略有不同,其余開發(fā)無差別。
構(gòu)建PHP擴展的步驟如下(不唯一):

①cd php_src/ext
②./ext_skel --extname=XXX
    此時當前目錄下會生成一個名為XXX的文件夾
③cd XXX/
④vim config.m4
    會有這段文字:
    dnl If your extension references something external, use with:
    dnl PHP_ARG_WITH(say, for say support,
    dnl Make sure that the comment is aligned:
    dnl [  --with-say             Include say support])
    dnl Otherwise use enable:
    dnl PHP_ARG_ENABLE(say, whether to enable say support,
    dnl Make sure that the comment is aligned:
    dnl [  --enable-say           Enable say support])
其中,dnl 是注釋符號。上面的代碼說,如果你所編寫的擴展如果依賴其它的擴展或者lib庫,需要去掉PHP_ARG_WITH相關(guān)代碼的注釋。否則,去掉 PHP_ARG_ENABLE 相關(guān)代碼段的注釋。本篇的擴展不依賴其他擴展,故修改為:
    dnl If your extension references something external, use with:
    dnl PHP_ARG_WITH(say, for say support,
    dnl Make sure that the comment is aligned:
    dnl [  --with-say             Include say support])
    dnl Otherwise use enable:
    PHP_ARG_ENABLE(say, whether to enable say support,
    Make sure that the comment is aligned:
    [  --enable-XXX           Enable say support])
⑤在XXX.c中具體實現(xiàn)
⑥編譯安裝
    phpize
    ./configure --with-php-config=php_path/bin/php-config
    make && make install
⑦修改php.ini文件
    增加
    [XXX]
    extension = XXX.so
三:了解PHP生命周期

任何一個PHP實例都會經(jīng)過Module init、Request init、Request shutdown和Module shutdown四個過程。
1.Module init
在所有請求到達前發(fā)生,例如啟動Apache服務器,PHP解釋器隨之啟動,相關(guān)的各個模塊(Redis、Mysql等)的MINIT方法被調(diào)用。僅被調(diào)用一次。創(chuàng)建XXX擴展后,相應的XXX.c文件中將自動生成該方法:

PHP_MINIT_FUNCTION(XXX) {  
    return SUCCESS;   
}

2.Request init
每個請求達到時都被觸發(fā)。SAPI層將控制權(quán)交由PHP層,PHP初始化本次請求執(zhí)行腳本所需的環(huán)境變量,函數(shù)列表等,調(diào)用所有模塊的RINIT函數(shù)。XXX.c中對應函數(shù)如下:

PHP_RINIT_FUNCTION(XXX){
    return SUCCESS;
}

3.Request shutdown
每個請求結(jié)束,PHP就會自動清理程序,順序調(diào)用各個模塊的RSHUTDOWN方法,清除程序運行期間的符號表。典型的RSHUTDOWN方法如:

PHP_RSHUTDOWN_FUNCTION(XXX){
    return SUCCESS;
}

4.Module shutdown
所有請求處理完畢后,SAPI也關(guān)閉了(即服務器關(guān)閉),PHP調(diào)用各個模塊的MSHUTDOWN方法釋放內(nèi)存。

PHP_MSHUTDOWN_FUNCTION(XXX){
    return SUCCESS;
}

PHP的生命周期常見如下幾種

①單進程SAPI生命周期
②多進程SAPI生命周期
③多線程SAPI聲明周期

這與PHP的運行模式有很大關(guān)系,常見的運行模式有CLI、CGI、FastCGI和mod_php。

①CLI模式——單進程SAPI生命周期

所謂CLI模式,即在終端界面通過php+文件名的方式執(zhí)行PHP文件


單進程SAPI生命周期

輸入命令后,依次調(diào)用MINIT,RINIT,RSHUTDOWN,MSHUTDOWN即完成生命周期,一次只處理一個請求。

②CGI模式——單進程SAPI生命周期

和CLI模式一樣,請求到達時,為每個請求fork一個進程,一個進程只對一個請求做出響應,請求結(jié)束后,進程也就結(jié)束了。
與CLI模式不同的是,CGI可以看作是規(guī)定了Web Server與PHP的交流規(guī)則,相當于執(zhí)行response = exec("php -f xxx.php -url=xxx -cookie=xxx -xxx=xxx")。

③FastCGI模式——多進程SAPI生命周期

CGI模式存在明顯缺點,每個進程處理一個請求及結(jié)束,新請求過來需要重新加載php.ini,調(diào)用MINIT等函數(shù)。FastCGI相當于可以執(zhí)行多個請求的CGI,處理完一個請求后進程不結(jié)束,等待下一個請求到來。
服務啟動時,F(xiàn)astCGI先啟動多個子進程等待處理請求,避免了CGI模式請求到來時fork()進程(即fork-and-execute),提高效率。


多進程SAPI生命周期
④mod_php模式——多進程SAPI生命周期

該模式將PHP嵌入到Apache中,相當于給Apache增加了解析PHP的功能。PHP隨服務器的啟動而啟動,兩者之間存在從屬關(guān)系。
證明:
CGI模式下,修改php.ini無需重啟服務器,每個請求結(jié)束后,進程自動結(jié)束,新請求到來時會重新讀取php.ini文件創(chuàng)建新進程。
mod_php下,進程是啟動即創(chuàng)建,只有結(jié)束現(xiàn)有進程,重新啟動服務器讀取PHP配置創(chuàng)建新進程,修改才有效。

多線程SAPI模式

多線程模式和多進程模式的某個進程類似,在整個生命周期中會并行重復著請求開始,請求結(jié)束的環(huán)節(jié)。
只有一個服務器進程運行,但同時運行多個線程,優(yōu)點是節(jié)省資源開銷,MINIT和MSHUTDOWN只需在Web Server啟動和結(jié)束時執(zhí)行一次。由于線程的特質(zhì),使得各個請求之間共享數(shù)據(jù)成為可能。


多線程SAPI模式
四:PHP內(nèi)核中的變量

PHP變量的弱類型實現(xiàn)在之前的文章中講到,可以參讀:http://m.itdecent.cn/p/ef0c91be06a0 PHP的實現(xiàn)方式即PHP變量在內(nèi)核中的存儲。
PHP提供了一系列內(nèi)核變量的訪問宏。推薦使用它們設置和訪問PHP的變量類型和值。
變量類型:

Z_TYPE(zval)  可以獲取和設置變量類型
Z_TYPE(zval)函數(shù)返回變量的類型,PHP變量類型有:
IS_NULL(空類型),IS_LONG(整型),IS_DOUBLE(浮點型),IS_STRING(字符串),
IS_ARRAY(數(shù)組類型),IS_OBJECT(對象類型),IS_BOOL(布爾類型),IS_RESOURCE(資源類型)

可以通過
Z_TYPE(zval) = IS_STRING的方式直接設置變量類型

變量值對應的訪問宏:

整數(shù)類型  Z_LVAL(zval)  對應zval的實體;Z_VAL_P(&zval)  對應結(jié)構(gòu)體的指針;Z_VAL_PP(&&zval)  對應結(jié)構(gòu)體的二級指針
浮點數(shù)類型 Z_DVAL(zval)    Z_DVAL_P(&zval)    Z_DVAL_PP(&&zval)
布爾類型 Z_BVAL(zval)    Z_BVAL_P(&zval)    Z_BVAL_PP(&&zval)
字符串類型 
    獲取值:Z_STRVAL(zval)    Z_STRVAL_P(&zval)    Z_STRVAL_PP(&&zval)
    獲取長度:Z_STRLEN(zval)    Z_STRLEN_P(&zval)    Z_STRLEN_PP(&&zval)
數(shù)組類型 Z_ARRVAL(zval)    Z_ARRVAL_P(&zval)    Z_ARRVAL_PP(&&zval)
資源類型 Z_RESVAL(zval)    Z_RESVAL_P(&zval)    Z_RESVAL_PP(&&zval)
五:了解Zend API
1.Zend引擎

Zend引擎就是腳本語言引擎(解釋器+虛擬機),負責解析、翻譯和執(zhí)行PHP腳本。其工作流程大致如下:

①Zend Engine Compiler編譯PHP腳本為Opcode
②Opcode由Zend Engine Executor解析執(zhí)行,期間Zend Engine Executor負責調(diào)用使用到的PHP extension
2.Zend內(nèi)存管理

使用C語言開發(fā)PHP擴展,需要注意內(nèi)存管理。忘記釋放內(nèi)存將造成內(nèi)存泄漏,釋放多次則產(chǎn)生系統(tǒng)錯誤。Zend引擎提供了一些內(nèi)存管理的接口,使用這些接口申請內(nèi)存交由Zend管理。
常見接口:

emalloc(size_t size)    申請size大小的內(nèi)存
efree(void *ptr)    釋放ptr指向的內(nèi)存塊
estrdup(char *str)    申請str大小的內(nèi)存,并將str內(nèi)容復制進去
estrndup(char *str, int slen)    同上,但指定長度復制
ecalloc(size_t numOfElem, size_t sizeOfElem)    復制numOfElem個sizeOfElem大小的內(nèi)存塊
erealloc(void *ptr, size_t nsize)    ptr指向內(nèi)存塊的大小擴大到nsize

內(nèi)存管理申請的所有內(nèi)存,將在腳本執(zhí)行完畢和處理請求終止時被釋放。

3.PHP擴展架構(gòu)

使用準備工作(二)中命令生成基本架構(gòu)后,生成的對應目錄中會有兩個文件,php_XXX.h和XXX.c,其中php_XXX.h文件用于聲明擴展的一些基本信息和實現(xiàn)的函數(shù),注意,只是聲明。具體的實現(xiàn)在XXX.c中。
php_XXX.h大致結(jié)構(gòu)如下:

#ifndef PHP_XXX_H
#define PHP_XXX_H

extern zend_module_entry php_list_module_entry;
#define phpext_php_list_ptr &php_list_module_entry

#define PHP_XXX_VERSION "0.1.0"

#ifdef PHP_WIN32
#   define PHP_XXX_API __declspec(dllexport)
#elif defined(__GNUC__) && __GNUC__ >= 4
#   define PHP_XXX_API __attribute__ ((visibility("default")))
#else
#   define PHP_XXX_API
#endif
#ifdef ZTS
#include "TSRM.h"
#endif

PHP_MINIT_FUNCTION(XXX);
PHP_MSHUTDOWN_FUNCTION(XXX);
PHP_RINIT_FUNCTION(XXX);
PHP_RSHUTDOWN_FUNCTION(XXX);
PHP_MINFO_FUNCTION(XXX);

PHP_FUNCTION(confirm_XXX_compiled);

#ifdef ZTS
#define PHP_LIST_G(v) TSRMG(php_list_globals_id, zend_php_list_globals *, v)
#else
#define PHP_LIST_G(v) (php_list_globals.v)
#endif
#endif

大致信息有版本號,MINIT、RINIT、RSHUTDOWN、MSHUTDOWN函數(shù)等,如果聲明自定義函數(shù),可以在之后以PHP_FUNCTION(XXX);的方式聲明函數(shù),并在XXX.c中具體實現(xiàn)。
XXX.c內(nèi)容如下:

---------------------------------------頭文件--------------------------------------
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_XXX.h"

static int le_XXX;
---------------------------------------Zend函數(shù)快--------------------------------------
const zend_function_entry XXX_functions[] = {
    PHP_FE(confirm_php_list_compiled,   NULL)       /* For testing, remove later. */
    PHP_FE_END  /* Must be the last line in php_list_functions[] */
};
---------------------------------------Zend模塊--------------------------------------
zend_module_entry XXX_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
    STANDARD_MODULE_HEADER,
#endif
    "XXX",
    XXX_functions,
    PHP_MINIT(XXX),
    PHP_MSHUTDOWN(XXX),
    PHP_RINIT(XXX),     /* Replace with NULL if there's nothing to do at request start */
    PHP_RSHUTDOWN(XXX), /* Replace with NULL if there's nothing to do at request end */
    PHP_MINFO(XXX),
#if ZEND_MODULE_API_NO >= 20010901
    PHP_XXX_VERSION,
#endif
    STANDARD_MODULE_PROPERTIES
};
---------------------------------------實現(xiàn)get_module函數(shù)--------------------------------------
#ifdef COMPILE_DL_XXX
ZEND_GET_MODULE(XXX)
#endif
---------------------------------------生命周期函數(shù)--------------------------------------
PHP_MINIT_FUNCTION(XXX)
{
    return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(XXX)
{
    return SUCCESS;
}

PHP_RINIT_FUNCTION(XXX)
{
    return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION(XXX)
{
    return SUCCESS;
}
---------------------------------------導出函數(shù)--------------------------------------
PHP_FUNCTION(confirm_XXX_compiled)
{
    char *arg = NULL;
    int arg_len, len;
    char *strg;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
        return;
    }

    len = spprintf(&strg, 0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "php_list", arg);
    RETURN_STRINGL(strg, len, 0);
}
---------------------------------------負責擴展info顯示--------------------------------------
PHP_MINFO_FUNCTION(XXX)
{
    php_info_print_table_start();
    php_info_print_table_header(2, "XXX support", "enabled");
    php_info_print_table_end();
}

①擴展頭文件
所有擴展務必包含的頭文件有且只有一個——php.h,以使用PHP定義的各種宏和API。如果在php_XXX.h中聲明了擴展相關(guān)的宏和函數(shù),需要將其引入。
②導出函數(shù)
先說導出函數(shù),方便理解Zend函數(shù)塊。PHP能夠調(diào)用擴展中的類和方法都是通過導出函數(shù)實現(xiàn)。導出函數(shù)就是按照PHP內(nèi)核要求編寫的函數(shù),形式如下:

void zif_extfunction(){  //extfunction即為擴展中實現(xiàn)的函數(shù)
    int ht;    //函數(shù)參數(shù)的個數(shù)
    zval *return_value;    //保存函數(shù)的返回值
    zval *this_ptr;    //指向函數(shù)所在對象
    int return_value_used;    //函數(shù)返回值腳本是否使用,0——不使用;1——使用
    zend_executor_globals *executor_globals;    //指向Zend引擎的全局設置
}

有如上定義,PHP腳本中即可使用擴展函數(shù)

<?php
    extfunction();
?>

由于導出函數(shù)格式固定,Zend引擎通過PHP_FUNCTION()宏聲明

PHP_FUNCTION(extfunction);

即可產(chǎn)生之前代碼塊中的導出函數(shù)結(jié)構(gòu)體。
③Zend函數(shù)塊
作用是將編寫的函數(shù)引入Zend引擎,通過zend_function_entry結(jié)構(gòu)體引入。zend_function_entry結(jié)構(gòu)體聲明如下:

typedef struct _zend_function_entry{
    char *fname;    //指定在PHP腳本里定義的函數(shù)名
    void (*handler)(INTERNAL_FUNCTION_PARAMETERS);  //指向?qū)С龊瘮?shù)的句柄
    unsigned char *func_arg_types;  //標示一些參數(shù)是否強制按引用方式傳遞,通常設為NULL
} zend_function_entry;

Zend引擎通過zend_function_entry數(shù)組將導出函數(shù)引入內(nèi)部。方式:

zend_function_entry XXX_functions[] = {
    PHP_FE(confirm_php_list_compiled,   NULL)       /* For testing, remove later. */
    PHP_FE_END  /* Must be the last line in php_list_functions[] */
};

PHP_FE宏會把zend_function_entry結(jié)構(gòu)補充完整。

PHP_FE(extfunction);  ===>  ("extfunction", zif_extfunction, NULL);

PHP_FE_END是告知Zend引擎Zend函數(shù)塊到此為止,有的版本可以使用{NULL, NULL, NULL}的方式。但推薦使用本文方式以兼容。
④Zend模塊聲明
Zend模塊包含所有需要向Zend引擎提供的擴展模塊信息,底層由zend_module_entry結(jié)構(gòu)體大體實現(xiàn)

typedef struct _zend_module_entry{
    unsigned short size;       ---|
    unsigned int zend_api;        |
    unsigned char zend_debug;     |--> 通常用STANDARD_MODULE_HEADER填充
    unsigned char zts;         ---|
    char *name;    //模塊名
    zend_function_entry *functions;    //函數(shù)聲明
    int (*module_start_func)(INIT_FUNC_ARGS);    //MINIT函數(shù)
    int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);    //MSHUTDOWN函數(shù)
    int (*request_start_func)(INIT_FUNC_ARGS);    //RINIT函數(shù)
    int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);    //RSHUTDOWN函數(shù)
    char *version;    //版本號
    ...    //其余信息不做討論
} zend_module_entry;

對比生成代碼中的模塊聲明和①②③中所講,Zend通過模塊聲明將所有信息讀取并加入到引擎中。
⑤get_module函數(shù)
當擴展被加載時,調(diào)用get_module函數(shù),該函數(shù)返回一個指向擴展模塊聲明的zend_module_entry指針。是PHP內(nèi)核與擴展通信的渠道。
get_module函數(shù)被條件宏包圍,故有些情況下不會執(zhí)行g(shù)et_module方法,當擴展被編譯為PHP內(nèi)建模塊時get_module方法不被實現(xiàn)。
⑥實現(xiàn)導出函數(shù)
在php_XXX.h中聲明的函數(shù)在XXX.c中具體實現(xiàn),實現(xiàn)方式如自動生成的confirm_XXX_compiled導出函數(shù)形式一致

PHP_FUNCTION(extfunction){
    ...具體實現(xiàn)...
}

在函數(shù)中獲取參數(shù)和返回結(jié)果,后續(xù)講解。
⑦模塊信息函數(shù)
PHP通過phpinfo查看PHP及其擴展信息,PHP_MINFO_FUNCTION負責實現(xiàn)。生成代碼函數(shù)體是最基本的模塊信息,可自行設置顯示內(nèi)容。

4.導出函數(shù)實現(xiàn)須知

這部分主要講解函數(shù)具體實現(xiàn)過程中對參數(shù)和變量的處理。

(1)獲取參數(shù)個數(shù)

通過ZEND_NUM_ARGS宏獲取參數(shù)個數(shù),這個宏實際上是獲取zif_extfunction的ht字段,定義在Zend/zned_API.h下

#define ZEND_NUM_ARGS()  (ht)
(2)取得參數(shù)實體

Zend引擎提供獲取參數(shù)實體的API,聲明如下:

int zend_parse_parameters(int num_args TSRMLS_CC, char *type_spec)

num_args:傳入的參數(shù)個數(shù)
type_spec:參數(shù)的類型,每種類型對應一個字符,當num_args>1時,接收參數(shù)通過字符串依次指明類型接收。
該函數(shù)成功將返回SUCCESS,失敗返回FAILURE。
可以接受的參數(shù)類型如下:

普通:
l : 長整型
d : 雙精度浮點類型
s : 字符串類型及其長度(需要兩個變量保存!?。。?b : 布爾類型
r : 資源類型,保存在zva *l中
a : 數(shù)組,保存在zval *中
o : 對象(任何類型),保存在zval *中
O : 對象(class entry指定類型),保存在zval *中
z : zval *
特殊:
| : 當前及之后的參數(shù)為可選參數(shù),有傳即獲取,否則設為默認值
/ : 當前及之后的參數(shù)將以SEPARATE_IF_NOT_REF的方式進行拷貝,除非是引用
! : 當前及之后的參數(shù)允許為NULL,僅用在r,a,o,O,z類型時

例:獲取一個字符串和一個布爾型參數(shù)

char *str;
int strlen;
zend_bool b;
if(zend_parse_paramsters(ZEND_NUM_ARGS() TSRMLS_CC, "sb", &str, &strlen, &b) == FAILURE){  //字符串需要接收內(nèi)容和長度
    return ;
}

例:獲取一個數(shù)組和一個可選的長整型參數(shù)

zval *arr;
long l;
if(zend_parse_parameter(ZEND_NUM_ARGS() TSRMLS_CC, "a|l", &arr, &l) == FAILURE){
    return ;
}

例:獲取一個對象或NULL

zval *obj;
if(zend_parse_parameter(ZEND_NUM_ARGS() TSRMLS_CC, "o!", &obj) == FAILURE){
    return ;
}
(3)獲取可變參數(shù)

開發(fā)過程中會遇到方法可接受可變參數(shù)的情況,不指定類型的情況下接收參數(shù)的方式:

int num_arg = ZEND_NUM_ARGS();
zval **parameters[num_args];
if(zend_get_parameters_array_ex(num_arg, parameters) == FAILURE){
    return ;
}
(4)參數(shù)類型轉(zhuǎn)換

(3)中PHP可以接受任意類型的參數(shù),可能會導致在具體實現(xiàn)過程中出問題,因此Zend提供了一系列參數(shù)類型轉(zhuǎn)換的API。


參數(shù)類型轉(zhuǎn)換API
(5)處理通過引用傳遞的參數(shù)

"z"代表的zval類型的傳參即為引用傳遞。PHP規(guī)定修改非引用傳遞的參數(shù)值不會影響原來變量的值,但PHP內(nèi)核采用引用傳遞方式傳參。PHP內(nèi)核使用"zval分離"的方式避免這一問題。"zval分離"即寫時復制,修改非引用類型的參數(shù)時,先復制一份新值,然后將引用指向新值,修改參數(shù)時不會影響原值。
判斷參數(shù)是否為引用,通過PZVAL_IS_REF(zval *),其定義為:

#define PZVAL_IS_REF(z) ((z)->is_ref)

使用宏SEPARATE_ZVAL(zval **)實現(xiàn)zval分離。

(6)擴展中創(chuàng)建變量

PHP擴展中創(chuàng)建變量需要以下三步:

①創(chuàng)建一個zval容器
②對zval容器進行填充
③引入到Zend引擎內(nèi)部符號表中

//創(chuàng)建zval容器
zval *new_var;
//初始化和填充
MAKE_STD_ZVAL(new_var);
//引入符號表
ZEND_SET_SYMBOL(EG(active_symbol_table), "new_var", new_var);

MAKE_STD_ZVAL()宏作用是通過ALLOC_ZVAL()申請一個zval空間,之后通過INIT_ZVAL()進行初始化。

#define MAKE_STD_ZVAL(zv) \
  ALLOC_ZVAL(zv); \
  INIT_ZVAL(zv);

INIT_ZVAL()宏定義如下:
#DEFINE INIT_ZVAL(z) \
  (z) -> refcount = 1; \
  (z) -> is_ref = 0;

MAKE_STD_ZVAL()只是為變量分配了內(nèi)存,設置了refcount和is_ref兩個屬性。
ZEND_SET_SYMBOL()宏將變量引入到符號表中,引入時先檢查變量是否已經(jīng)存在于表中,如果已經(jīng)存在,銷毀原有的zval并替換。
如果創(chuàng)建的是全局變量,前兩步不變,只對引入操作做調(diào)整。局部變量引入active_symbol_table中,全局變量引入symbol_table中,通過

ZEND_SET_SYMBOL(&EG(symbol_table), "new_var", new_var);

注意:active_symbol_table是個指針,symbol_table不是指針,需要增加&取地址。
···
擴展中:
PHP_FUNCTION(extfunction){
zval *new_var;
ZEND_STD_ZVAL(new_var);
ZVAL_LONG(new_var, 10);
ZEND_SET_SYMBOL(&EG(symbol_table), "new_var", new_var);
}

PHP腳本中:
<?php
extfunction();
var_dump($new_var);
?>
結(jié)果輸出:10
···

(7)變量賦值

①長整型(整型)賦值
PHP中所有整型都是保存在zval的value字段中,整數(shù)保存在value聯(lián)合體的lval字段中,type為IS_LONG,賦值通過宏操作進行:

ZVAL_LONG(zval, 10);

②雙精度浮點數(shù)類型賦值
浮點數(shù)保存在value的dval中,type對應IS_DOUBLE,通過宏操作

ZVAL_DOUBLE(zval, 3.14);

③字符串類型
value聯(lián)合體的str結(jié)構(gòu)體保存字符串值,val保存字符串,len保存長度,type為IS_STRING。

char *str = "hello world";
ZVAL_STRING(zval, str, 1);  //結(jié)尾參數(shù)表示字符串是否需要被復制。

④布爾類型
值存放在value.lval中,TRUE——1;FALSE——0,type對應IS_BOOL。

賦值為真:ZVAL_BOOL(zval, 1);
賦值為假:ZVAL_BOOL(zval, 0);

⑤數(shù)組類型變量
PHP數(shù)組基于HashTable實現(xiàn),變量賦值為數(shù)組類型時先要創(chuàng)建一個HashTable,保存在value的ht字段中。Zend提供array_init()實現(xiàn)賦值。

array_init(zval);

同時Zend提供了一套完整的關(guān)聯(lián)數(shù)組、索引數(shù)組API用于添加元素,這里不一一列舉。
⑥對象類型變量
對象和數(shù)組類似,PHP中對象可以轉(zhuǎn)換成數(shù)組,但數(shù)組無法轉(zhuǎn)換成對象,會丟失方法。Zend通過object_init()函數(shù)初始化一個對象。

if(object_init(zval) != SUCCESS){
    RETURN_NULL();
}

Zend也提供了對象設置屬性所需的API,和數(shù)組設置元素類似,用到時候找即可。
⑦資源類型
嚴格而言,資源不是數(shù)據(jù)類型,而是一個可以維護任何數(shù)據(jù)類型的抽象,類似C語言的指針。所有資源都保存在一個Zend內(nèi)部的資源列表中,每份資源都有一個指向?qū)嶋H數(shù)據(jù)的指針。
為了及時回收無用的資源,Zend引擎會自動回收引用數(shù)為0的資源的析構(gòu)函數(shù),析構(gòu)函數(shù)需要在擴展中自己定義。
Zend使用統(tǒng)一的zend_register_list_destructors_ex()為資源注冊析構(gòu)函數(shù),該函數(shù)返回一個句柄,將資源與析構(gòu)函數(shù)相關(guān)聯(lián)。定義如下:

ZEND_ZPI int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, 
  rsrc_dtor_func_t  pld, char *type_name, int module_number);
參數(shù)描述:
ld : 普通資源的析構(gòu)函數(shù)
pld : 持久化資源的析構(gòu)函數(shù)
type_name : 為資源類型起的名字,如:fopen()創(chuàng)建的資源名稱為stream
module_number : PHP_MINIT_FUNCTION函數(shù)會定義,可忽略
兩種析構(gòu)函數(shù)至少提供一個,為空可用NULL指定。

資源的析構(gòu)函數(shù)必須如下定義:(resource_destruction_handler)函數(shù)名隨意。

void resource_destruction_handler(zend_rsrc_entry *rsrc TSRMLS_DC){
    -----------具體實現(xiàn)代碼------------
}

其中,rsrc是指向zend_rsrc_entry的指針,結(jié)構(gòu)體結(jié)構(gòu)為:

typedef struct _zend_rsrc_entry{
    void *ptr;  //資源的實際地址,析構(gòu)時釋放
    int type; 
    int refcount;
} zend_rsrc_entry ; 

通過zend_register_list_destructors_ex()函數(shù)返回的資源句柄,通過一個全局變量保存,ext_skel生成的擴展架構(gòu)中,自動生成了一個'le_'為前綴的int型變量,zend_register_list_destructors_ex()在MINIT函數(shù)中使用并完成注冊。如實現(xiàn)鏈表的析構(gòu):

---------------------------------phplist擴展-----------------------------------
static le_phplist;  //架構(gòu)自動生成,保存資源句柄
//定義鏈表節(jié)點
struct ListNode{
    struct ListNode *next;
    void *data;
}
//析構(gòu)函數(shù)具體實現(xiàn)
void phplist_destruction_handler(zend_rsrc_entry *rsrc TSRMLS_DC){
    ListNode *pre, *next;
    pre = (ListNode *)rsrc->ptr;  
    while(pre){
        next = pre -> next;
        free(pre);
        pre = next;
    }
}
//MINIT中注冊析構(gòu)函數(shù)
PHP_MINIT_FUNCTION(phplist){
    //完成注冊
    le_phplist = zend_register_list_destructors_ex(phplist_destruction_handler,
      NULL, "php_list", module_number);
    return SUCCESS;
}

注冊完析構(gòu)函數(shù),需要把資源和句柄關(guān)聯(lián)起來,Zend提供zend_register_resource()函數(shù)或者ZEND_REGISTER_RESOURCE()宏完成這一操作。

int zend_register_resource(zval *rsrc_result, void *rsrc_pointer, int rsrc_type);
參數(shù)解釋:
rsrc_result : 存儲zend_register_resource返回的結(jié)果
rsrc_pointer : 指向保存的資源
rsrc_type : 資源類型

該函數(shù)返回int型結(jié)果,該結(jié)果為資源的id。函數(shù)定義源碼:

int zend_register_resource(zval *rsrc_result, void *rsrc_pointer, int rsrc_type){
    int rsrc_id;
    rsrc_id = zend_list_insert(rsrc_pointer, rsrc_type);  //該函數(shù)將資源加入資源列表,并返回資源在列表中的位置(即id)
    if(rsrc_result){
        rsrc_result -> value.lval = rsrc_id;
        rsrc_result -> type = IS_RESOURCE;
    }
    return rsrc_id;
}

用戶根據(jù)資源的id沖資源列表中獲取資源,Zend定義了ZEND_FETCH_RESOURCE()宏獲取指定的資源。

ZEND_FETCH_RESOURCE(rsrc, rsrc_type, rsrc_id, default_rsrc_id, resource_type_name, resource_type);
其中
rsrc : 保存返回的資源
rsrc_type : 表明想要的資源類型,如 ListNode *等
rsrc_id : 用戶通過PHP腳本傳來的資源id
default_rsrc_id : 沒有獲取到資源時的標識符,一般用-1指定
resource_type_name : 請求資源類的名稱,用于找不到時拋出錯誤信息使用
resource_type : 注冊析構(gòu)函數(shù)時的句柄,即le_phplist

例如獲取用戶指定的list

zval *lrc;
ListNode *list;
//獲取用戶參數(shù)
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &lrc) == FAILURE){
    RETURN_FALSE;
}
//獲取對應資源
ZEND_FETCH_RESOURCE(list, ListNode *, &lrc, -1, "php list", le_phplist);
此時list即為所要獲取的資源。

資源用完需要析構(gòu),當引用數(shù)為0時,Zend對資源進行回收,很多擴展對資源有相應的析構(gòu)函數(shù),比如mysql_connect()的mysql_close(),fopen()對應fclose()。PHP的unset()也可以直接釋放一個資源。
如果想顯示的定義函數(shù)釋放資源,在自定義函數(shù)中調(diào)用zend_list_delete()函數(shù)即可

ZEND_API int zend_list_delete(int id TSRMLS_DC);

該函數(shù)的作用是根據(jù)id將資源的引用數(shù)-1,然后判斷引用數(shù)是否大于0,是則觸發(fā)析構(gòu)函數(shù)清除資源。

(8)錯誤輸出API

Zend推薦使用zend_error()函數(shù)輸出錯誤信息,該函數(shù)定義如下:

ZEND_API void zend_error(int type, char *format, ...)
參數(shù):
type : PHP的6鐘錯誤信息類型
    ①E_ERROR:拋出一個錯誤,腳本將停止運行
    ②E_WARNING : 拋出警告,腳本繼續(xù)執(zhí)行
    ③E_NOTICE : 拋出通知,腳本繼續(xù)執(zhí)行,一般情況下php.ini設置不顯示
    ④E_CORE_ERROR : 拋出PHP內(nèi)核錯誤
    ⑤E_COMPILE_ERROR : 拋出編譯器內(nèi)部錯誤
    ⑥E_COMPILE_WARNING : 拋出編譯器警告
    注意:后三種錯誤不應由自定義擴展模塊拋出!?。?format : 錯誤輸出格式
(9)運行時信息函數(shù)

執(zhí)行PHP腳本出錯時,經(jīng)常會有相關(guān)的運行信息,指出哪個文件,哪個函數(shù),具體哪行有執(zhí)行錯誤,Zend引擎有相關(guān)的實現(xiàn)接口。

查看當前執(zhí)行的函數(shù)名
get_active_function_name(TSRMLS_C);
查看當前執(zhí)行的文件名
zend_get_executed_filename(TSRMLS_C);
查看所在行
zend_get_executed_lineno(TSRMLS_C);

三個函數(shù)都需要以TSRMLS_C為參數(shù),作為訪問執(zhí)行器(Executor)全局變量。TSRM_C是TSRM存儲器,與線程安全相關(guān),之后專門寫篇博客講講。

(10)擴展調(diào)用腳本中用戶自定義函數(shù)

這種情況比較少,但Zend功能全面,支持這類操作。
在擴展中使用用戶自定義函數(shù),通過call_user_function_ex()函數(shù)實現(xiàn),函數(shù)原型:

int call_user_function_ex(HashTable *function_table,   //要訪問的函數(shù)表指針
    zval **object_pp,   //調(diào)用方法的對象,沒有設為NULL
    zval *function_name,   //函數(shù)名
    zval **retval_ptr_ptr,  //保存返回值的指針
    zend_uint param_count,  //參數(shù)個數(shù)
    zval **params[],  //參數(shù)
    int no_separation,  //是否禁止zcal分離操作
    HashTable symbol_table  //符號表,一般設為NULL
    TSRMLS_DC  
);  

其中no_separation為1會禁止zval分離,節(jié)省內(nèi)存,但任何參數(shù)分離將導致操作失敗,通常設為0。
腳本中定義用戶函數(shù)

function userfunc(){
    return "call user function success";
}

-------------------------調(diào)用擴展方法----------------------------
$ret = call_user_function_in_ext();
var_dump($ret);

擴展中需要實現(xiàn)call_user_function_in_ext()函數(shù)

PHP_FUNCTION(call_user_function_in_ext){
    zval **funcName;
    zval *retval;
  
    if(ZEND_NUM_ARGS() != 1 || 
        zend_get_parameters_ex(1, &function_name) == FAILURE){
        zend_error(E_ERROR, "function %s call in extension fail", (*function_name)->value->str->val);
    }

    if((*function_name)->type != IS_STRING){
        zend_error(E_ERROR, "function name must be string");
    }

    if(call_user_function_ex(CG(function_table), NULL, *function_name, &retval, 0, NULL, 0, NULL TSRMLS_DC) != SUCCESS){
        zend_error(E_ERROR, "function call fail");
    }

    zval *ret_val = *retval;
    zval_copy_ctor(ret_val);
    zval_ptr_dtor(&retval);
}

此外Zend還有提供顯示phpinfo的函數(shù),比較簡單,不做講解。

創(chuàng)建擴展

創(chuàng)建一個鏈表操作的擴展,擴展名為phplist,生成架構(gòu)先

cd php_src/ext
./ext_skel --extname=phplist

此時ext目錄下生成phplist/,本例不依賴其他擴展或lib庫,按準備工作(二)修改config.m4文件。
之后實現(xiàn)擴展的函數(shù)。在php_phplist.h中聲明,具體實現(xiàn)在phplist.c中。
php_phplist.h如下:

#ifndef PHP_PHPLIST_H
#define PHP_PHPLIST_H

extern zend_module_entry phplist_module_entry;
#define phpext_phplist_ptr &phplist_module_entry

#define PHP_PHPLIST_VERSION "0.1.0" /* Replace with version number for your extension */

#ifdef PHP_WIN32
#   define PHP_PHPLIST_API __declspec(dllexport)
#elif defined(__GNUC__) && __GNUC__ >= 4
#   define PHP_PHPLIST_API __attribute__ ((visibility("default")))
#else
#   define PHP_PHPLIST_API
#endif

#ifdef ZTS
#include "TSRM.h"
#endif

PHP_MINIT_FUNCTION(phplist);
PHP_MSHUTDOWN_FUNCTION(phplist);
PHP_RINIT_FUNCTION(phplist);
PHP_RSHUTDOWN_FUNCTION(phplist);
PHP_MINFO_FUNCTION(phplist);

PHP_FUNCTION(confirm_phplist_compiled); /* For testing, remove later. */

/*聲明擴展函數(shù)*/
PHP_FUNCTION(list_create);  //創(chuàng)建鏈表
PHP_FUNCTION(list_add_head);    //添加到鏈表頭
PHP_FUNCTION(list_add_tail);    //添加到鏈表尾
PHP_FUNCTION(list_get_index);   //獲取節(jié)點
PHP_FUNCTION(list_get_length);  //獲取鏈表長度
PHP_FUNCTION(list_remove_index);    //移除節(jié)點

#ifdef ZTS
#define PHPLIST_G(v) TSRMG(phplist_globals_id, zend_phplist_globals *, v)
#else
#define PHPLIST_G(v) (phplist_globals.v)
#endif

#endif  /* PHP_PHPLIST_H */

在phplist.c中具體實現(xiàn)

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_phplist.h"

static int le_phplist;
static int isFree = 0;

const zend_function_entry phplist_functions[] = {
    PHP_FE(confirm_phplist_compiled,    NULL)       /* For testing, remove later. */
    PHP_FE(list_create, NULL)
    PHP_FE(list_add_head, NULL) 
    PHP_FE(list_add_tail, NULL) 
    PHP_FE(list_get_index, NULL)    
    PHP_FE(list_get_length, NULL)   
    PHP_FE(list_remove_index, NULL)
    PHP_FE(list_destroy, NULL)
    PHP_FE(list_get_head, NULL)
    PHP_FE_END  /* Must be the last line in phplist_functions[] */
};

zend_module_entry phplist_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
    STANDARD_MODULE_HEADER,
#endif
    "phplist",
    phplist_functions,
    PHP_MINIT(phplist),
    PHP_MSHUTDOWN(phplist),
    PHP_RINIT(phplist),     /* Replace with NULL if there's nothing to do at request start */
    PHP_RSHUTDOWN(phplist), /* Replace with NULL if there's nothing to do at request end */
    PHP_MINFO(phplist),
#if ZEND_MODULE_API_NO >= 20010901
    PHP_PHPLIST_VERSION,
#endif
    STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_PHPLIST
ZEND_GET_MODULE(phplist)
#endif

//定義鏈表節(jié)點和鏈表頭
typedef struct _ListNode{
    struct _ListNode *prev;
    struct _ListNode *next;
    zval *value;
}ListNode;
typedef struct _ListHead{
    struct _ListNode *head;
    struct _ListNode *tail;
    int size;
}ListHead;

//創(chuàng)建鏈表具體實現(xiàn)
ListHead * list_create(){

    ListHead *head;
    head = (ListHead *)malloc(sizeof(ListHead));
    if (head){
        head->size = 0;
        head->head = NULL;
        head->tail = NULL;
    }
    return head;
}

//向頭部添加
int list_add_head(ListHead *head, zval *value){

    ListNode *node;
    node = (ListNode *)malloc(sizeof(*node));
    if (!node){
        return 0;
    }
    node->value = value;
    node->prev = NULL;
    node->next = head->head;
    if (head->head){
        head->head->prev = node;
    }
    head->head = node;
    if(!head->tail){
        head->tail = head->head;
    }
    head->size++;
    return 1;
}

//鏈表尾添加
int list_add_tail(ListHead *list, zval *value){

    ListNode *node;
    node = (ListNode *)malloc(sizeof(*node));
    if(!node){
        return 0;
    }
    node->value = value;
    node->next = NULL;
    node->prev = list->tail;
    if (list->tail){
        list->tail->next = node;
    }
    list->tail = node;
    if (!list->head){
        list->head = list->tail;
    }
    list->size++;
    return 1;
}

//獲取指定元素
int list_get_index(ListHead *list, int index, zval **retval){

    ListNode *node;
    if(!list){
        return 0;
    }
    if (index <= 0 || list->size == 0 ||  index > list->size){
        return 0;
    }
    if (index < list->size / 2){
        node = list->head;
        while(node && index > 0){
            node = node->next;
            --index;
        }
    }else{
        node = list->tail;
        while(node && index > 0){
            node = node->prev;
            --index;
        }
    }
    *retval = node->value;
    return 1;
}

//獲取鏈表長度
int list_get_length(ListHead *list){

    if (list){
        return list->size;
    }else{
        return 0;
    }
}

//刪除節(jié)點
int list_remove_index(ListHead *list, int index){

    ListNode *node;
    if(!list){
        return 0;
    }
    if (index <= 0 || list->size == 0 || index > list->size){
        return 0;
    }
    if (index < list->size / 2){
        node = list->head;
        while(node && index > 0){
            node = node->next;
            --index;
        }
    }else{
        node = list->tail;
        while(node && index > 0){
            node = node->prev;
            --index;
        }
    }
    if (!node){
        return 0;
    }
    if (node->prev){
        node->prev->next = node->next;
    }else{
        list->head = node->next;
    }
    if(node->next){
        node->next->prev = node->prev;
    }else{
        list->tail = node->prev;
    }
    list->size--;
    return 1;
}

//釋放鏈表
void list_destroy(ListHead *list){
    
    ListNode *pre, *next;
    pre = list->head;
    while(pre){
        next = pre->next;
        free(pre);
        pre = next;
    }
    free(list);
}

int list_get_head(ListHead *list, zval **retval){

    if (!list || !list->head){
        return 0;
    }
    *retval = list->head->value;
    return 1;
}

//析構(gòu)函數(shù)實現(xiàn)
void phplist_destructor_handler(zend_rsrc_list_entry *rsrc TSRMLS_DC){
    if (!isFree){
        ListHead *list;
        list = (ListHead *)rsrc->ptr;
        list_destroy(list);
        isFree = 1;
    }
}

PHP_MINIT_FUNCTION(phplist)
{
    le_phplist = zend_register_list_destructors_ex(phplist_destructor_handler, NULL, "phplist", module_number);
    return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(phplist)
{
    return SUCCESS;
}

PHP_RINIT_FUNCTION(phplist)
{
    return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION(phplist)
{
    return SUCCESS;
}

PHP_MINFO_FUNCTION(phplist)
{
    php_info_print_table_start();
    php_info_print_table_header(2, "phplist support", "enabled");
    php_info_print_table_end();
}

PHP_FUNCTION(confirm_phplist_compiled)
{
    char *arg = NULL;
    int arg_len, len;
    char *strg;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
        return;
    }

    len = spprintf(&strg, 0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "phplist", arg);
    RETURN_STRINGL(strg, len, 0);
}

PHP_FUNCTION(list_create){

    ListHead *list;
    list = list_create();
    if (!list){
        RETURN_NULL();
    }
    ZEND_REGISTER_RESOURCE(return_value, list, le_phplist);
}

PHP_FUNCTION(list_add_head){

    zval *value;
    zval *lrc;
    ListHead *list;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rz", &lrc, &value) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);

    int ret = list_add_head(list, value);
    if(ret){
        RETURN_TRUE;
    }
    RETURN_FALSE;
}

PHP_FUNCTION(list_add_tail){

    zval *value;
    zval *lrc;
    ListHead *list;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rz", &lrc, &value) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);

    int ret = list_add_tail(list, value);
    if(ret){
        RETURN_TRUE;
    }
    RETURN_FALSE;
}

PHP_FUNCTION(list_get_index){

    zval *lrc;
    ListHead *list;
    long index;
    zval *retval;
    if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rl", &lrc, &index) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);
    int ret = list_get_index(list, index, &retval);
    if (ret){
        RETURN_ZVAL(retval, 1, 0);
    }
    RETURN_NULL();
}

PHP_FUNCTION(list_get_length){

    zval *lrc;
    ListHead *list;
    if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &lrc) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);
    RETURN_LONG(list_get_length(list));
}

PHP_FUNCTION(list_remove_index){

    zval *lrc;
    ListHead *list;
    long index;
    if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rl", &lrc, &index) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);
    int ret = list_remove_index(list, index);
    if (ret){
        RETURN_TRUE;
    }
    RETURN_FALSE;
}

PHP_FUNCTION(list_destroy){

    zval *lrc;
    ListHead *list;
    if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &lrc) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);
    list_destroy(list);
}

PHP_FUNCTION(list_get_head){

    zval *lrc;
    zval *retval;
    ListHead *list;
    if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &lrc) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);
    int ret = list_get_head(list, &retval);
    if (ret){
        RETURN_ZVAL(retval, 1, 0);
    }
    RETURN_NULL();
}

之后按照步驟在php.ini中引入擴展即可使用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容