手把手撸PHP扩展 0x06: 协程创建(二)

时间:2022-06-26
本文章向大家介绍手把手撸PHP扩展 0x06: 协程创建(二),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

这一篇文章,我们来实现一下我们创建协程的接口。

首先,我们需要对传给接口的参数进行解析。解析参数需要使用PHP提供给我们的宏来完成,分别是开头的和结尾的宏:

1、ZEND_PARSE_PARAMETERS_START
2、ZEND_PARSE_PARAMETERS_START_EX
3、ZEND_PARSE_PARAMETERS_END
4、ZEND_PARSE_PARAMETERS_END_EX

(其中,末尾的EXextended的缩写)

乍眼一看,好像这四个宏是两对,1、3一对,2、4一对。实际上这四个宏并不需要成对的使用。因为ZEND_PARSE_PARAMETERS_START只是ZEND_PARSE_PARAMETERS_START_EX的一种特殊情况,同理ZEND_PARSE_PARAMETERS_END也是ZEND_PARSE_PARAMETERS_END_EX的一种特殊情况。我们展开来看这几个宏比较直观。

ZEND_PARSE_PARAMETERS_START_EX

#define ZEND_PARSE_PARAMETERS_START_EX(flags, min_num_args, max_num_args) do { 
		const int _flags = (flags); 
		int _min_num_args = (min_num_args); 
		int _max_num_args = (max_num_args); 
		int _num_args = EX_NUM_ARGS(); 
		int _i; 
		zval *_real_arg, *_arg = NULL; 
		zend_expected_type _expected_type = Z_EXPECTED_LONG; 
		char *_error = NULL; 
		zend_bool _dummy; 
		zend_bool _optional = 0; 
		int error_code = ZPP_ERROR_OK; 
		((void)_i); 
		((void)_real_arg); 
		((void)_arg); 
		((void)_expected_type); 
		((void)_error); 
		((void)_dummy); 
		((void)_optional); 
		
		do { 
			if (UNEXPECTED(_num_args < _min_num_args) || 
			    (UNEXPECTED(_num_args > _max_num_args) && 
			     EXPECTED(_max_num_args >= 0))) { 
				if (!(_flags & ZEND_PARSE_PARAMS_QUIET)) { 
					if (_flags & ZEND_PARSE_PARAMS_THROW) { 
						zend_wrong_parameters_count_exception(_min_num_args, _max_num_args); 
					} else { 
						zend_wrong_parameters_count_error(_min_num_args, _max_num_args); 
					} 
				} 
				error_code = ZPP_ERROR_FAILURE; 
				break; 
			} 
			_i = 0; 
			_real_arg = ZEND_CALL_ARG(execute_data, 0);
#define ZEND_PARSE_PARAMETERS_START(min_num_args, max_num_args) 
	ZEND_PARSE_PARAMETERS_START_EX(0, min_num_args, max_num_args)

可以看到,ZEND_PARSE_PARAMETERS_START实际上就是让ZEND_PARSE_PARAMETERS_START_EX的第一个值默认为0了。我们重点来看看ZEND_PARSE_PARAMETERS_START_EX的三个参数:

flags:更改ZEND_PARSE_PARAMETERS_START的默认行为。flags可取的值有ZEND_PARSE_PARAMS_QUIETZEND_PARSE_PARAMS_THROW。我们来看看ZEND_PARSE_PARAMETERS_START_EX展开后与flags有关的地方:

if (UNEXPECTED(_num_args < _min_num_args) || 
			    (UNEXPECTED(_num_args > _max_num_args) && 
			     EXPECTED(_max_num_args >= 0))) { 
				if (!(_flags & ZEND_PARSE_PARAMS_QUIET)) { 
					if (_flags & ZEND_PARSE_PARAMS_THROW) { 
						zend_wrong_parameters_count_exception(_min_num_args, _max_num_args); 
					} else { 
						zend_wrong_parameters_count_error(_min_num_args, _max_num_args); 
					} 
				} 
				error_code = ZPP_ERROR_FAILURE; 
				break; 
			} 

可以发现:

UNEXPECTED(_num_args < _min_num_args) || 
			    (UNEXPECTED(_num_args > _max_num_args) && 
			     EXPECTED(_max_num_args >= 0))

实际上就是判断传递个接口的参数个数合法,如果不合法就进入这个if分支。接着,

!(_flags & ZEND_PARSE_PARAMS_QUIET)

是判断flags是否设置了ZEND_PARSE_PARAMS_QUIET。如果没有设置ZEND_PARSE_PARAMS_QUIET,则往这个if分支走。然后:

_flags & ZEND_PARSE_PARAMS_THROW

是判断flags是否设置了ZEND_PARSE_PARAMS_THROW,如果设置了ZEND_PARSE_PARAMS_THROW,那么就抛出一个参数个数不对的异常,否则就报一个error

那么,如果flagsZEND_PARSE_PARAMS_QUIET的话,及时参数个数传递错误,也不会报错。

ZEND_PARSE_PARAMETERS_END_EX

#define ZEND_PARSE_PARAMETERS_END_EX(failure) 
		} while (0); 
		if (UNEXPECTED(error_code != ZPP_ERROR_OK)) { 
			if (!(_flags & ZEND_PARSE_PARAMS_QUIET)) { 
				if (error_code == ZPP_ERROR_WRONG_CALLBACK) { 
					if (_flags & ZEND_PARSE_PARAMS_THROW) { 
						zend_wrong_callback_exception(_i, _error); 
					} else { 
						zend_wrong_callback_error(_i, _error); 
					} 
				} else if (error_code == ZPP_ERROR_WRONG_CLASS) { 
					if (_flags & ZEND_PARSE_PARAMS_THROW) { 
						zend_wrong_parameter_class_exception(_i, _error, _arg); 
					} else { 
						zend_wrong_parameter_class_error(_i, _error, _arg); 
					} 
				} else if (error_code == ZPP_ERROR_WRONG_ARG) { 
					if (_flags & ZEND_PARSE_PARAMS_THROW) { 
						zend_wrong_parameter_type_exception(_i, _expected_type, _arg); 
					} else { 
						zend_wrong_parameter_type_error(_i, _expected_type, _arg); 
					} 
				} 
			} 
			failure; 
		} 
	} while (0)
#define ZEND_PARSE_PARAMETERS_END() 
	ZEND_PARSE_PARAMETERS_END_EX(return)
		} while (0);

是对ZEND_PARSE_PARAMETERS_START_EX或者ZEND_PARSE_PARAMETERS_START的那个do {进行闭合。error_code是在真正去解析参数本身是否合法的宏里面设置的,如果参数个数传递正确的情况下并且每个参数本身都是合法的,就不会进入后面的代码了,直接到了ZEND_PARSE_PARAMETERS_END()后面的代码。如果参数本身不合法,例如本来是要接收一个整型,但是传递了一个数组,那么就会设置error_code为对应的值。然后逐个if进行判断,抛出对应的异常。

而这个failure则是我们可以在error_code不为ZPP_ERROR_OK的时候,做一些操作。例如,返回一个falsePHP脚本。反正,你可以在这里做任何的操作。

实现参数解析

有了前面的基础,解析参数的代码就好理解多了:

PHP_METHOD(study_coroutine_util, create)
{
    zend_fcall_info fci = empty_fcall_info;
    zend_fcall_info_cache fcc = empty_fcall_info_cache;

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_FUNC(fci, fcc)
    ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);
}

zend_fcall_info就是用来接收我们创建协程的时候传递的那个函数。我们使用ZEND_PARSE_PARAMETERS_END_EX的原因是因为我们希望在解析参数失败的时候,会向PHP返回一个false

RETURN_FALSE展开如下:

#define RETURN_FALSE  					{ RETVAL_FALSE; return; }
#define RETVAL_FALSE  					ZVAL_FALSE(return_value)

所以,RETURN_FALSE就是设置了变量return_value的值为false,然后在从这个接口return。但是,不是returnPHP脚本里面。原因很简单,是虚拟机去解析的PHP代码。

编译测试

我们编写如下PHP脚本:

<?php

StudyCoroutine::create();

然后编译、安装扩展:

~/codeDir/cppCode/study # ./make.sh

然后,执行脚本:

~/codeDir/cppCode/study # php test.php

Warning: StudyCoroutine::create() expects exactly 1 parameter, 0 given in /root/codeDir/cppCode/study/test.php on line 3
~/codeDir/cppCode/study # 

给出了warning,说是需要传递一个参数。

然后再修改PHP脚本:

<?php

StudyCoroutine::create(1);

然后执行:

~/codeDir/cppCode/study # php test.php

Fatal error: Uncaught TypeError: Argument 1 passed to StudyCoroutine::create() must be callable, int given in /root/codeDir/cppCode/study/test.php:3
Stack trace:
#0 /root/codeDir/cppCode/study/test.php(3): StudyCoroutine::create(1)
#1 {main}
  thrown in /root/codeDir/cppCode/study/test.php on line 3
~/codeDir/cppCode/study # 

给出了Fetal,说是参数必须是可调用的,int被传递了。

再次修改脚本:

<?php

StudyCoroutine::create(function() {
    echo 1;
});

执行:

~/codeDir/cppCode/study # php test.php
~/codeDir/cppCode/study # 

没报错,参数解析成功。