在编写PHP扩展时,最基础也是最重要的一个环节就是获取用户在调用函数时传递进来的参数。PHP层面的变量在C语言里以zval结构体存在,扩展函数需要把这些zval转换成C语言可以直接使用的基础类型,这个过程主要依赖Zend引擎提供的zend_parse_parameters系列API。本文将结合实例详细介绍几种常用的参数获取方式,并分析各自适用场景。

一、使用zend_parse_parameters解析基础类型参数
这是最标准也是最推荐的方式。每个扩展函数的第一参数double类型,第二个参数是格式化字符串,用来声明期望接收的参数类型,后面跟上对应C语言变量的地址。Zend引擎会自动完成类型检查和类型转换,比如用户传了一个字符串"123",而你期望的是long类型,引擎会尝试做隐式转换。
下面是一个完整的示例,演示如何获取一个整型和一个字符串参数:
PHP_FUNCTION(my_func)
{
zend_long num;
char *str;
size_t str_len;
// "l" 表示zend_long,"s"表示字符串(同时获得长度)
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ls", &num, &str, &str_len) == FAILURE) {
return;
}
php_printf("num = " ZEND_LONG_FMT ", str = %.*s\n", num, (int)str_len, str);
}
格式化字符串里的每个字符对应一种类型:l对应zend_long,d对应double,s对应字符串(需要额外传入size_t指针接收长度),b对应布尔值,r对应资源,a对应数组,o对应对象,z对应任意zval。需要注意字符串必须使用%.*s配合长度输出,因为PHP字符串中间可能包含二进制零字符,不能依赖C字符串的\0结尾。
这种方式的优点是安全性高,引擎会做严格的边界检查和类型校验,失败时自动抛出参数个数或类型错误警告,开发者不需要手写错误提示。缺点是灵活性稍差,遇到复杂的参数结构时格式化字符串会变得很长,可读性下降。
二、判断参数个数与可选参数的处理
真实场景中函数经常有可选参数,比如substr($str, $start, $length)的第三个参数可以省略。这时可以在格式化字符串中使用竖线|分隔必选参数和可选参数,竖线之后的参数如果没有传递,对应的C变量不会被赋值,因此务必在调用前初始化默认值。
PHP_FUNCTION(my_substr_like)
{
char *str;
size_t str_len;
zend_long start;
zend_long length = -1; // 默认值必须提前设置
if (zend_parse_parameters(ZEND_NUM_ARGS(), "sl|l", &str, &str_len, &start, &length) == FAILURE) {
return;
}
// 如果第三个参数未传递,length 保持 -1
php_printf("length = " ZEND_LONG_FMT "\n", length);
}
ZEND_NUM_ARGS()宏返回实际传入的参数个数,如果需要更精细的控制,比如参数个数不同走不同逻辑分支,可以先用它做判断再分别解析。此外还有ZEND_PARSE_PARAMETERS_*开头的快速版本,例如ZEND_PARSE_PARAMETERS_START与ZEND_PARSE_PARAMETERS_END配对的宏,这是PHP7之后新增的解析方式,编译期展开后性能更好,内核中大量使用:
PHP_FUNCTION(my_fast_parse)
{
zend_long a, b;
ZEND_PARSE_PARAMETERS_START(2, 2) // 最少2个,最多2个
Z_PARAM_LONG(a)
Z_PARAM_LONG(b)
ZEND_PARSE_PARAMETERS_END();
RETURN_LONG(a + b);
}
这套宏的优点是每个参数一个Z_PARAM_XXX条目,一眼就能看清参数含义,还支持Z_PARAM_OPTIONAL标记后续参数为可选,以及Z_PARAM_ARRAY、Z_PARAM_STRING等细分指令,写复杂签名时代码比格式化字符串清晰不少。
三、直接操作zval获取数组与复杂参数
当需要拿到参数原始的zval结构时,可以用格式字符z。这种方式不做任何类型转换,适合需要自己处理数组、对象等复杂结构的场合。拿到数组zval后,通常借助zend_hash系列函数遍历其中的元素。
PHP_FUNCTION(my_array_sum)
{
zval *arr;
// "z" 直接拿到 zval 指针,第二个参数设为0表示不需要副本语义
if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &arr) == FAILURE) {
return;
}
if (Z_TYPE_P(arr) != IS_ARRAY) {
zend_type_error("参数必须是数组");
return;
}
zend_long sum = 0;
zval *val;
ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(arr), val) {
if (Z_TYPE_P(val) == IS_LONG) {
sum += Z_LVAL_P(val);
}
} ZEND_HASH_FOREACH_END();
RETURN_LONG(sum);
}
上面的例子中,Z_TYPE_P判断zval的实际类型,Z_LVAL_P取出整数值,ZEND_HASH_FOREACH_VAL是遍历哈希表的语法糖,比手动调用zend_hash_get_current_data简洁得多。如果格式化字符串用a而不是z,引擎会强制要求参数必须是数组,传其他类型直接报错,省去了手动校验的步骤。
还有一种场景是接收引用参数,典型如sort函数会修改原数组。这需要在函数声明时使用arginfo声明by_ref,同时在参数声明中用ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX宏把对应参数标记为引用,解析时使用Z_PARAM_ARRAY_EX并设置引用标志,引擎才能正确传递引用而不是值拷贝。
四、参数校验的常见坑点与最佳实践
第一个常见的坑是字符串长度被忽略。不少初学者拿到char *之后直接用strlen计算长度,一旦用户传入的字符串中含有\0,后半段数据就丢了。正确做法是始终使用zend_parse_parameters返回的长度值,所有对字符串的截取、比较都基于这个长度进行。
第二个坑是浮点数精度。格式字符d会把数值转换成double,而l对应的是zend_long。在64位系统上zend_long可以表示很大的整数,如果用户传入超过精度的整数却用d去接收,会丢失精度。所以处理ID、时间戳这类字段时坚持用l。
第三个坑是修改传入的数组或字符串。如果直接修改通过z接收的zval,可能影响到用户原始变量,特别是在写时复制的语义下行为难以预期。稳妥的做法是先调用zval_copy_ctor或者用separate_array确保拿到独立副本后再修改。
最后建议养成一个习惯:解析参数后立即做业务校验,比如长度为0的字符串、负数索引、空数组等边界情况尽早return并给出明确的错误信息。同时善用arginfo为函数声明参数类型,这样从PHP 7.4开始可以开启严格类型检查,把一部分校验工作交给引擎,扩展代码会更简洁也更不容易出错。调试阶段可以配合zend_error或php_error_docref输出诊断信息,方便定位参数到底在哪一步出了问题。
PHP扩展请求参数zend_parse_parameters修改时间:2026-09-15 22:58:39