[翻訳][php拡張機能の開発と組み込み] 第9章 - リソースのデータ型

黄舟
リリース: 2023-03-05 16:22:02
オリジナル
1093 人が閲覧しました

リソース データ型

これまで、非常に基本的なユーザー空間のデータ型、文字列、値、TRUE/FALSE およびその他の値について学習してきましたが、前の章で配列について触れ始めました。これらの基本データ型の配列を収集しました。

複雑な構造

実際の世界では、通常、不明瞭な構造ポインターを含む、より複雑なデータ コレクションを操作する必要があります。不明瞭な構造ポインターの一般的な例は、stdio です。stdio のファイル記述子です。

#include <stdio.h>  
int main(void)  
{  
    FILE *fd;  
    fd = fopen("/home/jdoe/.plan", "r");  
    fclose(fd);  
    return 0;  
}
ログイン後にコピー

stdio のファイル記述子は、他のほとんどのファイル記述子と同じであり、ブックマークのようなものであり、拡張呼び出しアプリケーションは feof() に含める必要があるだけです。この値は、次の場合に渡されます。 fread()、fwrite()、fclose() などの実装関数を呼び出す場合、このブックマークはユーザー空間コードにアクセスできる必要があるため、標準の PHP 変数または zval * メソッドで表す必要があります。ここでは新しいデータ型が必要です。RESOURCE データ型は zval * に単純な整数値を格納し、検索用の登録済みリソースのインデックスとして使用されます。リソース エントリには、内部データ型とポインターで表されるリソース インデックスが含まれます。リソース データやその他の情報を保存します。

リソース タイプを定義します。

登録されたリソース エントリに含まれるリソース情報をより明確にするには、まず、sample.c でリソースのタイプを定義する必要があります。いくつかの関数を実装し、次のコード スニペットを追加します

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;  
}
ログイン後にコピー

次に、コード ファイルの最後までスクロールして、sample_module_entry 構造を変更し、行 NULL, /* MINIT */ をこの構造に指定したのと同じように次の内容に置き換えます。関数リスト構造を に追加するときと同様に、行末にカンマを必ず残す必要があります。

PHP_MINIT(sample), /* MINIT */
ログイン後にコピー

最後に、php_sample.h で PHP_SAMPLE_DESCRIPTOR_RES_NAME を定義し、次のコードを他の定数定義の下に配置する必要があります。 :

#define PHP_SAMPLE_DESCRIPTOR_RES_NAME "File Descriptor"
ログイン後にコピー

PHP_MINIT_FUNCTION() は、第 1 章「PHP のライフサイクル」で紹介された 4 つの特別な起動および終了操作の最初の操作を表し、ライフサイクルについては第 12 章「起動、終了、およびその間のいくつかの重要なポイント」で説明されています。これについては、第 13 章「INI 設定」で詳しく説明します。

ここで知っておくべき非常に重要なことは、MINIT 関数は拡張機能が最初にロードされるときに一度実行され、すべてのリクエストが到着する前に実行されるということです。この機会にデストラクターを登録しますが、それらは NULL 値ですが、リソース タイプを知るには一意の整数 ID で十分な場合は、すぐに変更できます。

登録されたリソース

これで、リソース データを保存したいことがエンジンに認識されます。これを行うには、次のように fopen() コマンドを再実装する必要があります:

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);  
}
ログイン後にコピー

FILE が何であるかをコンパイラーに知らせるために、 * を含める必要があります。 stdio.h。これは、sample.c に配置できますが、この章の後半の部分に備えて、php_sample.h に配置するようにお願いします。

この行より前のコードはすべて読み取り可能である必要があります。このコード行は、fp ポインターをリソースのインデックスに格納し、MINIT で定義された型に関連付け、return_value での検索に使用できるキーを格納するタスクを実行します。 .

複数のポインタ値を保存する必要がある場合、または直接量を保存する必要がある場合は、データを保存するために新しいメモリを割り当て、このメモリを指すポインタをリソースとして登録する必要があります。

翻訳注:

1. リソース データ タイプの登録では、実際に新しく構築された zend_rsrc_list_dtors_entry 構造体 (Zend/zend_list.c で定義された静的グローバル変数 HashTable) がこのリソース タイプの情報を記述します。のデータ (ZEND_REGISTER_RESOURCE) は実際に EG (正規リスト) の zend_hash_next_free_element() を使用して次の数値添え字をリソース ID として取得し、受信リソース ポインタ (zend_rsrc_list_entry 構造体としてカプセル化) を添え字に対応する要素の EG (正規リスト) に保存します。 .

3. リクエストの初期化フェーズで EG (normal_list) の初期化が完了します。コードを追跡すると、関数の呼び出しプロセスが次のようになっていることがわかります。 (Zend/zend.c) --> init_compiler(Zend/zend_compile.c) --> zend_init_rsrc_list(Zend/zend_list.c) zend_init_rsrc_list() 関数を観察すると、デストラクタは list_entry_destructor (Zend/zend_list.c) です。 list_entry_destructor() のロジックは、list_destructors (上記の最初の手順で説明した静的グローバル変数) から解放されるリソース オブジェクト タイプの情報を見つけて、登録されたものに従うことです。リソースタイプ この時点で指定されたデストラクターが破棄されます。

4. 上記の点に従って、この章で説明した内容を明確にすることができます。 まず、このリソースタイプにはモジュールなどの情報が含まれます。そして、特定のリソース オブジェクトを作成するときに、リソース オブジェクトとリソース タイプの間に関連付けが作成されます

リソースを解放します。

现在你已经有办法附加内部数据块到用户空间. 因为大多数你附加到用户空间的资源变量都需要在某个时刻去清理(这里是调用fclose()), 因此你可能需要一个匹配的sample_fclose()函数接受资源变量, 处理它的销毁并从注册的资源列表(EG(regular_list))中删除它.

如果变量被简单的unset()会怎么样呢? 没有到原来的FILE *指针的引用, 就没有办法去fclose()它, 它就会保持打开状态直到php进程终止. 因为单进程将服务多个请求, 这可能需要很长时间.

答案就是你传递给zend_register_list_destructors_ex的NULL指针. 顾名思义, 你注册的是析构函数. 第一个指针指向的函数在一个请求生命周期内注册资源的最后一个引用被破坏时调用. 实际上就是我们所说的在已存储的资源变量上调用unset().

传递给zend_register_list_destructors_ex的第二个指针指向另外一个回调函数, 它用于持久化资源, 当一个进程或线程终止时被调用. 本章后面将会介绍持久化资源.

现在我们来定义第一个析构函数. 将下面的代码放到你的PHP_MINIT_FUNCTION上面:

static void php_sample_descriptor_dtor(  
                    zend_rsrc_list_entry *rsrc TSRMLS_DC)  
{  
    FILE *fp = (FILE*)rsrc->ptr;  
    fclose(fp);  
}
ログイン後にコピー

下一步是将zend_register_list_destructors_ex调用中的第一个NULL替换为php_sample_destriptor_dtor:

le_sample_descriptor = zend_register_list_destructors_ex(  
        php_sample_descriptor_dtor, NULL,  
        PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);
ログイン後にコピー

现在, 当变量被赋值为sample_fopen()注册的资源值时, 当变量通过unset()或到达函数结束隐式的结束其生命周期时, 将自动的调用fclose()释放FILE *指针. 不再需要sample_fclose()的实现了.

<?php  
  $fp = sample_fopen("/home/jdoe/notes.txt", "r");  
  unset($fp);  
?>
ログイン後にコピー

当unset($fp)被调用时, 引擎会自动的调用php_sample_descriptor_dtor去处理资源的清理.

资源解码

创建资源仅仅是第一步, 因为书签的作用只是让你可以回到原来的那一页. 这里是另外一个函数:

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();  
    }  
    /* 使用zval *验证资源类型, 并从注册资源表中取回它的指针 */  
    ZEND_FETCH_RESOURCE(fp, FILE*, &file_resource, -1,  
        PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);  
    /* 写数据并返回实际写入到文件的字节数 */  
    RETURN_LONG(fwrite(data, 1, data_len, fp));  
}
ログイン後にコピー

在zend_parse_parameters()中使用"r"格式描述符相对比较新, 不过, 在你阅读完第7章"接受参数"后应该可以理解. 这里真正新鲜的是ZEND_FETCH_RESOURCE()的使用.

展开ZEND_FETCH_RESOURCE()宏, 代码如下:

#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);
ログイン後にコピー

套用当前示例则如下:

fp = (FILE*) zend_fetch_resource(&file_descriptor TSRMLS_CC, -1,  
                    PHP_SAMPLE_DESCRIPTOR_RES_NAME, NULL,  
                    1, le_sample_descriptor);  
if (!fp) {  
    RETURN_FALSE;  
}
ログイン後にコピー


就像上一章学习的zend_hash_find()函数一样, zend_fetch_resource()实际上是使用索引在一个HashTable集合中找出之前存储的数据. 与zend_hash_find()的不同在于这个函数执行了额外的数据完整性检查, 比如确保资源表中的条目是正确的资源类型.

现在, 你请求的zend_fetch_resource()是和在le_sample_descriptor中存储的资源类型匹配的. 如果提供的资源ID不存在, 或者是不正确的类型, zend_fetch_resource()将返回NULL, 并自动的产生一个错误.

通过在ZEND_FETCH_RESOURCE()宏内部包含ZEND_VERIFY_RESOURCE()宏, 函数实现可以自动的返回, 使得函数自身的代码可以聚焦条件正确时对资源数据值的处理上. 现在你的函数得到了原来的FILE *指针, 直接和普通程序一样调用内部的fwrite()函数.

为了避免zend_fetch_resource()在失败时产生错误, 可以将resource_type_name参数传递为NULL. 由于无法产生有意义的错误消息, zend_fetch_resoure()将会静默的失败.

还有一种将资源变量ID翻译成所存储的资源指针的方法是使用zend_list_find()函数:

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));  
}
ログイン後にコピー

虽然对于一般的C语言背景程序员, 这种方式更加容易理解, 但它相比ZEND_FETCH_RESOURCE()更加冗长. 你可以根据自己的编码风格选择合适的方法, 但是还是希望你可以去看看php内核中的其他扩展, 更多的还是使用了ZEND_FETCH_RESOURCE()宏.

强制析构

前面你看到了使用unset()让一个变量结束其生命周期可以触发资源的析构, 并导致其下的资源被以你注册的析构函数清理. 现在想想一个资源变量被拷贝到了其他变量中:

<?php  
  $fp = sample_fopen("/home/jdoe/world_domination.log", "a");  
  $evil_log = $fp;  
  unset($fp);  
?>
ログイン後にコピー

此时, $fp并不是注册资源的唯一引用, 因此该资源并没有结束它的生命周期, 不会被释放. 这表示$evil_log仍然可以写. 当你真正的需要一个资源不再被使用时, 为了避免四处找寻引用它的代码, 就需要一个sample_fclose()实现:

PHP_FUNCTION(sample_fclose)  
{  
    FILE *fp;  
    zval *file_resource;  
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r",  
                        &file_resource) == FAILURE ) {  
        RETURN_NULL();  
    }  
    /* 虽然并不需要真的取回FILE *资源, 但执行这个宏可以去检查我们关闭资源类型是否正确 */  
    ZEND_FETCH_RESOURCE(fp, FILE*, &file_resource, -1,  
        PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);  
    /* 强制资源进入自解模式 */  
    zend_hash_index_del(&EG(regular_list),  
                    Z_RESVAL_P(file_resource));  
    RETURN_TRUE;  
}
ログイン後にコピー

这个删除方法更有力的说明了资源变量是注册在一个全局的HashTable中的. 使用资源ID作为索引在regular_list中查找并删除这个资源条目是很简单的. 虽然其他的HashTable直接操作函数, 比如zend_hash_index_find()/zend_hash_next_index_insert()可以用来替代FETCH和REGISTER宏, 但是这种做法是不鼓励的, 因为这可能使得Zend API在发生变更时影响已有的扩展.

和用户空间的HashTable变量(数组)一样, EG(regular_list)这个HashTable有一个自动的dtor函数, 每当一条记录被移除或覆盖时都会调用该函数. 这个方法会检查你的资源类型, 调用在MINIT中调用zend_register_list_destructors_ex()提供的析构函数.

在php内核和Zend引擎中, 你可以看到很多地方在现在这种情况时使用的是zend_list_delete(), 而不是zend_hash_index_del(). 这是因为zend_list_delete()中有对引用计数的维护, 这一点你将在本章后面看到.

持久化资源

对于存储资源变量的复杂数据类型通常需要可观的内存分配, CPU时间, 或网络通信去初始化. 对于每个调用都需要重新建立的资源类型, 比如数据库连接, 让它们可以在多个请求之间共享是很有用的.

内存分配

通过前面章节的学习我们知道, emalloc()以及它的同族函数是在php中分配内存时的首选, 因为它们能够做到系统的malloc()函数所不能的垃圾回收, 使得在脚本意外终止时通过它们分配的内存可以被回收. 如果一个持久化的资源要跨请求逗留, 这样的垃圾回收很显然不是一件好事.

想象一下, 现在还需要和FILE *指针一起保存打开文件的文件名. 现在, 你就需要在php_sample.h中创建一个自定义结构体来保存这个联合信息:

typedef struct _php_sample_descriptor_data {  
    char *filename;  
    FILE *fp;  
} php_sample_descriptor_data;
ログイン後にコピー

sample.c中所有你处理文件资源的代码都需要修改:

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));  
}
ログイン後にコピー

从技术角度来说, sample_fclose()可以不用修改, 因为它并不会真的直接处理资源数据. 如果你有信心, 可以自己去更新它.

迄今为止, 一切都是完美的, 因为你仍然只是注册了一个非持久化的描述符资源. 此时, 可以增加一个新的函数去获取已经打开的资源的文件名.

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);  
}
ログイン後にコピー

然而, 在你开始注册持久化版本的描述符资源时, 问题很快就会显现.

延后析构

你已经能够看到了, 非持久化资源一旦所有持有该资源ID引用的变量都被unset()或结束其生命周期, 它们都会从EG(regular_list)(它是包含所有每个请求注册的资源的HashTable)中被移除.

本章后面你将看到的持久化资源, 也存储在一个HashTable中: EG(persistent_list). 它跟EG(regular_list)有所不同, 使用的索引是关联形式的, 元素不会在请求结束后自动的从HashTable中移除. EG(persistent_list)中的条目只有通过手动调用zend_hash_del()或在线程/进程完全终止(通常是在webserver停止时)时才会被移除.

与EG(regular_list)类似, EG(persistent_list)也有自己的dtor函数. 类似于regular_list, 这个函数也是使用资源类型查找对应的析构函数并调用. 但这里它调用的是调用zend_register_list_destructors_ex()注册资源类型时提供的第二个参数.

实际上, 持久化和非持久化资源注册为两种完全分开的类型是为了避免非持久化析构代码在本应为持久化的资源上再调用一次. 具体依赖于你的实现, 你可以选择在同一个类型中组合非持久化和持久化两种析构函数. 现在, 在sample.c中增加另外一个静态的int变量用于新的持久化资源:

static int le_sample_descriptor_persist;
ログイン後にコピー

接着扩充你的MINIT函数, 增加一个资源注册, 使用新的用于持久化分配结构的dtor函数:

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;  
}
ログイン後にコピー

通过给这两个资源类型相同的名字, 它们的不同对于终端用户就是透明的. 在内部, 只有一种在请求清理过程会调用php_sample_descriptor_dtor; 另外一个, 你马上会看到, 它将和webserver的进程或线程保持相同的生命周期.

持久化注册

现在相应的清理函数已经到位了, 是时候创建一些可用的资源结构了. 通常会使用两个独立的函数, 在内部映射到同一个实现上, 但是这可能会使得已经很混杂的主题更加混乱, 所以我们这里只是在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);  
  
        /* 在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);  
    }  
}
ログイン後にコピー

这个函数的核心部分现在你应该已经很熟悉了. 打开一个文件, 将它的名字存储到新分配的内存中, 将它注册为请求特有的资源ID并设置到return_value中. 这一次新的知识点是第二部分, 但它也并不完全陌生.

这里, 你实际上做的事情和ZEND_REGISTER_RESOURCE()所做的基本一致; 不过, 这里不再是获取一个数值索引放到每个请求特有的列表(EG(regular_list))中, 而是赋值给了一个关联key(可以使用它在未来的请求中重新获取资源), 将它放到了持久化列表中, 这个持久化列表(EG(persistent_list))并不会在每个请求结束后被清理.

当这样的一个持久化描述符资源结束其生命周期时, EG(regular_list)的dtor函数将会检查已注册的析构器列表, 发现le_sample_descriptor_persist的(非持久化)析构器为NULL, 因此不做任何事(即不进行释放操作). 这使得FILE *指针和它的char *名字字符串可以在下一个请求中安全的使用.

当资源最终从EG(persistent_list)中移除时(由于进程或线程终止, 或者由于你的扩展有意的移除), 引擎会查找持久化析构器. 由于这个资源类型定义了持久化析构器, 因此它将会被正确的调用pefree()释放原来由pemalloc()分配的内存.

重用

将一个资源条目的拷贝放到persistent_list中, 除了延长执行时间, 占用内存以及文件锁资源, 不会有任何好处, 除非你在后续的请求中以某种方式重用它.

这就是hash_key的来由. 当sample_fopen()被调用时, 无论是持久化或非持久化方式, 你的函数都可以使用请求的文件名和模式参数重新创建hash_key, 并在打开文件之前尝试从persistent_list中使用hash_key查找该资源.

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;  
    }  
    /* 尝试查找已经打开的文件 */  
    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&#39;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);  
        /* 存储一份拷贝到persistent_list */  
        le.type = le_sample_descriptor_persist;  
        le.ptr = fdata;  
        /* hash_key现在已经创建了 */  
        zend_hash_update(&EG(persistent_list),  
            hash_key, hash_key_len + 1,  
            (void*)&le, sizeof(list_entry), NULL);  
    }  
    efree(hash_key);  
}
ログイン後にコピー

因为所有的扩展都使用同一个持久化HashTable存储它们的资源, 因此选择唯一的可复现的hash_key非常重要. sample_fopen()中使用了一种常见的方式: 使用扩展和资源类型名字作为前缀, 接着是创建的资源的关键信息.

活性检查和提前离开

尽管你打开一个文件并无限期的保持打开是安全的, 但是对于其他资源类型则不然, 尤其是远程网络资源可能会变得不可用, 尤其是在请求间长时间不使用时.

因此在取回一个持久化资源时, 对它的可用性检查就非常重要. 如果资源不再可用, 就必须从持久化列表中移除, 并且应该继续以没有找到已分配资源(持久化)的逻辑执行.

下面的假想代码块在持久化列表中的套接字上执行了一个活性检查:

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);  
}
ログイン後にコピー

如你所见, 这里所做的只是在运行时手动的从持久化资源列表中移除. 这个行为会触发调用zend_register_list_destructors_ex()注册的持久化dtor函数. 在这段代码完成后, 函数所处的状态和没有从持久化列表中找到资源时的状态一致.

未知类型的取回

此刻你可以创建文件描述符资源, 将它们持久化存储, 并可以透明的获取它们, 但是你试试用sample_fwrite()函数使用你的持久化资源对象? 很无奈, 它不能工作. 回顾一下, 数值ID怎样转换成资源指针:

ZEND_FETCH_RESOURCE(fdata, php_sample_descriptor_data*,  
    &file_resource, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME,  
    le_sample_descriptor);
ログイン後にコピー

le_sample_descriptor明确指定了类型名, 因此资源的类型将被验证. 这样做就可以确保你在希望得到php_sample_descriptor_data *结构的资源时, 不会得到mysql_connection_handler *或其他类型的资源. 但这对于混合匹配类型来说就不是一件好事. 我们知道, 在le_sample_descriptor和le_sample_descriptor_persist两种资源类型中存储了相同的数据结构, 这样做是为了保证用户空间的简单性, 因此, 理想的情况是sample_fwrite()可以公平的接受两种类型.

这可以通过ZEND_FETCH_RESOURCE()的兄弟宏: ZEND_FETCH_RESOURCE2()来解决. 这两个宏唯一的不同是后者允许指定两种资源类型. 这样, 我们就可以对上面的代码进行修改:

ZEND_FETCH_RESOURCE2(fdata, php_sample_descriptor_data*,  
    &file_resource, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME,  
    le_sample_descriptor, le_sample_descriptor_persist);
ログイン後にコピー

现在, file_resource中包含的资源ID就可以指向持久化以及非持久化的Sample Descriptor资源了, 并且它们都能够通过验证.

要允许多个资源类型需要使用原生的zend_fetch_resource()实现. 回顾前面, ZEND_FETCH_RESOURCE()宏展开如下:

fp = (FILE*) zend_fetch_resource(&file_descriptor TSRMLS_CC, -1,  
    PHP_SAMPLE_DESCRIPTOR_RES_NAME, NULL,  
    1, le_sample_descriptor);  
ZEND_VERIFY_RESOURCE(fp);
ログイン後にコピー

类似的, ZEND_FETCH_RESOURCE2()宏展开后也使用了相同的原生函数:

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);
ログイン後にコピー

看到规律了吗? zend_fetch_resource()第6个以及后面的参数的含义是"我将要匹配N种可能的资源类型, 它们分别是...", 因此, 如果要匹配第三种资源类型(比如: le_sample_othertype), 就可以如下编码:

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);
ログイン後にコピー

如果要四个, 就依此类推.

译注: 译者使用的php-5.4.9下, 原著的示例不能正常使用, 因此贴出译者自己环境下可编译的代码, 需要的读者可以参考这个示例.

PHP_FUNCTION(sample_fopen)  
{  
    sample_descriptor_data_t    *sddp;  
    FILE                        *fp;  
    char                        *filename, *mode;  
    int                         filename_len, mode_len;  
    zend_bool                   persist = 1;  
    char                        *hash_key;  
    int                         hash_key_len;  
    int                         rsrc_l;  
    zend_rsrc_list_entry        *le_p;  
  
    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;  
    }     
  
    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 **)&le_p) == SUCCESS){   
        rsrc_l  = ZEND_REGISTER_RESOURCE(return_value, le_p->ptr, le_sample_descriptor_persist);  
    } else {  
        fp  = fopen(filename, mode);  
        if ( !fp ) {   
            efree(hash_key);  
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to open %s using mode %s",   
                    filename, mode);  
            RETURN_FALSE;  
        }     
  
        sddp        = pemalloc(sizeof(sample_descriptor_data_t), persist);  
        sddp->fname = pestrdup(filename, persist);  
        sddp->fp    = fp;   
        rsrc_l      = ZEND_REGISTER_RESOURCE(return_value, sddp, persist ? 
        le_sample_descriptor_persist : le_sample_descriptor);  
        if ( persist ) {   
            zend_rsrc_list_entry    le;   
  
            le.type = le_sample_descriptor_persist;  
            le.ptr  = sddp;  
            zend_hash_update(&EG(persistent_list), hash_key, hash_key_len + 1, 
            (void *)&le, sizeof(zend_rsrc_list_entry), NULL);  
        }     
    }     
    efree(hash_key);  
    RETURN_RESOURCE(rsrc_l);  
}  
  
PHP_FUNCTION(sample_fwrite)  
{  
    sample_descriptor_data_t    *sddp;  
    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_FALSE;  
    }     
    ZEND_FETCH_RESOURCE2(sddp, sample_descriptor_data_t *, &file_resource, -1,   
        SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor, le_sample_descriptor_persist);  
#if ZEND_DEBUG  
php_printf("FILE * pointer: %p\n", sddp->fp);  
#endif  
    RETURN_LONG(fwrite(data, 1, data_len, sddp->fp));  
}  
  
PHP_FUNCTION(sample_fclose)  
{  
    sample_descriptor_data_t    *sddp;  
    zval                        *file_resource;  
  
    if ( zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &file_resource) == FAILURE ) {   
        RETURN_FALSE;  
    }     
  
    ZEND_FETCH_RESOURCE2(sddp, sample_descriptor_data_t *, &file_resource, -1, 
    SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor, le_sample_descriptor_persist);  
    zend_hash_index_del(&EG(regular_list), Z_RESVAL_P(file_resource));  
  
    RETURN_TRUE;  
}  
  
PHP_FUNCTION(sample_fname)  
{  
    sample_descriptor_data_t    *sddp;  
    zval    *file_resource;  
  
    if ( zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &file_resource) == FAILURE ) {   
        RETURN_FALSE;  
    }     
  
    ZEND_FETCH_RESOURCE2(sddp, sample_descriptor_data_t *, &file_resource, -1, 
    SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor, le_sample_descriptor_persist);  
    RETURN_STRING(sddp->fname, 1);   
}
ログイン後にコピー

其他引用计数器

和用户空间变量类似, 已注册资源也有引用计数. 这里, 引用计数指有多少容器结构知道这个资源ID.

现在我们已经知道, 当用户空间变量(zval *)的类型是IS_RESOURCE时, 它并不会真正的持有任何结构的指针, 只是简单的保存一个HashTable的索引值, 通过这个索引值可以在EG(regular_list) HashTable中查找到真正的资源指针.

当一个资源第一次被创建时, 比如通过调用sample_fopen(), 它被放到一个zval *容器中, 并将它的refcount初始化为1, 因为只有一个变量持有它.

$a = sample_fopen(&#39;notes.txt&#39;, &#39;r&#39;);  
/* var->refcount = 1, rsrc->refcount = 1 */
ログイン後にコピー

如果变量被拷贝, 通过第3章"内存管理"的学习可以知道, 并不会创建新的zval *. 而是两个变量共享同一个写时复制的zval *. 这种情况下, zval *的refcount被增加到2; 然而, 此时资源的refcount值仍然为1, 因为它仅被一个zval *持有.

$b = $a;  
/* var->refcount = 2, rsrc->refcount = 1 */
ログイン後にコピー

当这两个变量中的一个被unset()时, zval *的refcount减小, 但是它并不会被真的销毁, 因为还有其他变量仍然指向它.

unset($b);  
/* var->refcount = 1, rsrc->refcount = 1 */
ログイン後にコピー

现在你还应该知道, 混合引用赋值和写时复制将强制隔离并拷贝到新的zval *中. 当发生这件事时, 资源的引用计数将会增加, 因为它现在被两个zval *持有.

$b = $a;  
$c = &$a;  
/* bvar->refcount = 1, bvar->is_ref = 0 
   acvar->refcount = 2, acvar->is_ref = 1 
   rsrc->refcount = 2 */
ログイン後にコピー

现在, 卸载$b将会完全释放它的zval *, 将rsrc->refcount修改为1. 卸载$a或$c但不两者都卸载则不会减小资源的refcount, 因为它们的zval *(acvar)实际上还是存在的. 直到所有三个变量(涉及到两个zval *)都被unset()后, 资源的refcount才会减小到0, 它的析构函数才会被触发.

小结

使用本章涉及的主题, 你就可以开始应用php著名的粘合性了. 资源数据类型使得你的扩展可以很容易的将第三方库的透明指针这样的抽象概念, 连接到用户空间脚本语言中, 使得php更加强大.

接下来两章你将深入php词法中最后但很重要的数据类型. 你将首先探究简单的基于Zend引擎1的类, 接着就要把它迁移到更强大的Zend引擎2中.

以上就是 [翻译][php扩展开发和嵌入式]第9章-资源数据类型的内容,更多相关内容请关注PHP中文网(m.sbmmt.com)!


ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート