## 起步
到這已經能聲明簡單函數,返回靜態或者動態值了。定義INI選項,聲明內部數值或全局數值。本章節將介紹如何接收從調用腳本(php文件)傳入參數的數值,以及 PHP內核 和 Zend引擎 如何操作內部變量。
## 接收參數
與用戶控件的代碼不同,內部函數的參數實際上并不是在函數頭部聲明的,函數聲明都形如: `PHP_FUNCTION(func_name)` 的形式,參數聲明不在其中。參數的傳入是通過參數列表的地址傳入的,并且是傳入每一個函數,不論是否存在參數。
通過定義函數`hello_str()`來看一下,它將接收一個參數然后把它與問候的文本一起輸出。
```c
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中,提供另一種獲取參數的方式FAST_ZPP,是為了提高參數解析的性能。
```c
#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中沒有與它們對應的數據類型,所以不會執行類型轉換。
## Zval
一般而言,zval和php用戶空間變量是很傷腦筋的,概念很難懂。到了PHP7,它的結構在[Zend/zend_types.h](https://github.com/php/php-src/blob/master/Zend/zend_types.h)中有定義:
```c
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類型的:
```c
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](https://github.com/php/php-src/blob/master/Zend/zend_types.h)中有定義:
```c
/* 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()`:
```c
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 ](https://github.com/php/php-src/blob/master/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);
}
```

`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,其他的變量被分配到每一步迭代:
```c
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)
```
因此它的對應函數實現如下:
```c
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()`展示個他們各自的行為。
```c
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;
}
```
用戶調用的函數:
```c
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);
```
