手把手撸PHP扩展 0x05: 协程创建(一)

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

协程相关结构定义

首先,我们需要一个PHP可用的协程,根据梳理一下架构这篇文章的内容,我们需要在study_coroutine.h里面来定义:

#include "php_study.h"

namespace Study
{
class PHPCoroutine
{
    static long create(zend_fcall_info_cache *fci_cache, uint32_t argc, zval *argv);
};
}

然后,我们还需要一个与PHP无关的协程数据结构,我们定义在include/coroutine.h下面:

#ifndef COROUTINE_H
#define COROUTINE_H

namespace Study
{
class Coroutine
{
    static long create(coroutine_func_t fn, void* args = nullptr);
};
}

#endif	/* COROUTINE_H */

coroutine_func_t是协程需要跑的函数,我们可以定义在include/context.h下面:

#ifndef CONTEXT_H
#define CONTEXT_H

typedef void (*coroutine_func_t)(void*);

#endif	/* CONTEXT_H */

它定义了一个指向函数的指针类型,返回值是void,参数是一个void *指针。

然后,我们在include/coroutine.h中引入这个context.h文件:

#include "context.h"

协程接口参数声明

OK,此时,我们需要为PHP脚本提供一个创建协程的接口,我们在文件study_coroutine_util.cc里面来完成。首先,我们需要确定一下这个接口的参数是什么,很显然,是一个PHP函数:

#include "study_coroutine.h"

ZEND_BEGIN_ARG_INFO_EX(arginfo_study_coroutine_create, 0, 0, 1)
    ZEND_ARG_CALLABLE_INFO(0, func, 0)
ZEND_END_ARG_INFO()

其中ZEND_BEGIN_ARG_INFO_EXZEND_END_ARG_INFO是一对宏,用来声明函数接受的参数。其中ZEND_BEGIN_ARG_INFO_EX展开如下:

#define ZEND_BEGIN_ARG_INFO_EX(name, _unused, return_reference, required_num_args)	
	static const zend_internal_arg_info name[] = { 
		{ (const char*)(zend_uintptr_t)(required_num_args), 0, return_reference, 0 },

name是参数的的名字。

_unused我们不用管,因为ZEND_BEGIN_ARG_INFO_EX展开之后,并没有用到_unused

return_reference表示是否返回引用。

required_num_args表示这个函数最少需要传递的参数个数。

ZEND_ARG_CALLABLE_INFO用来显式声明参数为callable,将检查函数、成员方法是否可调用。

ZEND_END_ARG_INFO展开如下:

#define ZEND_END_ARG_INFO()		};

因此,我们对这个参数声明展开后,会得到如下内容:

ZEND_BEGIN_ARG_INFO_EX(arginfo_study_coroutine_create, 0, 0, 1)
    ZEND_ARG_CALLABLE_INFO(0, func, 0)
ZEND_END_ARG_INFO()
  
static const zend_internal_arg_info arginfo_study_coroutine_create[] = { 
		{ (const char*)(zend_uintptr_t)(1), 0, 0, 0 },
    ZEND_ARG_CALLABLE_INFO(0, func, 0)
};

所以,虽然我在

ZEND_BEGIN_ARG_INFO_EX(arginfo_study_coroutine_create, 0, 0, 1)

中写下了单词arginfostudy_coroutine_create,似乎一定是要这样写,但是,我们把ZEND_BEGIN_ARG_INFO_EX宏展开之后会发现,arginfo_study_coroutine_create只是一个变量名而已。因此,我这里这样组合这个变量是为了可读性更好,并不是一定要这样声明这个参数,这一点大家需要去注意。

协程接口方法声明

然后,我们需要在文件study_coroutine_util.cc里面去声明这个方法:

static PHP_METHOD(study_coroutine_util, create);

PHP_METHOD展开之后的内容如下:

#define PHP_METHOD  			ZEND_METHOD
#define ZEND_METHOD(classname, name)	ZEND_NAMED_FUNCTION(ZEND_MN(classname##_##name))
#define ZEND_MN(name) zim_##name
#define ZEND_NAMED_FUNCTION(name)		void ZEND_FASTCALL name(INTERNAL_FUNCTION_PARAMETERS)#define INTERNAL_FUNCTION_PARAMETERS zend_execute_data *execute_data, zval *return_value

所以,接口方法展开的内容如下:

PHP_METHOD(study_coroutine_util, create);
ZEND_METHOD(study_coroutine_util, create);
ZEND_NAMED_FUNCTION(ZEND_MN(study_coroutine_util##_##create));
ZEND_NAMED_FUNCTION(zim_##study_coroutine_util##_##create);
void ZEND_FASTCALL zim_##study_coroutine_util##_##create(INTERNAL_FUNCTION_PARAMETERS);
void ZEND_FASTCALL zim_##study_coroutine_util##_##create(zend_execute_data *execute_data, zval *return_value);

static PHP_METHOD(study_coroutine_util, create);相当于声明了如下函数:

void zim_study_coroutine_util_create(zend_execute_data *execute_data, zval *return_value);

(其中,zimzend internal method的缩写)

通过对接口方法的展开,我们发现,虽然接口命名是单词study_coroutine_utilcreate,似乎必须得是真正的类名加上方法名。其实不然,这里也只是为了可读性更好。

我们还可以对比一下PHP_FUNCTION这个宏,实际上,它和PHP_METHOD的一个区别就是少拼接了classname

协程接口实现

我们在study_coroutine_util.cc文件里面写下:

PHP_METHOD(study_coroutine_util, create)
{
    php_printf("success!n");
}

因为文章篇幅的原因,我们这里简单实现。

协程接口收集

接着,我们需要对这个方法进行收集,放在变量study_coroutine_util_methods里面。在study_coroutine_util.cc写下:

const zend_function_entry study_coroutine_util_methods[] =
{
    PHP_ME(study_coroutine_util, create, arginfo_study_coroutine_create, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC)
    PHP_FE_END
};

这里我们使用到一个数据结构:

typedef struct _zend_function_entry {
	const char *fname;
	zif_handler handler;
	const struct _zend_internal_arg_info *arg_info;
	uint32_t num_args;
	uint32_t flags;
} zend_function_entry;

fname是函数的名字,对应的是PHP_ME的第二个参数,Zend引擎将会创建一个包含函数名fnameinterned zend_string。在这里,是create。这个fname是我们可以在PHP脚本中使用的。而PHP_ME的第一个参数study_coroutine_util是为了拼凑出:

PHP_METHOD(study_coroutine_util, create)

声明的函数:

zim_study_coroutine_util_create

handler是一个函数指针,也就是该函数的主体。那么是什么样的函数指针呢?我们得看看前面的zif_handler

/* zend_internal_function_handler */
typedef void (ZEND_FASTCALL *zif_handler)(INTERNAL_FUNCTION_PARAMETERS);

可以发现,这是一个没有返回值,参数是INTERNAL_FUNCTION_PARAMETERS的函数。这个其实和PHP_METHOD这个宏展开得到的函数声明类型是一致的。在这里,这个handler存放的就是函数指针zim_study_coroutine_util_create

arg_info是这个接口方法对应的参数。可以发现,它实际上就是我们上面的那个参数展开后的类型。所以这里很明显,我们必须填写arginfo_study_coroutine_create,也就是我们参数展开后定义的那个变量。

num_args是接口方法的参数个数。可以发现,我们这里并没有填写参数的个数,实际上,这个参数的个数会通过宏ZEND_FENTRY来计算出来:

#define ZEND_FENTRY(zend_name, name, arg_info, flags)	{ #zend_name, name, arg_info, (uint32_t) (sizeof(arg_info)/sizeof(struct _zend_internal_arg_info)-1), flags },

flags是标志。例如这个接口方法是publicstatic的。

协程类注册

然后,我们需要去注册我们的StudyCoroutine这个类。我们在MINIT这个阶段进行注册,代码如下:

zend_class_entry study_coroutine_ce;
zend_class_entry *study_coroutine_ce_ptr;

PHP_MINIT_FUNCTION(study)
{
	INIT_NS_CLASS_ENTRY(study_coroutine_ce, "Study", "Coroutine", study_coroutine_util_methods);

	return SUCCESS;
}

但是,考虑到以后我们会有许多的类,我们不在MINIT里面直接写注册的代码,而是让study_coroutine_util.cc提供一个函数,我们在这个函数里面实现注册功能:

/**
 * Define zend class entry
 */
zend_class_entry study_coroutine_ce;
zend_class_entry *study_coroutine_ce_ptr;

void study_coroutine_util_init()
{
	INIT_NS_CLASS_ENTRY(study_coroutine_ce, "Study", "Coroutine", study_coroutine_util_methods);
  study_coroutine_ce_ptr = zend_register_internal_class(&study_coroutine_ce TSRMLS_CC); // Registered in the Zend Engine
}

然后,我们在php_study.h里面来进行声明:

void study_coroutine_util_init();

然后,我们在MINIT中对这个函数进行调用,完成类的注册:

PHP_MINIT_FUNCTION(study)
{
	study_coroutine_util_init();
	return SUCCESS;
}

编译测试

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

编写测试脚本:

<?php

StudyCoroutine::create();
~/codeDir/cppCode/study # php test.php
success!
~/codeDir/cppCode/study # 

OK,到这里,我们算是完成了协程创建接口的前期工作。

下一篇:协程创建(二)

----------伟大的分割线-----------

PHP饭米粒(phpfamily) 由一群靠谱的人建立,愿为PHPer带来一些值得细细品味的精神食粮!

饭米粒只发原创或授权发表的文章,不转载网上的文章

所发的文章,均可找到原作者进行沟通。