『自闭 PHP 内核』 vol1. 来写一个 PHP 扩展吧~
之前学 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!
字符串,还可以选择是让他直接打印出来还是作为函数返回值赋值给变量。
使用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
(仅用在a
、o
、O
、r
和z
身上)。如果用户传进来了一个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
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!
,无论是在网页上还是命令行模式上都能使用。
总结一下吧~
结合上面的步骤,我还写了get_eggplant()
和get_any()
两个函数。分别是可以输出茄子的 emoji 🍆,以及可以输出任意次数的任意字符串。虽然都是些比较鸡肋的功能,但重点还是在于学习关于扩展的编写嘛。
可以说 PHP 已经尽可能地让扩展开发者更舒服的开发扩展。字符串拼接那里给我的印象十分深刻。原生的 C 还需要自己手动 malloc 内存,而在 PHP 扩展里面,只需要使用 Zend 自带的字符串类型zend_string
,就可以有一堆函数方法来轻松赋值、拼接字符串。
这一波操作下来,虽然没啥太大的坑,但我深深地感觉自己的 C 语言还是太菜了……
喜欢这篇文章?为什么不打赏一下呢?