PHP7擴展開發(三):參數、數組和Zvals

起步

到這已經能聲明簡單函數,返回靜態或者動態值了。定義INI選項,聲明內部數值或全局數值。本章節將介紹如何接收從調用腳本(php文件)傳入參數的數值,以及 PHP內核Zend引擎 如何操做內部變量。php

接收參數

與用戶控件的代碼不一樣,內部函數的參數實際上並非在函數頭部聲明的,函數聲明都形如: PHP_FUNCTION(func_name) 的形式,參數聲明不在其中。參數的傳入是經過參數列表的地址傳入的,而且是傳入每個函數,不管是否存在參數。node

經過定義函數hello_str()來看一下,它將接收一個參數而後把它與問候的文本一塊兒輸出。express

PHP_FUNCTION(hello_greetme)
{
    char *name = NULL;
    size_t name_len;
    zend_string *strg;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &name_len) == FAILURE) {
        RETURN_NULL();
    }

    strg = strpprintf(0, "你好: %s", name);
    RETURN_STR(strg);
}

大多數 zend_parse_parameters() 塊看起來都差很少。 ZEND_NUM_ARGS() 告訴Zend引擎要取的參數的信息, TSRMLS_CC 用來確保線程安全,返回值檢測是SUCCESS仍是FAILURE。一般狀況下返回是SUCCESS的。除非傳入的參數太少或太多或者參數不能被轉爲適當的類型,Zend會自動輸出一條錯誤信息並將控制權還給調用腳本。數組

指定 "s" 代表此函數指望只傳入一個參數,而且該參數被轉化爲string數據類型,地址傳入char * 變量。安全

此外,還有一個int變量經過地址傳遞到 zend_parse_parameters() 。這使Zend引擎提供字符串的字節長度,如此二進制安全的函數再也不依賴strlen(name)來肯定字符串的長度。由於實際上使用strlen(name)甚至得不到正確的結果,由於name可能在字符串結束以前包含了NULL字符。php7

在php7中,提供另外一種獲取參數的方式FAST_ZPP,是爲了提升參數解析的性能。多線程

#ifdef FAST_ZPP
ZEND_PARSE_PARAMETERS_START(1, 2)
    Z_PARAM_STR(type)
    Z_PARAM_OPTIONAL
    Z_PARAM_ZVAL_EX(value, 0, 1)
ZEND_PARSE_PARAMETERS_END();
#endif

參數類型表

類型 代碼 變量類型
Boolean b zend_bool
Long l long
Double d double
String s char*, int
Resource r zval *
Array a zval *
Object o zval *
zval z zval *

最後四個類型都是zvals *.這是由於在php的實際使用中,zval數據類型存儲全部的用戶空間變量。三種「複雜」數據類型:資源、數組、對象。當它們的數據類型代碼被用於zend_parse_parameters()時,Zend引擎會進行類型檢查,可是由於在C中沒有與它們對應的數據類型,因此不會執行類型轉換。app

Zval

通常而言,zval和php用戶空間變量是很傷腦筋的,概念很難懂。到了PHP7,它的結構在Zend/zend_types.h中有定義:函數

struct _zval_struct {
    zend_value        value;            /* value */
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,         /* active type */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)     /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t     next;                 /* hash collision chain */
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
        uint32_t     access_flags;         /* class constant access flags */
        uint32_t     property_guard;       /* single property guard */
    } u2;
};

能夠看到,變量是經過_zval_struct結構體存儲的,而變量的值是zend_value類型的:性能

typedef union _zend_value {
    zend_long         lval;             /* long value */
    double            dval;             /* double value */
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

雖然結構體看起來很大,但細細看,其實都是聯合體,value的擴充,u1是type_info,u2是其餘各類輔助字段。

zval 類型

變量存儲的數據是有數據類型的,php7中整體有如下類型,Zend/zend_types.h中有定義:

/* regular data types */
#define IS_UNDEF                    0
#define IS_NULL                     1
#define IS_FALSE                    2
#define IS_TRUE                     3
#define IS_LONG                     4
#define IS_DOUBLE                   5
#define IS_STRING                   6
#define IS_ARRAY                    7
#define IS_OBJECT                   8
#define IS_RESOURCE                 9
#define IS_REFERENCE                10

/* constant expressions */
#define IS_CONSTANT                 11
#define IS_CONSTANT_AST             12

/* fake types */
#define _IS_BOOL                    13
#define IS_CALLABLE                 14
#define IS_ITERABLE                 19
#define IS_VOID                     18

/* internal types */
#define IS_INDIRECT                 15
#define IS_PTR                      17
#define _IS_ERROR                   20

測試

書寫一個相似gettype()來取得變量的類型的hello_typeof():

PHP_FUNCTION(hello_typeof)
{
    zval *userval = NULL;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &userval) == FAILURE) {
        RETURN_NULL();
    }
    switch (Z_TYPE_P(userval)) {
        case IS_NULL:
            RETVAL_STRING("NULL");
            break;

        case IS_TRUE:
            RETVAL_STRING("true");
            break;

        case IS_FALSE:
            RETVAL_STRING("false");
            break;

        case IS_LONG:
            RETVAL_STRING("integer");
            break;

        case IS_DOUBLE:
            RETVAL_STRING("double");
            break;

        case IS_STRING:
            RETVAL_STRING("string");
            break;

        case IS_ARRAY:
            RETVAL_STRING("array");
            break;

        case IS_OBJECT:
            RETVAL_STRING("object");
            break;

        case IS_RESOURCE:
            RETVAL_STRING("resource");
            break;

        default:
            RETVAL_STRING("unknown type");
    }
}

這裏使用RETVAL_STRING()與以前的RETURN_STRING()差異並不大,它們都是宏。只不過RETURN_STRING中包含了RETVAL_STRING的宏代替,詳細在 Zend/zend_API.h 中有定義:

#define RETVAL_STRING(s)                ZVAL_STRING(return_value, s)
#define RETVAL_STRINGL(s, l)            ZVAL_STRINGL(return_value, s, l)

#define RETURN_STRING(s)                { RETVAL_STRING(s); return; }
#define RETURN_STRINGL(s, l)            { RETVAL_STRINGL(s, l); return; }

建立zval

前面用到的zval是由Zend引擎分配空間,也經過一樣的途徑釋放。然而有時候須要建立本身的zval,能夠參考以下代碼:

{
    zval temp;
    ZVAL_LONG(&temp, 1234);
}

數組

數組做爲運載其餘變量的變量。內部實現上使用了衆所周知的 HashTable .要建立將被返回PPHP的數組,最簡單的方法:

PHP語法 C語法(arr是zval*) 意義
$arr = array(); array_init(arr); 初始化一個新數組
$arr[] = NULL; add_next_index_null(arr); 向數字索引的數組增長指定類型的值
$arr[] = 42; add_next_index_long(arr, 42);
$arr[] = true; add_next_index_bool(arr, 1);
$arr[] = 3.14; add_next_index_double(arr, 3.14);
$arr[] = 'foo'; add_next_index_string(arr, "foo", 1);
$arr[] = $myvar; add_next_index_zval(arr, myvar);
$arr[0] = NULL; add_index_null(arr, 0); 向數組中指定的數字索引增長指定類型的值
$arr[1] = 42; add_index_long(arr, 1, 42);
$arr[2] = true; add_index_bool(arr, 2, 1);
$arr[3] = 3.14; add_index_double(arr, 3, 3.14);
$arr[4] = 'foo'; add_index_string(arr, 4, "foo", 1);
$arr[5] = $myvar; add_index_zval(arr, 5, myvar);
$arr['abc'] = NULL; add_assoc_null(arr, "abc");
$arr['def'] = 711; add_assoc_long(arr, "def", 711); 向關聯索引的數組增長指定類型的值
$arr['ghi'] = true; add_assoc_bool(arr, "ghi", 1);
$arr['jkl'] = 1.44; add_assoc_double(arr, "jkl", 1.44);
$arr['mno'] = 'baz'; add_assoc_string(arr, "mno", "baz", 1);
$arr['pqr'] = $myvar; add_assoc_zval(arr, "pqr", myvar);

作一個測試:

PHP_FUNCTION(hello_get_arr)
{
    array_init(return_value);
    add_next_index_null(return_value);
    add_next_index_long(return_value, 42);
    add_next_index_bool(return_value, 1);
    add_next_index_double(return_value, 3.14);
    add_next_index_string(return_value, "foo");
    add_assoc_string(return_value, "mno", "baz");
    add_assoc_bool(return_value, "ghi", 1);
}

20161122192253.png

add_*_string()函數參數從四個改成了三個。

數組遍歷

假設咱們須要一個取代如下功能的擴展:

<?php
function hello_array_strings($arr) {
    if (!is_array($arr)) {
        return NULL;
    }

    printf("The array passed contains %d elements\n", count($arr));

    foreach ($arr as $data) {
        if (is_string($data))
            echo $data.'\n';
    }
}

php7的遍歷數組和php5差不少,7提供了一些專門的宏來遍歷元素(或keys)。宏的第一個參數是HashTable,其餘的變量被分配到每一步迭代:

ZEND_HASH_FOREACH_VAL(ht, val)
ZEND_HASH_FOREACH_KEY(ht, h, key)
ZEND_HASH_FOREACH_PTR(ht, ptr)
ZEND_HASH_FOREACH_NUM_KEY(ht, h)
ZEND_HASH_FOREACH_STR_KEY(ht, key)
ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val)
ZEND_HASH_FOREACH_KEY_VAL(ht, h, key, val)

所以它的對應函數實現以下:

PHP_FUNCTION(hello_array_strings)
{
    ulong num_key;
    zend_string *key;
    zval *val, *arr;
    HashTable *arr_hash;
    int array_count;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &arr) == FAILURE) {
        RETURN_NULL();
    }

    arr_hash = Z_ARRVAL_P(arr);
    array_count = zend_hash_num_elements(arr_hash);
    php_printf("The array passed contains %d elements\n", array_count);
    ZEND_HASH_FOREACH_KEY_VAL(arr_hash, num_key, key, val) {
        //if (key) { //HASH_KEY_IS_STRING
        //}
        PHPWRITE(Z_STRVAL_P(val), Z_STRLEN_P(val));
        php_printf("\n");
    }ZEND_HASH_FOREACH_END();
}

由於這是新的遍歷方法,而我看的仍是php5的處理方式,調試出上面的代碼花了很多功夫,總的來講,用宏的方式遍歷大大減小了編碼體積。哈希表是php中很重要的一個內容,有時間再好好研究一下。

遍歷數組的其餘方式

遍歷 HashTable 還有其餘方法。Zend引擎針對這個任務展露了三個很是相似的函數:zend_hash_apply(), zend_hash_apply_with_argument(), zend_hash_apply_with_arguments。第一個形式僅僅遍歷HashTable,第二種形式容許傳入單個void*參數,第三種形式經過var arg列表容許數量不限的參數。hello_array_walk()展現個他們各自的行爲。

static int php_hello_array_walk(zval *ele TSRMLS_DC)
{
    zval temp = *ele; // 臨時zval,避免convert_to_string 污染原元素
    zval_copy_ctor(&temp);  // 分配新 zval 空間並複製 ele 的值
    convert_to_string(&temp); // 字符串類型轉換

    //簡單的打印
    PHPWRITE(Z_STRVAL(temp), Z_STRLEN(temp));
    php_printf("\n");
    zval_dtor(&temp); //釋放臨時的 temp
    return ZEND_HASH_APPLY_KEEP;
}

static int php_hello_array_walk_arg(zval *ele, char *greeting TSRMLS_DC)
{
    php_printf("%s", greeting);
    php_hello_array_walk(ele TSRMLS_CC);
    return ZEND_HASH_APPLY_KEEP;
}

static int php_hello_array_walk_args(zval *ele, int num_args, va_list args, zend_hash_key *hash_key)
{
    char *prefix = va_arg(args, char*);
    char *suffix = va_arg(args, char*);

    TSRMLS_FETCH();
    php_printf("%s", prefix);
    // 打印鍵值對結果
    php_printf("key is : [ ");
    if (hash_key->key) {
        PHPWRITE(ZSTR_VAL(hash_key->key), ZSTR_LEN(hash_key->key));
    } else {
        php_printf("%ld", hash_key->h);
    }
    php_printf(" ]");
    php_hello_array_walk(ele TSRMLS_CC);
    php_printf("%s\n", suffix);
    return ZEND_HASH_APPLY_KEEP;
}

用戶調用的函數:

PHP_FUNCTION(hello_array_walk)
{
    zval *arr;
    HashTable *arr_hash;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &arr) == FAILURE) {
        RETURN_NULL();
    }

    arr_hash = Z_ARRVAL_P(arr);

    //第一種遍歷 簡單遍歷各個元素
    zend_hash_apply(arr_hash, (apply_func_t)php_hello_array_walk TSRMLS_CC);
    //第二種遍歷 帶一個參數的簡單遍歷各個元素
    zend_hash_apply_with_argument(arr_hash, (apply_func_arg_t)php_hello_array_walk_arg, "Hello " TSRMLS_CC);
    //第三種遍歷 帶多參數的遍歷key->value
    zend_hash_apply_with_arguments(arr_hash, (apply_func_args_t)php_hello_array_walk_args, 2, "Hello ", "Welcome to my extension!");

    RETURN_TRUE;
}

爲了複用,在輸出值時調用php_hello_array_walk(ele TSRMLS_CC)。傳入hello_array_walk()的數組被遍歷了三次,一次不帶參數,一次帶單個參數,一次帶兩給參數。三個遍歷的函數返回了ZEND_HASH_APPLY_KEEP。這告訴zend_hash_apply()函數離開HashTable中的(當前)元素,繼續處理下一個。

這兒也能夠返回其餘值:ZEND_HASH_APPLY_REMOVE
除當前元素並繼續應用到下一個;ZEND_HASH_APPLY_STOP在當前元素停止數組的遍歷並退出zend_hash_apply()函數。

TSRMLS_FETCH() 是一個關於線程安全的動做,用於避免各線程的做用域被其餘的侵入。由於zend_hash_apply()的多線程版本用了vararg列表,tsrm_ls標記沒有傳入walk()函數。

<?php
$arr = ["99", "fff", "key1"=>"888", "key2"=>"aaa"];
hello_array_walk($arr);

20161128104657.png

相關文章
相關標籤/搜索