# 15.3 實現一個包裝器
# 實現一個包裝器
為了演示包裝器和流操作的內部工作原理, 我們需要重新實現php手冊的stream\_wrapper\_register()一頁示例中的var://包裝器.
此刻, 首先從下面功能完整的變量流包裝實現開始. 構建他, 并開始檢查每一塊的工作原理.
> 譯注: 為了方便大家閱讀, 對代碼的注釋進行了適量補充調整, 此外, 由于phpapi的調整, 原著中的代碼不能直接在譯者使用的php-5.4.10中運行, 進行了適當的修改. 因此下面代碼結構可能和原著略有不同, 請參考閱讀.(下面opendir的例子也進行了相應的修改)
## config.m4
```
PHP_ARG_ENABLE(varstream,whether to enable varstream support,
[ enable-varstream Enable varstream support])
if test "$PHP_VARSTREAM" = "yes"; then
AC_DEFINE(HAVE_VARSTREAM,1,[Whether you want varstream])
PHP_NEW_EXTENSION(varstream, varstream.c, $ext_shared)
fi
```
## php\_varstream.h
```
#ifndef PHP_VARSTREAM_H
#define PHP_VARSTREAM_H
extern zend_module_entry varstream_module_entry;
#define phpext_varstream_ptr &varstream_module_entry
#ifdef PHP_WIN32
# define PHP_VARSTREAM_API __declspec(dllexport)
#elif defined(__GNUC__) && __GNUC__ >= 4
# define PHP_VARSTREAM_API __attribute__ ((visibility("default")))
#else
# define PHP_VARSTREAM_API
#endif
#ifdef ZTS
#include "TSRM.h"
#endif
PHP_MINIT_FUNCTION(varstream);
PHP_MSHUTDOWN_FUNCTION(varstream);
#define PHP_VARSTREAM_WRAPPER "var"
#define PHP_VARSTREAM_STREAMTYPE "varstream"
/* 變量流的抽象數據結構 */
typedef struct _php_varstream_data {
off_t position;
char *varname;
int varname_len;
} php_varstream_data;
#ifdef ZTS
#define VARSTREAM_G(v) TSRMG(varstream_globals_id, zend_varstream_globals *, v)
#else
#define VARSTREAM_G(v) (varstream_globals.v)
#endif
#endif
```
## varstream.c
```
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "ext/standard/url.h"
#include "php_varstream.h"
static size_t php_varstream_write(php_stream *stream,
const char *buf, size_t count TSRMLS_DC)
{
php_varstream_data *data = stream->abstract;
zval **var;
size_t newlen;
/* 查找變量 */
if (zend_hash_find(&EG(symbol_table), data->varname,
data->varname_len + 1,(void**)&var) == FAILURE) {
/* 變量不存在, 直接創建一個字符串類型的變量, 并保存新傳遞進來的內容 */
zval *newval;
MAKE_STD_ZVAL(newval);
ZVAL_STRINGL(newval, buf, count, 1);
/* 將新的zval *放到變量中 */
zend_hash_add(&EG(symbol_table), data->varname,
data->varname_len + 1, (void*)&newval,
sizeof(zval*), NULL);
return count;
}
/* 如果需要, 讓變量可寫. 這里實際上處理的是寫時復制 */
SEPARATE_ZVAL_IF_NOT_REF(var);
/* 轉換為字符串類型 */
convert_to_string_ex(var);
/* 重置偏移量(譯注: 相比于正常的文件系統, 這里的處理實際上不支持文件末尾的空洞創建, 讀者如果熟悉*nix文件系統, 應該了解譯者所說, 否則請略過) */
if (data->position > Z_STRLEN_PP(var)) {
data->position = Z_STRLEN_PP(var);
}
/* 計算新的字符串長度 */
newlen = data->position + count;
if (newlen < Z_STRLEN_PP(var)) {
/* 總長度不變 */
newlen = Z_STRLEN_PP(var);
} else if (newlen > Z_STRLEN_PP(var)) {
/* 重新調整緩沖區大小以保存新內容 */
Z_STRVAL_PP(var) =erealloc(Z_STRVAL_PP(var),newlen+1);
/* 更新字符串長度 */
Z_STRLEN_PP(var) = newlen;
/* 確保字符串NULL終止 */
Z_STRVAL_PP(var)[newlen] = 0;
}
/* 將數據寫入到變量中 */
memcpy(Z_STRVAL_PP(var) + data->position, buf, count);
data->position += count;
return count;
}
static size_t php_varstream_read(php_stream *stream,
char *buf, size_t count TSRMLS_DC)
{
php_varstream_data *data = stream->abstract;
zval **var, copyval;
int got_copied = 0;
size_t toread = count;
if (zend_hash_find(&EG(symbol_table), data->varname,
data->varname_len + 1, (void**)&var) == FAILURE) {
/* 變量不存在, 讀不到數據, 返回0字節長度 */
return 0;
}
copyval = **var;
if (Z_TYPE(copyval) != IS_STRING) {
/* 對于非字符串類型變量, 創建一個副本進行讀, 這樣對于只讀的變量, 就不會改變其原始類型 */
zval_copy_ctor(©val);
INIT_PZVAL(©val);
got_copied = 1;
}
if (data->position > Z_STRLEN(copyval)) {
data->position = Z_STRLEN(copyval);
}
if ((Z_STRLEN(copyval) - data->position) < toread) {
/* 防止讀取到變量可用緩沖區外的內容 */
toread = Z_STRLEN(copyval) - data->position;
}
/* 設置緩沖區 */
memcpy(buf, Z_STRVAL(copyval) + data->position, toread);
data->position += toread;
/* 如果創建了副本, 則釋放副本 */
if (got_copied) {
zval_dtor(©val);
}
/* 返回設置到緩沖區的字節數 */
return toread;
}
static int php_varstream_closer(php_stream *stream,
int close_handle TSRMLS_DC)
{
php_varstream_data *data = stream->abstract;
/* 釋放內部結構避免泄露 */
efree(data->varname);
efree(data);
return 0;
}
static int php_varstream_flush(php_stream *stream TSRMLS_DC)
{
php_varstream_data *data = stream->abstract;
zval **var;
/* 根據不同情況, 重置偏移量 */
if (zend_hash_find(&EG(symbol_table), data->varname,
data->varname_len + 1, (void**)&var)
== SUCCESS) {
if (Z_TYPE_PP(var) == IS_STRING) {
data->position = Z_STRLEN_PP(var);
} else {
zval copyval = **var;
zval_copy_ctor(©val);
convert_to_string(©val);
data->position = Z_STRLEN(copyval);
zval_dtor(©val);
}
} else {
data->position = 0;
}
return 0;
}
static int php_varstream_seek(php_stream *stream, off_t offset,
int whence, off_t *newoffset TSRMLS_DC)
{
php_varstream_data *data = stream->abstract;
switch (whence) {
case SEEK_SET:
data->position = offset;
break;
case SEEK_CUR:
data->position += offset;
break;
case SEEK_END:
{
zval **var;
size_t curlen = 0;
if (zend_hash_find(&EG(symbol_table),
data->varname, data->varname_len + 1,
(void**)&var) == SUCCESS) {
if (Z_TYPE_PP(var) == IS_STRING) {
curlen = Z_STRLEN_PP(var);
} else {
zval copyval = **var;
zval_copy_ctor(©val);
convert_to_string(©val);
curlen = Z_STRLEN(copyval);
zval_dtor(©val);
}
}
data->position = curlen + offset;
break;
}
}
/* 防止隨機訪問指針移動到緩沖區開始位置之前 */
if (data->position < 0) {
data->position = 0;
}
if (newoffset) {
*newoffset = data->position;
}
return 0;
}
static php_stream_ops php_varstream_ops = {
php_varstream_write,
php_varstream_read,
php_varstream_closer,
php_varstream_flush,
PHP_VARSTREAM_STREAMTYPE,
php_varstream_seek,
NULL, /* cast */
NULL, /* stat */
NULL, /* set_option */
};
/* Define the wrapper operations */
static php_stream *php_varstream_opener(
php_stream_wrapper *wrapper,
char *filename, char *mode, int options,
char **opened_path, php_stream_context *context
STREAMS_DC TSRMLS_DC)
{
php_varstream_data *data;
php_url *url;
if (options & STREAM_OPEN_PERSISTENT) {
/* 按照變量流的定義, 是不能持久化的
* 因為變量在請求結束后將被釋放
*/
php_stream_wrapper_log_error(wrapper, options
TSRMLS_CC, "Unable to open %s persistently",
filename);
return NULL;
}
/* 標準URL解析: scheme://user:pass@host:port/path?query#fragment */
url = php_url_parse(filename);
if (!url) {
php_stream_wrapper_log_error(wrapper, options
TSRMLS_CC, "Unexpected error parsing URL");
return NULL;
}
/* 檢查是否有變量流URL必須的元素host, 以及scheme是否是var */
if (!url->host || (url->host[0] == 0) ||
strcasecmp("var", url->scheme) != 0) {
/* Bad URL or wrong wrapper */
php_stream_wrapper_log_error(wrapper, options
TSRMLS_CC, "Invalid URL, must be in the form: "
"var://variablename");
php_url_free(url);
return NULL;
}
/* 創建一個數據結構保存協議信息(變量流協議重要是變量名, 變量名長度, 當前偏移量) */
data = emalloc(sizeof(php_varstream_data));
data->position = 0;
data->varname_len = strlen(url->host);
data->varname = estrndup(url->host, data->varname_len + 1);
/* 釋放前面解析出來的url占用的內存 */
php_url_free(url);
/* 實例化一個流, 為其賦予恰當的流ops, 綁定抽象數據 */
return php_stream_alloc(&php_varstream_ops, data, 0, mode);
}
static php_stream_wrapper_ops php_varstream_wrapper_ops = {
php_varstream_opener, /* 調用php_stream_open_wrapper(sprintf("%s://xxx", PHP_VARSTREAM_WRAPPER))時執行 */
NULL, /* stream_close */
NULL, /* stream_stat */
NULL, /* url_stat */
NULL, /* dir_opener */
PHP_VARSTREAM_WRAPPER,
NULL, /* unlink */
#if PHP_MAJOR_VERSION >= 5
/* PHP >= 5.0 only */
NULL, /* rename */
NULL, /* mkdir */
NULL, /* rmdir */
#endif
};
static php_stream_wrapper php_varstream_wrapper = {
&php_varstream_wrapper_ops,
NULL, /* abstract */
0, /* is_url */
};
PHP_MINIT_FUNCTION(varstream)
{
/* 注冊流包裝器:
* 1. 檢查流包裝器名字是否正確(符合這個正則: /^[a-zA-Z0-9+.-]+$/)
* 2. 將傳入的php_varstream_wrapper增加到url_stream_wrappers_hash這個HashTable中, key為PHP_VARSTREAM_WRAPPER
*/
if (php_register_url_stream_wrapper(PHP_VARSTREAM_WRAPPER,
&php_varstream_wrapper TSRMLS_CC)==FAILURE) {
return FAILURE;
}
return SUCCESS;
}
PHP_MSHUTDOWN_FUNCTION(varstream)
{
/* 卸載流包裝器: 從url_stream_wrappers_hash中刪除 */
if (php_unregister_url_stream_wrapper(PHP_VARSTREAM_WRAPPER
TSRMLS_CC) == FAILURE) {
return FAILURE;
}
return SUCCESS;
}
zend_module_entry varstream_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
"varstream",
NULL,
PHP_MINIT(varstream),
PHP_MSHUTDOWN(varstream),
NULL,
NULL,
NULL,
#if ZEND_MODULE_API_NO >= 20010901
"0.1",
#endif
STANDARD_MODULE_PROPERTIES
};
#ifdef COMPILE_DL_VARSTREAM
ZEND_GET_MODULE(varstream)
#endif
```
在構建加載擴展后, php就可以處理以var://開始的URL的請求, 它的行為和手冊中用戶空間實現的行為一致.
## 內部實現
首先你注意到的可能是這個擴展完全沒有暴露用戶空間函數. 它所做的只是在MINIT函數中調用了一個核心PHPAPI的鉤子, 將var協議和我們定義的包裝器關聯起來:
```
static php_stream_wrapper php_varstream_wrapper = {
&php_varstream_wrapper_ops,
NULL, /* abstract */
0, /* is_url */
}
```
很明顯, 最重要的元素就是ops, 它提供了訪問特定流包裝器的創建以及檢查函數. 你可以安全的忽略abstract屬性, 它僅在運行時使用, 在初始化定義時, 它只是作為一個占位符. 第三個元素is\_url, 它告訴php在使用這個包裝器時是否考慮php.ini中的allow\_url\_fopen選項. 如果這個值非0, 并且將allow\_url\_fopen設置為false, 則這個包裝器不能被腳本使用.
在本章前面你已經知道, 調用用戶空間函數比如fopen將通過這個包裝器的ops元素得到php\_varstream\_wrapper\_ops, 這樣去調用流的打開函數php\_varstream\_opener.
這個函數的第一塊代碼檢查是否請求持久化的流:
```
if (options & STREAM_OPEN_PERSISTENT) {
```
對于很多包裝器這樣的請求是合法的. 然而目前的情況這個行為沒有意義. 一方面用戶空間變量的定義就是臨時的, 另一方面, varstream的實例化代價很低, 這就使得持久化的優勢很小.
像流包裝層報告錯誤很簡單, 只需要返回一個NULL值而不是流實例即可. 流包裝層透出到用戶空間的失敗消息并不會說明具體的錯誤, 只是說明不能打開URL. 要想給開發者暴露更多的錯誤信息, 可以在返回之前使用php\_stream\_wrapper\_log\_error()函數.
```
php_stream_wrapper_log_error(wrapper, options
TSRMLS_CC, "Unable to open %s persistently",
filename);
return NULL;
```
## URL解析
實例化varstream的下一步需要一個人類可讀的URL, 將它分塊放入到一個易管理的結構體中. 幸運的是它使用了和用戶空間url\_parse()函數相同的機制. 如果URL成功解析, 將會分配一個php\_url結構體并設置合適的值. 如果在URL中沒有某些值, 在返回的php\_url中對應的將被設置為NULL. 這個結構體必須在離開php\_varstream\_opener函數之前被顯式釋放, 否則它的內存將會泄露:
```
typedef struct php_url {
/* scheme://user:pass@host:port/path?query#fragment */
char *scheme;
char *user;
char *pass;
char *host;
unsigned short port;
char *path;
char *query;
char *fragment;
} php_url;
```
最后, varstream包裝器創建了一個數據結構, 保存了流指向的變量名, 讀取時的當前位置. 這個結構體將在流的讀取和寫入函數中用于獲取變量, 并且將在流結束使用時由php\_varstream\_close函數釋放.
## opendir()
讀寫變量內容的實現可以再次進行擴展. 這里可以加入一個新的特性, 允許使用目錄函數讀取數組中的key. 在你的php\_varstream\_wrapper\_ops結構體之前增加下面的代碼:
```
static size_t php_varstream_readdir(php_stream *stream,
char *buf, size_t count TSRMLS_DC)
{
php_stream_dirent *ent = (php_stream_dirent*)buf;
php_varstream_dirdata *data = stream->abstract;
char *key;
int type, key_len;
long idx;
/* 查找數組中的key */
type = zend_hash_get_current_key_ex(Z_ARRVAL_P(data->arr),
&key, &key_len, &idx, 0, &(data->pos));
/* 字符串key */
if (type == HASH_KEY_IS_STRING) {
if (key_len >= sizeof(ent->d_name)) {
/* truncate long keys to maximum length */
key_len = sizeof(ent->d_name) - 1;
}
/* 設置到目錄結構上 */
memcpy(ent->d_name, key, key_len);
ent->d_name[key_len] = 0;
/* 數值key */
} else if (type == HASH_KEY_IS_LONG) {
/* 設置到目錄結構上 */
snprintf(ent->d_name, sizeof(ent->d_name), "%ld",idx);
} else {
/* 迭代結束 */
return 0;
}
/* 移動數組指針(位置記錄到流的抽象結構中) */
zend_hash_move_forward_ex(Z_ARRVAL_P(data->arr),
&data->pos);
return sizeof(php_stream_dirent);
}
static int php_varstream_closedir(php_stream *stream,
int close_handle TSRMLS_DC)
{
php_varstream_dirdata *data = stream->abstract;
zval_ptr_dtor(&(data->arr));
efree(data);
return 0;
}
static int php_varstream_dirseek(php_stream *stream,
off_t offset, int whence,
off_t *newoffset TSRMLS_DC)
{
php_varstream_dirdata *data = stream->abstract;
if (whence == SEEK_SET && offset == 0) {
/* 重置數組指針 */
zend_hash_internal_pointer_reset_ex(
Z_ARRVAL_P(data->arr), &(data->pos));
if (newoffset) {
*newoffset = 0;
}
return 0;
}
/* 不支持其他類型的隨機訪問 */
return -1;
}
static php_stream_ops php_varstream_dirops = {
NULL, /* write */
php_varstream_readdir,
php_varstream_closedir,
NULL, /* flush */
PHP_VARSTREAM_DIRSTREAMTYPE,
php_varstream_dirseek,
NULL, /* cast */
NULL, /* stat */
NULL, /* set_option */
};
static php_stream *php_varstream_opendir(
php_stream_wrapper *wrapper,
char *filename, char *mode, int options,
char **opened_path, php_stream_context *context
STREAMS_DC TSRMLS_DC)
{
php_varstream_dirdata *data;
php_url *url;
zval **var;
/* 不支持持久化流 */
if (options & STREAM_OPEN_PERSISTENT) {
php_stream_wrapper_log_error(wrapper, options
TSRMLS_CC, "Unable to open %s persistently",
filename);
return NULL;
}
/* 解析URL */
url = php_url_parse(filename);
if (!url) {
php_stream_wrapper_log_error(wrapper, options
TSRMLS_CC, "Unexpected error parsing URL");
return NULL;
}
/* 檢查請求URL的正確性 */
if (!url->host || (url->host[0] == 0) ||
strcasecmp("var", url->scheme) != 0) {
/* Bad URL or wrong wrapper */
php_stream_wrapper_log_error(wrapper, options
TSRMLS_CC, "Invalid URL, must be in the form: "
"var://variablename");
php_url_free(url);
return NULL;
}
/* 查找變量 */
if (zend_hash_find(&EG(symbol_table), url->host,
strlen(url->host) + 1, (void**)&var) == FAILURE) {
php_stream_wrapper_log_error(wrapper, options
TSRMLS_CC, "Variable $%s not found", url->host);
php_url_free(url);
return NULL;
}
/* 檢查變量類型 */
if (Z_TYPE_PP(var) != IS_ARRAY) {
php_stream_wrapper_log_error(wrapper, options
TSRMLS_CC, "$%s is not an array", url->host);
php_url_free(url);
return NULL;
}
/* 釋放前面分配的URL結構 */
php_url_free(url);
/* 分配抽象數據結構 */
data = emalloc(sizeof(php_varstream_dirdata));
if ( Z_ISREF_PP(var) && Z_REFCOUNT_PP(var) > 1) {
/* 全拷貝 */
MAKE_STD_ZVAL(data->arr);
*(data->arr) = **var;
zval_copy_ctor(data->arr);
INIT_PZVAL(data->arr);
} else {
/* 寫時拷貝 */
data->arr = *var;
Z_SET_REFCOUNT_P(data->arr, Z_REFCOUNT_P(data->arr) + 1);
}
/* 重置數組指針 */
zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(data->arr),
&data->pos);
return php_stream_alloc(&php_varstream_dirops,data,0,mode);
}
```
現在, 將你的php\_varstream\_wrapper\_ops結構體中的dir\_opener的NULL替換成你的php\_varstream\_opendir函數. 最后, 將下面新定義的類型放入到你的php\_varstream.h文件的php\_varstream\_data定義下面:
```
#define PHP_VARSTREAM_DIRSTREAMTYPE "varstream directory"
typedef struct _php_varstream_dirdata {
zval *arr;
HashPosition pos;
} php_varstream_dirdata;
```
在你基于fopen()實現的varstream包裝器中, 你直接使用持久變量名, 每次執行讀寫操作時從符號表中獲取變量. 而這里, opendir()的實現中獲取變量時處理了變量不存在或者類型錯誤的異常. 你還有一個數組變量的拷貝, 這就說明原數組的改變并不會影響后續的readdir()調用的結果. 原來存儲變量名的方式也可以正常工作, 這里只是給出另外一種選擇作為演示示例.
由于目錄訪問是基于成塊的目錄條目, 而不是字符, 因此這里需要一套獨立的流操作. 這個版本中, write沒有意義, 因此保持它為NULL. read的實現使用zend\_hash\_get\_current\_key\_ex()函數將數組映射到目錄名. 而隨機訪問也只是對SEEK\_SET有效, 用來響應rewinddir()跳轉到數組開始位置.
> 實際上, 目錄流并沒有使用SEEK\_CUR, SEEK\_END, 或者除了0之外的偏移量. 在實現目錄流操作時, 最好還是涉及你的函數能以某種方式處理這些情況, 以使得在流包裝層變化時能夠適應其目錄隨機訪問.
## links
- [目錄](preface.md)
- 15.2 [包裝器操作](15.2.html)
- 15.4 [操縱](15.4.html)
- 介紹
- 1 PHP的生命周期
- 1.1 讓我們從SAPI開始
- 1.2 PHP的啟動與終止
- 1.3 PHP的生命周期
- 1.4 線程安全
- 1.5 PHP的生命周期
- 2 PHP變量在內核中的實現
- 2.1 變量的類型
- 2.2 變量的值
- 2.3 創建PHP變量
- 2.4 變量的存儲方式
- 2.5 變量的檢索
- 2.6 類型轉換
- 2.7 小結
- 3 內存管理
- 3.1 內存管理
- 3.2 引用計數
- 3.3 內存管理
- 4 動手編譯PHP
- 4.1 動手編譯PHP
- 4.2 動手編譯PHP
- 4.3 Unix/Linux平臺下的編譯
- 4.4 在Win32平臺上編譯PHP
- 4.5 動手編譯PHP
- 5 Your First Extension
- 5.1 Your First Extension
- 5.2 編譯我們的擴展
- 5.3 靜態編譯
- 5.4 編寫函數
- 5.5 Your First Extension
- 6 函數返回值
- 6.1 函數返回值
- 6.2 引用與函數的執行結果
- 6.3 函數返回值
- 7 函數的參數
- 7.1 函數的參數
- 7.2 函數的參數
- 7.3 函數的參數
- 8 使用HashTable與{數組}
- 8.1 使用HashTable與{數組}
- 8.2 使用HashTable與{數組}
- 8.3 使用HashTable與{數組}
- 8.4 使用HashTable與{數組}
- 9 PHP中的資源類型
- 9.1 PHP中的資源類型
- 9.2 PHP中的資源類型
- 9.3 PHP中的資源類型
- 9.4 PHP中的資源類型
- 10 PHP中的面向對象(一)
- 10.1 PHP中的面向對象(一)
- 10.2 PHP中的面向對象(一)
- 10.3 PHP中的面向對象(一)
- 10.4 PHP中的面向對象(一)
- 10.5 PHP中的面向對象(一)
- 11 PHP中的面向對象(二)
- 11.1 PHP中的面向對象(二)
- 11.2 PHP中的面向對象(二)
- 11.3 PHP中的面向對象(二)
- 12 啟動與終止的那點事
- 12.1 關于生命周期
- 12.2 MINFO與phpinfo
- 12.3 常量
- 12.4 PHP擴展中的全局變量
- 12.5 PHP語言中的超級全局變量(Superglobals)
- 12.6 小結
- 13 INI設置
- 13.1 聲明和訪問INI設置
- 13.2 小結
- 14 流式訪問
- 14.1 流的概覽
- 14.2 訪問流
- 14.3 靜態資源操作
- 14.4 links
- 15 流的實現
- 15.1 php流的表象之下
- 15.2 包裝器操作
- 15.3 實現一個包裝器
- 15.4 操縱
- 15.5 檢查
- 15.6 小結
- 16 有趣的流
- 16.1 上下文
- 16.2 過濾器
- 16.3 小結
- 17 配置和鏈接
- 17.1 autoconf
- 17.2 庫的查找
- 17.3 強制模塊依賴
- 17.4 Windows方言
- 17.5 小結
- 18 擴展生成
- 18.1 ext_skel
- 18.2 PECL_Gen
- 18.3 小結
- 19 設置宿主環境
- 19.1 嵌入式SAPI
- 19.2 構建并編譯一個宿主應用
- 19.3 通過嵌入包裝重新創建cli
- 19.4 老技術新用
- 19.5 小結
- 20 高級嵌入式
- 20.1 回調到php中
- 20.2 錯誤處理
- 20.3 初始化php
- 20.4 覆寫INI_SYSTEM和INI_PERDIR選項
- 20.5 捕獲輸出
- 20.6 同時擴展和嵌入
- 20.7 小結