起步
到这已经能声明简单函数,返回静态或者动态值了。定义INI选项,声明内部数值或全局数值。本章节将介绍如何接收从调用脚本(php文件)传入参数的数值,以及 PHP内核
和 Zend引擎
如何操作内部变量。
接收参数
与用户控件的代码不同,内部函数的参数实际上并不是在函数头部声明的,函数声明都形如: PHP_FUNCTION(func_name)
的形式,参数声明不在其中。参数的传入是通过参数列表的地址传入的,并且是传入每一个函数,不论是否存在参数。
通过定义函数hello_str()
来看一下,它将接收一个参数然后把它与问候的文本一起输出。
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
,是为了提高参数解析的性能。
#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中有定义:
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);
}
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);
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。