ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 实现一个包装器 为了演示包装器和流操作的内部工作原理, 我们需要重新实现php手册的stream_wrapper_register()一页示例中的var://包装器. 此刻, 首先从下面功能完整的变量流包装实现开始. 构建他, 并开始检查每一块的工作原理. > 译注: 为了方便大家阅读, 对代码的注释进行了适量补充调整, 此外, 由于phpapi的调整, 原著中的代码不能直接在译者使用的php-5.4.10中运行, 进行了适当的修改. 因此下面代码结构可能和原著略有不同, 请参考阅读.(下面opendir的例子也进行了相应的修改) ## config.m4 ```c 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 ```c #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 ```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(&copyval); INIT_PZVAL(&copyval); 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(&copyval); } /* 返回设置到缓冲区的字节数 */ 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(&copyval); convert_to_string(&copyval); data->position = Z_STRLEN(copyval); zval_dtor(&copyval); } } 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(&copyval); convert_to_string(&copyval); curlen = Z_STRLEN(copyval); zval_dtor(&copyval); } } 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协议和我们定义的包装器关联起来: ```c 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. 这个函数的第一块代码检查是否请求持久化的流: ```c if (options & STREAM_OPEN_PERSISTENT) { ``` 对于很多包装器这样的请求是合法的. 然而目前的情况这个行为没有意义. 一方面用户空间变量的定义就是临时的, 另一方面, varstream的实例化代价很低, 这就使得持久化的优势很小. 像流包装层报告错误很简单, 只需要返回一个NULL值而不是流实例即可. 流包装层透出到用户空间的失败消息并不会说明具体的错误, 只是说明不能打开URL. 要想给开发者暴露更多的错误信息, 可以在返回之前使用php_stream_wrapper_log_error()函数. ```c 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函数之前被显式释放, 否则它的内存将会泄露: ```c 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结构体之前增加下面的代码: ```c 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定义下面: ```c #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.md>) * 15.4 [操纵](<15.4.md>)