企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
## 起步 到这已经能声明简单函数,返回静态或者动态值了。定义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); } ``` ![](https://box.kancloud.cn/f827127c8eea2473e1cf9dd8dac8a860_633x256.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,其他的变量被分配到每一步迭代: ```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); ``` ![](https://box.kancloud.cn/42ec46e0876a623f9b419f53739e024b_481x258.png)