Complex Structures

Out in the real world, you'll usually have to work with more complex collections of data, often involving pointers to opaque structures. One common example of an opaque structure is the stdio file descriptor that appears even to C code as nothing more than a pointer.

#include int main(void) { FILE *fd; fd = fopen("/home/jdoe/.plan", "r"); fclose(fd); return 0; }

The way the stdio file descriptor is then usedlike most file descriptorsis like a bookmark. The calling applicationyour extensionneed only pass this value into the implementation functions such as feof(), fread(), fwrite(), fclose(), and so on. At some point, however, this bookmark must be accessible to userspace code; therefore, it's necessary to be able to represent it within the standard PHP variable, or zval*.

This is where a new data type comes into play. The RESOURCE data type stores a simple integer value within the zval* itself, which is then used as a lookup into an index of registered resources. The resource entry contains information about what internal data type the resource index represents as well as a pointer to the stored resource data.

Defining Resource Types

In order for registered resource entries to understand anything about the resource they contain, it's necessary for that resource type to be declared. Start by adding the following piece of code to sample.c right after your existing function implementations:

static int le_sample_descriptor; PHP_MINIT_FUNCTION(sample) { le_sample_descriptor = zend_register_list_destructors_ex( NULL, NULL, PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number); return SUCCESS; }

Next, scroll down to the bottom of your file and modify the sample_module_entry structure replacing the NULL, /* MINIT */ line. Just as when you added your function list to this structure, you will want to make sure to keep a comma at the end of this line.

PHP_MINIT(sample), /* MINIT */

Finally, you'll need to define PHP_SAMPLE_DESCRIPTOR_RES_NAME within php_sample.h by placing the following line next to your other constant definitions:

#define PHP_SAMPLE_DESCRIPTOR_RES_NAME "File Descriptor"

PHP_MINIT_FUNCTION() represents the first of four special startup and shutdown operations that you were introduced to conceptually in Chapter 1, "The PHP Life Cycle," and which you'll explore in greater depth in Chapter 12, "Startup, Shutdown, and a Few Points in Between," and Chapter 13, "INI Settings."

What's important to know at this juncture is that the MINIT method is executed once when your extension is first loaded and before any requests have been received. Here you've used that opportunity to register destructor functionsthe NULL values, which you'll change soon enoughfor a resource type that will be thereafter known by a unique integer ID.

Registering Resources

Now that the engine is aware that you'll be storing some resource data, it's time to give userspace code a way to generate the actual resources. To do that, implement the following re-creation of the fopen() command:

PHP_FUNCTION(sample_fopen) { 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; } ZEND_REGISTER_RESOURCE(return_value, fp, le_sample_descriptor); }

Note

In order for the compiler to know what FILE* is, you'll need to include stdio.h. This could be placed in sample.c, but in preparation for a later part of this chapter, I'll ask you to place it in php_sample.h instead.

If you've been paying attention to the previous chapters, you'll recognize everything up to the final line. This one command does the job of storing the fp pointer into that index of resources, associating it with the type declared during MINIT, and storing a lookup key into return_value.

Note

If it's necessary to store more than one pointer value, or store an immediate value, a new memory segment must be allocated to store the data, and then a pointer to that memory segment can be registered as a resource.

 

Destroying Resources

At this point you have a method for attaching internal chunks of data to userspace variables. Because most of the data you're likely to attach to a userspace resource variable will need to be cleaned up at some pointby calling fclose() in this caseyou'll probably assume you need a matching sample_fclose() function to receive the resource variable and handle destroying and unregistering it.

What would happen if the variable were simply unset() though? Without a reference to the original FILE* pointer, there'd be no way to fclose() it, and it would remain open until the PHP process died. Because a single process serves many requests, this could take a very long time.

The answer comes from those NULL pointers you passed to zend_register_list_destructors_ex. As the name implies, you're registering destruction methods. The first pointer refers to a method to be called when the last reference to a registered resource falls out of scope within a request. In practice, this typically means when unset() is called on the variable in which the resource was stored.

The second pointer passed into zend_register_list_destructors_ex refers to another callback method that is executed for persistent resources when a process or thread shuts down. You'll take a look at persistent resources later in this chapter.

Let's define the first of these destruction methods now. Place the following bit of code above your PHP_MINIT_FUNCTION block:

static void php_sample_descriptor_dtor( zend_rsrc_list_entry *rsrc TSRMLS_DC) { FILE *fp = (FILE*)rsrc->ptr; fclose(fp); }

Next replace the first NULL in zend_register_list_destructors_ex with a reference back to php_sample_descriptor_dtor:

le_sample_descriptor = zend_register_list_destructors_ex( php_sample_descriptor_dtor, NULL, PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);

Now, when a variable is assigned with a registered resource value from sample_fopen(), it knows to automatically fclose() the FILE* pointer when the variable falls out of scope either explicitly through unset(), or implicitly at the end of a function. No sample_fclose() implementation is even needed!

When unset($fp); is called here, php_sample_descriptor_dtor is automatically called by the engine to handle cleanup of the resource.

Decoding Resources

Creating a resource is only the first step because a bookmark is only as useful as its ability to return you to the original page. Here's another new function:

PHP_FUNCTION(sample_fwrite) { FILE *fp; 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(); } /* Use the zval* to verify the resource type and * retrieve its pointer from the lookup table */ ZEND_FETCH_RESOURCE(fp, FILE*, &file_resource, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor); /* Write the data, and * return the number of bytes which were * successfully written to the file */ RETURN_LONG(fwrite(data, 1, data_len, fp)); }

Using the "r" format specifier to zend_parse_parameters() is a relatively new trick, but one that should be understandable from what you read in Chapter 7, "Accepting Parameters."What's truly fresh here is the use of ZEND_FETCH_RESOURCE().

Unfolding the ZEND_FETCH_RESOURCE() macro, one finds the following:

#define ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id, default_id, resource_type_name, resource_type) rsrc=(rsrc_type) zend_fetch_resource(passed_id TSRMLS_CC, default_id, resource_type_name, NULL, 1, resource_type); ZEND_VERIFY_RESOURCE(rsrc);

Or in this case:

fp = (FILE*) zend_fetch_resource(&file_descriptor TSRMLS_CC, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME, NULL, 1, le_sample_descriptor); if (!fp) { RETURN_FALSE; }

Like the zend_hash_find() method you explored in the last chapter, zend_fetch_resource() uses an index into a collectiona HashTable in factto pull out previously stored data. Unlike zend_hash_find(), this method performs additional data integrity checking such as ensuring that the entry in the resource table matches the correct resource type.

In this case, you've asked zend_fetch_resource() to match the resource type stored in le_sample_descriptor. If the supplied resource ID does not exist, or is of the incorrect type, then zend_fetch_resource() will return NULL and automatically generate an error.

By including the ZEND_VERIFY_RESOURCE() macro within the ZEND_FETCH_RESOURCE() macro, function implementations can automatically return, leaving the extension-specific code to focus on handling the generated resource value when conditions are correct. Now that your function has the original FILE* pointer back, it simply calls the internal fwrite() method as any normal program would.

Tip

To avoid having zend_fetch_resource() generate an error on failure, simply pass NULL for the resource_type_name parameter. Without a meaningful error message to display, zend_fetch_resource() will fail silently instead.

Another approach to translating a resource variable ID into a pointer is to use the zend_list_find() function:

PHP_FUNCTION(sample_fwrite) { FILE *fp; zval *file_resource; char *data; int data_len, rsrc_type; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs", &file_resource, &data, &data_len) == FAILURE ) { RETURN_NULL(); } fp = (FILE*)zend_list_find(Z_RESVAL_P(file_resource), &rsrc_type); if (!fp || rsrc_type != le_sample_descriptor) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid resource provided"); RETURN_FALSE; } RETURN_LONG(fwrite(data, 1, data_len, fp)); }

Although this method is probably more recognizable to someone with a generic background in C programming, it is also much more verbose than using ZEND_FETCH_RESOURCE(). Pick a method that suits your programming style best, but expect to see the ZEND_FETCH_RESOURCE() macro used predominantly in other extension codes such as those found in the PHP core.

Forcing Destruction

Earlier you saw how using unset() to take a variable out of scope can trigger the destruction of a resource and cause its underlying resources to be cleaned up by your registered destruction method. Imagine now that a resource variable were copied into other variables:

This time, $fp wasn't the only reference to the registered resource so it hasn't actually gone out of scope yet and won't be destroyed. This means that $evil_log can still be written to. In order to avoid having to search around for lost, stray references to a resource when you really, truly want it gone, it becomes necessary to have a sample_fclose() implementation after all:

PHP_FUNCTION(sample_fclose) { FILE *fp; zval *file_resource; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &file_resource) == FAILURE ) { RETURN_NULL(); } /* While it's not necessary to actually fetch the * FILE* resource, performing the fetch provides * an opportunity to verify that we are closing * the correct resource type. */ ZEND_FETCH_RESOURCE(fp, FILE*, &file_resource, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor); /* Force the resource into self-destruct mode */ zend_hash_index_del(&EG(regular_list), Z_RESVAL_P(file_resource)); RETURN_TRUE; }

This deletion method reinforced the fact that resource variables are registered within a global HashTable. Removing resource entries from this HashTable is a simple matter of using the resource ID as an index lookup into the regular list. Although other direct HashTable manipulation methodssuch as zend_hash_index_find() and zend_hash_next_index_insert()will work in place of the FETCH and REGISTER macros, such practice is discouraged where possible so that changes in the Zend API don't break existing extensions.

Like userspace variable HashTables (arrays), the EG(regular_list) HashTable has an automatic dtor method that is called whenever an entry is removed or overwritten. This method checks your resource's type, and calls the registered destruction method you provided during your MINIT call to zend_register_list_destructors_ex().

Note

In many places in the PHP Core and the Zend Engine you'll see zend_list_delete() used in this context rather than zend_hash_index_del(). The zend_list_delete() form takes into account reference counting, which you'll see later in this chapter.

Категории