The type of complex data structures that are usually stored in resource variables often require a fair amount of memory allocation, CPU time, or network communication to initialize. In cases where a script is very likely to need to reestablish these kind of resources on each invocation such as database links, it becomes useful to preserve the resource between requests.
Memory Allocation
From your exposure to earlier chapters you know that emalloc() and friends are the preferred set of functions to use when allocating memory within PHP because they are capable of garbage collectionshould a script have to abruptly exitin ways that system malloc() functions simply aren't. If a persistent resource is to stick around between requests, however, such garbage collection is obviously not a good thing.
Imagine for a moment that it became necessary to store the name of the opened file along with the FILE* pointer. Now, you'd need to create a custom struct in php_sample.h to hold this combination of information:
typedef struct _php_sample_descriptor_data { char *filename; FILE *fp; } php_sample_descriptor_data;
And all the functions in sample.c dealing with your file resource would need to be modified:
static void php_sample_descriptor_dtor( zend_rsrc_list_entry *rsrc TSRMLS_DC) { php_sample_descriptor_data *fdata = (php_sample_descriptor_data*)rsrc->ptr; fclose(fdata->fp); efree(fdata->filename); efree(fdata); } PHP_FUNCTION(sample_fopen) { php_sample_descriptor_data *fdata; FILE *fp; char *filename, *mode; int filename_len, mode_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &filename, &filename_len, &mode, &mode_len) == FAILURE) { RETURN_NULL(); } if (!filename_len || !mode_len) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid filename or mode length"); RETURN_FALSE; } fp = fopen(filename, mode); if (!fp) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to open %s using mode %s", filename, mode); RETURN_FALSE; } fdata = emalloc(sizeof(php_sample_descriptor_data)); fdata->fp = fp; fdata->filename = estrndup(filename, filename_len); ZEND_REGISTER_RESOURCE(return_value, fdata, le_sample_descriptor); } PHP_FUNCTION(sample_fwrite) { php_sample_descriptor_data *fdata; zval *file_resource; char *data; int data_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs", &file_resource, &data, &data_len) == FAILURE ) { RETURN_NULL(); } ZEND_FETCH_RESOURCE(fdata, php_sample_descriptor_data*, &file_resource, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor); RETURN_LONG(fwrite(data, 1, data_len, fdata->fp)); }
Note
Technically, sample_fclose() can be left as-is because it doesn't actually deal with the resource data directly. If you're feeling confident, try updating it to use the corrections yourself.
So far, everything is perfectly happy because you're still only registering non-persistent descriptor resources. You could even add a new function at this point to retrieve the original name of the file back out of the resource:
PHP_FUNCTION(sample_fname) { php_sample_descriptor_data *fdata; zval *file_resource; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &file_resource) == FAILURE ) { RETURN_NULL(); } ZEND_FETCH_RESOURCE(fdata, php_sample_descriptor_data*, &file_resource, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor); RETURN_STRING(fdata->filename, 1); }
However, soon problems will start to arise with usages such as this as you start to register persistent versions of your descriptor resource.
Delayed Destruction
As you've seen with non-persistent resources, once all the variables holding a resource ID have been unset() or have fallen out of scope, they are removed from EG(regular_list), which is the HashTable containing all per-request registered resources.
Persistent resources, as you'll see later this chapter, are also stored in a second HashTable: EG(persistent_list). Unlike EG(regular_list), the indexes used by this table are associative, and the elements are not automatically removed from the HashTable at the end of a request. Entries in EG(persistent_list) are only removed through manual calls to zend_hash_del()which you'll see shortlyor when a thread or process completely shuts down (usually when the web server is stopped).
Like the EG(regular_list) HashTable, the EG(persistent_list) HashTable also has its own dtor method. Like the regular list, this method is also a simple wrapper that uses the resource's type to look up a proper destruction method. This time, it takes the destruction method from the second parameter to zend_register_list_destructors_ex(), rather than the first.
In practice, persistent and non-persistent resources are typically registered as two distinct types to avoid having non-persistent destruction code run against a resource that is supposed to be persistent. Depending on your implementation, you may choose to combine non-persistent and persistent destruction methods in a single type. For now, add another static int to the top of sample.c for a new persistent descriptor resource:
static int le_sample_descriptor_persist;
Then extend your MINIT function with a resource registration that uses a new dtor function aimed specifically at persistently allocated structures:
static void php_sample_descriptor_dtor_persistent( zend_rsrc_list_entry *rsrc TSRMLS_DC) { php_sample_descriptor_data *fdata = (php_sample_descriptor_data*)rsrc->ptr; fclose(fdata->fp); pefree(fdata->filename, 1); pefree(fdata, 1); } PHP_MINIT_FUNCTION(sample) { le_sample_descriptor = zend_register_list_destructors_ex( php_sample_descriptor_dtor, NULL, PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number); le_sample_descriptor_persist = zend_register_list_destructors_ex( NULL, php_sample_descriptor_dtor_persistent, PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number); return SUCCESS; }
By giving these two resource types the same name, their distinction will be transparent to the end user. Internally, only one will have php_sample_descriptor_dtor called on it during request cleanup; the other, as you'll see in a moment, will stick around for up to as long as the web server's process or thread does.
Long Term Registration
Now that a suitable cleanup method is in place, it's time to actually create some usable resource structures. Often this is done using two separate functions that map internally to the same implementation, but since that would only complicate an already muddy topic, you'll accomplish the same feat here by simply accepting a Boolean parameter to sample_fopen():
PHP_FUNCTION(sample_fopen) { php_sample_descriptor_data *fdata; FILE *fp; char *filename, *mode; int filename_len, mode_len; zend_bool persist = 0; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"ss|b", &filename, &filename_len, &mode, &mode_len, &persist) == FAILURE) { RETURN_NULL(); } if (!filename_len || !mode_len) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid filename or mode length"); RETURN_FALSE; } fp = fopen(filename, mode); if (!fp) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to open %s using mode %s", filename, mode); RETURN_FALSE; } if (!persist) { fdata = emalloc(sizeof(php_sample_descriptor_data)); fdata->filename = estrndup(filename, filename_len); fdata->fp = fp; ZEND_REGISTER_RESOURCE(return_value, fdata, le_sample_descriptor); } else { list_entry le; char *hash_key; int hash_key_len; fdata =pemalloc(sizeof(php_sample_descriptor_data),1); fdata->filename = pemalloc(filename_len + 1, 1); memcpy(data->filename, filename, filename_len + 1); fdata->fp = fp; ZEND_REGISTER_RESOURCE(return_value, fdata, le_sample_descriptor_persist); /* Store a copy in the persistent_list */ le.type = le_sample_descriptor_persist; le.ptr = fdata; hash_key_len = spprintf(&hash_key, 0, "sample_descriptor:%s:%s", filename, mode); zend_hash_update(&EG(persistent_list), hash_key, hash_key_len + 1, (void*)&le, sizeof(list_entry), NULL); efree(hash_key); } }
The core portions of this function should be very familiar by now. A file was opened, it's name stored into newly allocated memory, and it was registered into a request-specific resource ID populated into return_value. What's new this time is the second portion, but hopefully it's not altogether alien.
Here, you've actually done something very similar to what ZEND_RESOURCE_REGISTER() does; however, instead of giving it a numeric index and placing it in the per-request list, you've assigned it an associative key that can be reproduced in a later request and stowed into the persistent list, which isn't automatically purged at the end of every script.
When one of these persistent descriptor resources goes out of scope, EG(regular_list)'s dtor function will check the registered list destructors for le_sample_descriptor_persist and, seeing that it's NULL, simply do nothing. This leaves the FILE* pointer and the char* name string safe for the next request.
When the resource is finally removed from EG(persistent_list), either because the thread/process is shutting down or because your extension has deliberately removed it, the engine will now go looking for a persistent destructor. Because you defined one for this resource type, it will be called and issue the appropriate pefree()s to match the earlier pemallocs().
Reuse
Putting a copy of a resource entry into the persistent_list would serve no purpose beyond extending the time that such resources can tie up memory and file locks unless you're somehow able to reuse them on subsequent requests.
Here's where that hash_key comes in. When sample_fopen() is called, either for persistent or non-persistent use, your function can re-create the hash_key using the requested filename and mode and try to find it in the persistent_list before going to the trouble of opening the file again:
PHP_FUNCTION(sample_fopen) { php_sample_descriptor_data *fdata; FILE *fp; char *filename, *mode, *hash_key; int filename_len, mode_len, hash_key_len; zend_bool persist = 0; list_entry *existing_file; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"ss|b", &filename, &filename_len, &mode, &mode_len, &persist) == FAILURE) { RETURN_NULL(); } if (!filename_len || !mode_len) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid filename or mode length"); RETURN_FALSE; } /* Try to find an already opened file */ hash_key_len = spprintf(&hash_key, 0, "sample_descriptor:%s:%s", filename, mode); if (zend_hash_find(&EG(persistent_list), hash_key, hash_key_len + 1, (void **)&existing_file) == SUCCESS) { /* There's already a file open, return that! */ ZEND_REGISTER_RESOURCE(return_value, existing_file->ptr, le_sample_descriptor_persist); efree(hash_key); return; } fp = fopen(filename, mode); if (!fp) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to open %s using mode %s", filename, mode); RETURN_FALSE; } if (!persist) { fdata = emalloc(sizeof(php_sample_descriptor_data)); fdata->filename = estrndup(filename, filename_len); fdata->fp = fp; ZEND_REGISTER_RESOURCE(return_value, fdata, le_sample_descriptor); } else { list_entry le; fdata =pemalloc(sizeof(php_sample_descriptor_data),1); fdata->filename = pemalloc(filename_len + 1, 1); memcpy(data->filename, filename, filename_len + 1); fdata->fp = fp; ZEND_REGISTER_RESOURCE(return_value, fdata, le_sample_descriptor_persist); /* Store a copy in the persistent_list */ le.type = le_sample_descriptor_persist; le.ptr = fdata; /* hash_key has already been created by now */ zend_hash_update(&EG(persistent_list), hash_key, hash_key_len + 1, (void*)&le, sizeof(list_entry), NULL); } efree(hash_key); }
Because all extensions use the same persistent HashTable list to store their resources in, it's important that you choose a hash key that is both reproducible and unique. A common conventionas seen in the sample_fopen() functionis to use the extension and resource type names as a prefix, followed by the creation criteria.
Liveness Checking and Early Departure
Although it's safe to assume that once you open a file, you can keep it open indefinitely, other resource typesparticularly remote network resourcesmay have a tendency to become invalidated, especially when they're left unused for long periods between requests.
When recalling a stored persistent resource into active duty, it is therefore important to make sure that it's still usable. If the resource is no longer valid, it must be removed from the persistent list and the function should continue as though no already allocated resource had been found.
The following hypothetical code block performs a liveness check on a socket stored in the persistent list:
if (zend_hash_find(&EG(persistent_list), hash_key, hash_key_len + 1, (void**)&socket) == SUCCESS) { if (php_sample_socket_is_alive(socket->ptr)) { ZEND_REGISTER_RESOURCE(return_value, socket->ptr, le_sample_socket); return; } zend_hash_del(&EG(persistent_list), hash_key, hash_key_len + 1); }
As you can see, all that's been done here is to manually remove the list entry from the persistent list during runtime as opposed to engine shutdown (when it would normally be destroyed). This action handles the work of calling the persistent dtor method, which would have been defined by zend_register_list_destructors_ex(). On completion of this code block, the function will be in the same state it would have been if no resource had been found in the persistent list.
Agnostic Retrieval
At this point you can create file descriptor resources, store them persistently, and recall them transparently, but have you tried using a persistent version with your sample_fwite() function? Frustratingly, it doesn't work! Recall how the resource pointer is resolved from its numeric ID:
ZEND_FETCH_RESOURCE(fdata, php_sample_descriptor_data*, &file_resource, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);
le_sample_descriptor is explicitly named so that the type can be verified and you can be assured that you're not using a mysql_connection_handle* or some other type when you expect to see, for example, a php_sample_descruptor_data* structure. Mixing and matching types is generally a "bad thing." You know that the same data structure stored in le_sample_descriptor resources are also stored in le_sample_descruotor_persist resources, so to keep things simple in userspace, it'd be ideal if sample_fwrite() could simply accept either type equally.
This is solved by using ZEND_FETCH_RESOURCE()'s sibling: ZEND_FETCH_RESOURCE2(). The only difference between these two macros is that the latter enables you to specifythat's righttwo resource types. In this case you'd change that line to the following:
ZEND_FETCH_RESOURCE2(fdata, php_sample_descriptor_data*, &file_resource, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor, le_sample_descriptor_persist);
Now, the resource ID contained in file_resource can refer to either a persistent or non-persistent Sample Descriptor resource and they will both pass validation checks.
Allowing for more than two resource types requires using the underlying zend_fetch_resource() implementation. Recall that the ZEND_FETCH_RESOURCE() macro you originally used expands out to
fp = (FILE*) zend_fetch_resource(&file_descriptor TSRMLS_CC, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME, NULL, 1, le_sample_descriptor); ZEND_VERIFY_RESOURCE(fp);
Similarly, the ZEND_FETCH_RESOURCE2() macro you were just introduced to also expands to the same underlying function:
fp = (FILE*) zend_fetch_resource(&file_descriptor TSRMLS_CC, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME, NULL, 2, le_sample_descriptor, le_sample_descriptor_persist); ZEND_VERIFY_RESOURCE(fp);
See a pattern? The sixth and subsequent parameters to zend_fetch_resource() say "There are N possible resource types I'm willing to match, and here they are...." So to match a third resource type (for example: le_sample_othertype), type the following:
fp = (FILE*) zend_fetch_resource(&file_descriptor TSRMLS_CC, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME, NULL, 3, le_sample_descriptor, le_sample_descriptor_persist, le_sample_othertype); ZEND_VERIFY_RESOURCE(fp);
And so on and so forth.
The PHP Life Cycle
Variables from the Inside Out
Memory Management
Setting Up a Build Environment
Your First Extension
Returning Values
Accepting Parameters
Working with Arrays and HashTables
The Resource Data Type
PHP4 Objects
PHP5 Objects
Startup, Shutdown, and a Few Points in Between
INI Settings
Accessing Streams
Implementing Streams
Diverting the Stream
Configuration and Linking
Extension Generators
Setting Up a Host Environment
Advanced Embedding
Appendix A. A Zend API Reference
Appendix B. PHPAPI
Appendix C. Extending and Embedding Cookbook
Appendix D. Additional Resources