『自闭 PHP 内核』 vol1. 来写一个 PHP 扩展吧~

『自闭 PHP 内核』 vol1. 来写一个 PHP 扩展吧~

编程那点事 随便写写 PHP 4062 字 / 9 分钟

之前学 C 语言的目标,就是想有一天能去看 PHP 的底层源码。一直对 PHP 挺惊讶的,她是一个很容易上手的语言,但是却是使用 C 语言编写的。C 语言原生能实现的功能,所含的库就不多语法残废,又十分地接近系统底层;很难想象像 PHP 这么一个语法宽松随便的语言,是怎样用 C 实现的。 之前在协会分享会时,Hammer 学长曾带着我们去看了 PHP 源码中关于弱类型比较的部分,还用 gdb 一步步地跟着走了一遍。当时我确实对于 PHP 的源码产生了些兴趣。因为 C 的关键字、语法就那么些,因此感觉代码不会有语法看不懂的地方。

正如这个文章(或者说系列)的题目:自闭 PHP 内核,也就是说这东西到后面很可能会把我整自闭,然后我就咕了。 嘛,先学一点是一点吧。 比起 PHP 底层源码, PHP 的扩展开发会稍微友好一些,很多功能都有相应的宏函数或 Zend API 支持。相比直接上来啃源码,还是先来写一个 PHP 扩展吧。

编译一份 PHP

为了不搞砸现有的 PHP 环境,我开了台 Ubuntu 的虚拟机,在里面编译 PHP。(主要是 Mac 上的环境已经被我搞的乱七八糟了,PHP 编译疯狂报错 我这边的版本是从php.net上下载的 PHP 7.2.20。下载到本地:https://www.php.net/distributions/php-7.2.20.tar.gz 解压后,命令行cd到目录下。 执行:

./configure --prefix=/usr/local/php7-ext --enable-fpm

注意这里的--prefix参数,这里指定了 PHP 的安装目录,请根据自己的需要修改。 当执行完后显示Thank you for using PHP.,即成功生成 Makefile 了。若出现 error,请自行谷歌解决。 之后开始编译:

make && sudo make install

编译成功后,我的 PHP 可执行文件在/usr/local/php7-ext/bin/php。可以尝试执行/usr/local/php7-ext/bin/php -v来查看 PHP 版本。

使用ext_skel来自动构建扩展包框架

PHP 的扩展源码都是有固定的文件与目录结构的。我们可以使用ext_skel脚本,来让其生成一个基本的框架,然后我们在其上进行开发。 你可以在 PHP 源码下的ext文件夹中找到ext_skel脚本。 用ext_skel来创建一个名为Eggplant的 PHP 扩展:

./ext_skel --extname=Eggplant

之后我们的ext目录下会多出一个名为Eggplant的文件夹,里面放着的就是扩展的源码。 扩展目录文件 其中php_Eggplant.h文件中是扩展的头文件,里面放一些自定义的结构体、全局变量等。Eggplant.c就是我们干活的地方了,是扩展的主程序文件,扩展的各个函数入口都在这里。

粗略地聊聊Eggplant.c

打开Eggplant.c,可以看到ext_skel其实已经给我们写好了很多代码了,我想聊聊其中几个有意思的。 我们会看到很多诸如PHP_FUNCTION(xxxx) PHP_MINIT_FUNCTION(xxxx)这样的“函数”,这些都是 C 中的宏定义函数。这里需要明确一个概念,PHP 的扩展中,用到了很多的宏定义,例如我们想创建一个自定义的函数my_function,可以使用宏函数:

PHP_FUNCTION(my_function){
	...
}

而 PHP 在编译时,会将其变成其内部使用的名称。对于开发者而言,这确实使得代码易读了很多。后面会看到很多这样的例子。

开始编写扩展

PHP_MINFO_FUNCTION

首先是我很感兴趣的这个:PHP_MINFO_FUNCTION,它是用来设置你的扩展在phpinfo()中显示的信息。根据函数名,我们可以很轻松地猜到其中函数的用法:

PHP_MINFO_FUNCTION(Eggplant)
{
	php_info_print_table_start();		// 表格开始
	php_info_print_table_header(2, "Eggplant support", "enabled");		// 表格头部,其中 2 表示两列。之后为表格内容。
	php_info_print_table_end();			// 表格结束

	/* Remove comments if you have entries in php.ini
	DISPLAY_INI_ENTRIES();
	*/
}

除了php_info_print_table_header设置表格头外,还可以使用php_info_print_table_row添加单独的行:

php_info_print_table_row(2, "Author", "E99p1ant");

这里更多的骚操作,其实可以去看 PHP 源码中关于phpinfo()的实现。 https://github.com/php/php-src/blob/61f78de4860b951b8548b745f40caef3b5369528/ext/standard/info.c 在这里你可以看到他们是怎样设置了页面的 CSS、怎样输出数组的结构、怎样获取设备的信息等。

PHP_FUNCTION

那么下面来编写一个我们自己的函数get_eggplant,它可以根据我们的入参疯狂输出Eggplant!字符串,还可以选择是让他直接打印出来还是作为函数返回值赋值给变量。 Eggplant-扩展效果

使用PHP_FUNCTION宏定义函数来声明我们的自定义函数:

PHP_FUNCTION(get_eggplant)
{
	
}

处理入参

这个自定义函数,需要有两个入参。一个times传入一个整数,来设置输出Eggplant!的次数;还有一个可选参数backValue来选择是将值打印出来还是返回。 下面这张表给出了 PHP 中的数据类型所对应的 C 语言的实现: | 缩写 | PHP | C 实现 | 说明 | | :————: | :————: | :————: | | b | Boolean | zend_bool | 布尔 | | l | Integer | long | 整型 | | d | Floating point | double | 浮点型 | | s | String | char*, int | 字符串(前者接收指针,后者接收长度) | | r | Resource | zval* | 资源 | | a | Array | zval* | 数组 | | o | Object instance | zval* | 对象 | | O | Object instance of a specified type | zval* | 特定类型的对象 | | z | Non-specific | zval* | zval 任意类型 | | Z | zval | zval** | zval 类型 |

long times;
zend_bool backValue;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l|b", ×, &backValue) == FAILURE) {
	RETURN_NULL();
}

这里的times是整数,对应 C 语言中的是long类型,而backValue是布尔类型,对应 C 语言中是zend_bool类型。多说一点,我们可以去源码中看看关于zend_bool的定义,会发现它其实是个无符号整数。

typedef unsigned char zend_bool;

推荐一个可以方便用来查询定义的网站https://phpinternals.net/docs/

这里使用zend_parse_parameters函数来获取入参。注意这里的l|b,根据上面那张表,l表示这里第一个是整形参数,|之后的字母表示该参数可选,即含有初值。 b表示第二个可选参数是布尔类型的。

| - 表明剩下的参数都是可选参数。如果用户没有传进来这些参数值,那么这些值就会被初始化成默认值。 / - 表明参数解析函数将会对剩下的参数以SEPARATE_ZVAL_IF_NOT_REF()的方式来提供这个参数的一份拷贝,除非这些参数是一个引用。 ! - 表明剩下的参数允许被设定为NULL(仅用在aoOrz身上)。如果用户传进来了一个NULL值,则存储该参数的变量将会设置为NULL

"l|b"之后便是传入相应参数所对应的指针,来接受数据。 这里最后还将zend_parse_parameters的返回值与宏定义FAILURE进行比较,来判断函数入参是否正确。若不正确,函数则返回NULL,且同时 PHP 会报错。

主要代码

zend_string *backStr = zend_string_init("", 0, 0);

zend_string *eggplant = zend_string_init("Eggplant! ", strlen("Eggplant! "), 0);

for(long i = 0; i < times; i++){
	backStr = strpprintf(0, "%s%s", ZSTR_VAL(backStr), ZSTR_VAL(eggplant));
}
if(backValue == 1){
	php_printf("%s", ZSTR_VAL(backStr));
	RETURN_TRUE;
}else{
	RETURN_STR(backStr);
}

这里就是这个函数的主要代码了。首先,先定义了一个zend_string类型的指针变量backStr来存储最后的结果字符串。 zend_string其实是一个结构体:

struct _zend_string {
    zend_refcounted_h gc;		// 与 PHP 垃圾回收(GC)相关联使用的字符串
    zend_ulong        h;		// 字符串的哈希值,在构建哈希表缓存时会用到
    size_t            len;		// 字符串的长度,感觉可能就是用这里的长度而不用自带的 strlen 来实现字符串的二进制安全。可以使用宏函数 ZSTR_LEN 获取。
    char              val[1];		// 字符串的值,可以使用宏函数 ZSTR_VAL 获取。
};

通过查阅zend_string类型的相关函数文档,我们可以使用zend_string_init函数来给zend_string赋初始值。就像上面的代码那样。 之后写了个循环,使用strpprintf函数来格式化拼接字符串,这里就用到了宏函数ZSTR_VAL来获取字符串变量的值。

最后判断backValue,来确定对结果的处理。使用php_printf函数来格式化打印内容。这个函数在 CLI 模式下是直接打印到命令行中,在 FPM 模式下则是输出到网页中。当用户选择将结果打印时,就使用RETURN_TRUE来返回一个true(也就是1);否则使用RETURN_STR返回字符串结果。

定义参数信息

关于函数的入参,也是需要使用宏函数进行定义的:

ZEND_BEGIN_ARG_INFO(arginfo_get_eggplant, 0)
ZEND_END_ARG_INFO()

关于这一块我还没有做过多的了解。

使用PHP_FE宏来将参数信息添加到函数中

之后,我们还需要使用PHP_FE宏函数将刚才定义的参数信息arginfo_get_eggplant来添加到函数中:

const zend_function_entry Eggplant_functions[] = {
	PHP_FE(confirm_Eggplant_compiled, NULL)		/* For testing, remove later. */
	PHP_FE(get_eggplant, arginfo_get_eggplant)
	PHP_FE_END	/* Must be the last line in Eggplant_functions[] */
};

至此,整个get_eggplant函数的编写就完成了! 下面就可以来将我们的扩展安装到 PHP 中了!

编译扩展

在扩展目录下,执行你 PHP 安装目录下的phpize

/usr/local/php7-test/bin/phpize

然后执行:

./configure --with-php-config=/usr/local/php7-ext/bin/php-config

有坑注意 这里有个小坑,这边的参数`--with-php-config`,其`config`指的不是 PHP 的配置文件`php.ini`,而是安装目录下的`php-config`文件。
最后编译插件:

make && sudo make install

最后会输出扩展编译后所存放的位置:/usr/local/php7-test/lib/php/extensions/no-debug-non-zts-20170718/

开启扩展

编译完扩展后,我们需要在 PHP 的配置文件php.ini中设置开启扩展。我们自行编译的 PHP 中是不自带php.ini文件的,需要我们自行创建。 通过查看phpinfo()我们可以得知 PHP 会默认尝试去加载/usr/local/php7-test/lib下的php.ini文件。 在/usr/local/php7-test/lib下创建php.ini文件:

extension=Eggplant.so

保存后,我们可以用内置服务器搭一个站,看一下phpinfo(),当看到有Eggplant扩展时,即代表扩展已经加载进来了。 尝试输入:

<?php
$str = get_eggplant(5, false);
echo($str);

即可看到页面上输出了 5 个Eggplant!,无论是在网页上还是命令行模式上都能使用。

源码:https://github.com/wuhan005/PHP_Extension_Demo

总结一下吧~

结合上面的步骤,我还写了get_eggplant()get_any()两个函数。分别是可以输出茄子的 emoji 🍆,以及可以输出任意次数的任意字符串。虽然都是些比较鸡肋的功能,但重点还是在于学习关于扩展的编写嘛。 可以说 PHP 已经尽可能地让扩展开发者更舒服的开发扩展。字符串拼接那里给我的印象十分深刻。原生的 C 还需要自己手动 malloc 内存,而在 PHP 扩展里面,只需要使用 Zend 自带的字符串类型zend_string,就可以有一堆函数方法来轻松赋值、拼接字符串。

这一波操作下来,虽然没啥太大的坑,但我深深地感觉自己的 C 语言还是太菜了……