Implementing a Wrapper
To illustrate the internal workings of wrappers and stream operations, you'll be reimplementing the var:// wrapper described in the PHP manual's stream_wrapper_register() page.
This time, start with the following, fully functional, variable stream wrapper implementation. Once built, you can start examining the workings of each individual piece (see Listings 14.1, 14.2, and 14.3).
Listing 14.1. 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 |
Listing 14.2. php_varstream.h
#ifdef HAVE_CONFIG_H #include "config.h" #endif #include "php.h" #define PHP_VARSTREAM_EXTNAME "varstream" #define PHP_VARSTREAM_EXTVER "1.0" /* Will be registered as var:// */ #define PHP_VARSTREAM_WRAPPER "var" #define PHP_VARSTREAM_STREAMTYPE "varstream" extern zend_module_entry varstream_module_entry; #define phpext_varstream_ptr &varstream_module_entry typedef struct _php_varstream_data { off_t position; char *varname; int varname_len; } php_varstream_data; |
Listing 14.3. varstream.c
#include "php_varstream.h" #include "ext/standard/url.h" /* Define the stream operations */ 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; /* Fetch variable */ if (zend_hash_find(&EG(symbol_table), data->varname, data->varname_len + 1,(void**)&var) == FAILURE) { /* $var doesn't exist, * Simply create it as a string * holding the new contents */ zval *newval; MAKE_STD_ZVAL(newval); ZVAL_STRINGL(newval, buf, count, 1); /* Store new zval* in $var */ zend_hash_add(&EG(symbol_table), data->varname, data->varname_len + 1, (void*)&newval, sizeof(zval*), NULL); return count; } /* Make the variable writable if necessary */ SEPARATE_ZVAL_IF_NOT_REF(var); convert_to_string_ex(var); if (data->position > Z_STRLEN_PP(var)) { data->position = Z_STRLEN_PP(var); } newlen = data->position + count; if (newlen < Z_STRLEN_PP(var)) { /* Total length stays the same */ newlen = Z_STRLEN_PP(var); } else if (newlen > Z_STRLEN_PP(var)) { /* Resize the buffer to hold new contents */ Z_STRVAL_PP(var) =erealloc(Z_STRVAL_PP(var),newlen+1); /* Update string length */ Z_STRLEN_PP(var) = newlen; /* Make sure string winds up NULL terminated */ Z_STRVAL_PP(var)[newlen] = 0; } /* Write new data into $var */ 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) { /* The variable doesn't exist * so there's nothing to read, * "return" zero bytes */ return 0; } copyval = **var; if (Z_TYPE(copyval) != IS_STRING) { /* Turn non-string type into sensible value */ 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) { /* Don't overrun the available buffer */ toread = Z_STRLEN(copyval) - data->position; } /* Populate buffer */ memcpy(buf, Z_STRVAL(copyval) + data->position, toread); data->position += toread; /* Free temporary zval if necessary */ if (got_copied) { zval_dtor(©val); } /* Return number of bytes populated into buf */ return toread; } static int php_varstream_closer(php_stream *stream, int close_handle TSRMLS_DC) { php_varstream_data *data = stream->abstract; /* Free the internal state structure to avoid leaking */ 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; } } /* Prevent seeking prior to the start */ 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) { /* variable streams, by definition, can't be persistent * Since their variable disapears * at the end of a request */ php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, "Unable to open %s persistently", filename); return NULL; } url = php_url_parse(filename); if (!url) { php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, "Unexpected error parsing URL"); return NULL; } 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; } /* Create data struct for protocol information */ data = emalloc(sizeof(php_varstream_data)); data->position = 0; data->varname_len = strlen(url->host); data->varname = estrndup(url->host, data->varname_len + 1); php_url_free(url); /* Instantiate a stream, * assign the appropriate stream ops, * and bind the abstract data */ return php_stream_alloc(&php_varstream_ops, data, 0, mode); } static php_stream_wrapper_ops php_varstream_wrapper_ops = { php_varstream_opener, /* stream_opener */ 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) { /* Register the stream wrapper */ if (php_register_url_stream_wrapper(PHP_VARSTREAM_WRAPPER, &php_varstream_wrapper TSRMLS_CC)==FAILURE) { return FAILURE; } return SUCCESS; } PHP_MSHUTDOWN_FUNCTION(varstream) { /* Unregister the stream wrapper */ if (php_unregister_url_stream_wrapper(PHP_VARSTREAM_WRAPPER TSRMLS_CC) == FAILURE) { return FAILURE; } return SUCCESS; } /* Declare the module */ zend_module_entry varstream_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif PHP_VARSTREAM_EXTNAME, NULL, /* functions */ PHP_MINIT(varstream), PHP_MSHUTDOWN(varstream), NULL, /* RINIT */ NULL, /* RSHUTDOWN */ NULL, /* MINFO */ #if ZEND_MODULE_API_NO >= 20010901 PHP_VARSTREAM_EXTVER, #endif STANDARD_MODULE_PROPERTIES }; /* Export the shared symbol */ #ifdef COMPILE_DL_VARSTREAM ZEND_GET_MODULE(varstream) #endif |
After building and loading the extension, PHP will be aware of, and ready to dispatch stream requests for, URLs beginning with var:// mimicking all the behavior found in the matching userspace implementation.
Inside the Implementation
The first thing you'll notice about this extension is that it exports absolutely no userspace functions whatsoever. What is does do is call into a core PHPAPI hook from its MINIT method to associate a scheme namevar in this casewith a short and simple wrapper definition structure.
static php_stream_wrapper php_varstream_wrapper = { &php_varstream_wrapper_ops, NULL, /* abstract */ 0, /* is_url */ }
The most important element here is, obviously, the ops element, which provides access to the wrapper-specific stream creation and inspection functions. You can safely ignore the abstract property as it's only used during runtime and exists in the initial declaration as simply a placeholder. The third element, is_url, tells PHP whether or not the allow_url_fopen option in the php.ini should be considered when using this wrapper. If this value is nonzero and allow_url_fopen is set to false, this wrapper will be unavailable to running scripts.
As you already know from earlier in this chapter, calls to userspace functions such as fopen() will follow this wrapper through its ops element to php_varstream_wrapper_ops, where it can call the stream opener function, php_varstream_opener.
The first block of code used by this method checks to see whether a persistent stream has been requested:
if (options & STREAM_OPEN_PERSISTENT) {
For many wrappers such a request is perfectly valid; however, in this case such behavior simply doesn't make sense. Userspace variables are ephemeral by definition and the relative cheapness of instantiating a varstream makes the advantages of using persistency negligible.
Reporting failure to the streams layer requires nothing more than returning a NULL value from the method rather than a stream instance. As the failure bubbles its way up to userspace, the streams layer will generate a nondescript failure message saying that it was unable to open the URL. To give the developer more detailed information, you'd use the php_stream_wrapper_log_error() function prior to returning:
php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, "Unable to open %s persistently", filename); return NULL;
URL Parsing
The next step in instantiating varstream requires taking the human readable URL, and chunking it up into manageable pieces. Fortunately, the same mechanism used by the userspace url_parse() function is available as an internal API call. If the URL can be successfully parsed, a php_url structure will be allocated and populated with the appropriate values. If a particular value is not present in the URL, its value will be set to NULL. This structure must be explicitly freed before leaving the php_varstream_opener function, or its memory will be leaked.
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;
Finally, the varstream wrapper creates a data structure to hold the name of the variable being streamed, and its current locationfor read streams. This structure will be used by the stream's read and write functions to locate the variable to act upon, and will be freed during stream shutdown by the php_varstream_close method.
opendir()
This example could be extended beyond the basic implementation of reading and writing variable contents. One new feature might be to allow the use of the directory functions to read through the keys in an array. Add the following code prior to your existing php_varstream_wrapper_ops structure:
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; type = zend_hash_get_current_key_ex(Z_ARRVAL_P(data->arr), &key, &key_len, &idx, 0, &(data->pos)); 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; } else if (type == HASH_KEY_IS_LONG) { snprintf(ent->d_name, sizeof(ent->d_name), "%ld",idx); } else { /* No more keys */ 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) { /* rewinddir() */ zend_hash_internal_pointer_reset_ex( Z_ARRVAL_P(data->arr), &(data->pos)); if (newoffset) { *newoffset = 0; } return 0; } /* Other types of seeking not supported */ 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 = php_url_parse(filename); if (!url) { php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, "Unexpected error parsing URL"); return NULL; } 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; } php_url_free(url); data = emalloc(sizeof(php_varstream_dirdata)); if ((*var)->is_ref && (*var)->refcount > 1) { /* Make a full copy */ MAKE_STD_ZVAL(data->arr); *(data->arr) = **var; zval_copy_ctor(data->arr); INIT_PZVAL(data->arr); } else { /* Put in copy-on-write set */ data->arr = *var; ZVAL_ADDREF(data->arr); } zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(data->arr), &data->pos); return php_stream_alloc(&php_varstream_dirops,data,0,mode); }
Now, replace the NULL entry in your php_varstream_wrapper_ops structure for dir_opener with a reference to your php_varstream_opendir method. Lastly, add the new defines and types used in this code block to your php_varstream.h file following the definition of php_varstream_data:
#define PHP_VARSTREAM_DIRSTREAMTYPE "varstream directory" typedef struct _php_varstream_dirdata { zval *arr; HashPosition pos; } php_varstream_dirdata;
In the fopen()-based implementation of your varstream wrapper, you simply referenced the name of the variable and fetched it from the symbol table each time a read or write operation was performed. This time, you fetched the variable during the opendir() implementation allowing errors such as the variable not existing or being of the wrong type to be handled immediately. You also made a point-in-time copy of the array variable, meaning that any changes to the original array will not change the results of subsequent readdir() calls. The original approachstoring the variable namewould have worked just as well; this alternative is simply provided for illustration.
Because directory access is based on blocksdirectory entriesrather than characters, a separate set of stream operations is necessary. For this version, write has no meaning so you're able to simply leave it as NULL. read is implemented as a method that uses the zend_hash_get_current_key_ex() method to map the array indices to directory names. And seek focuses on the SEEK_SET whence to jump to the start of the array in response to calls to rewinddir().
Note
In practice, directory streams never use SEEK_SET, SEEK_END, or an offset other than 0. When implementing directory stream operations, however, it's best to design your method with some way to handle these cases should the streams layer ever change to accommodate the notion of true directory seeking.