用php-x写php扩展

最近工作中碰到了php扩展开发的需求,所以做了一些相关的学习,在此把碰到的问题简单记录下来。

需求来源

本次开发的目标是『调用链trace系统的sdk』,之前的代码是php代码实现的,对性能影响较大,影响了接口的处理能力,所以考虑改为扩展实现。

另外,php的进程模型是fpm,每次请求执行结束就释放了所有资源,因此只能通过扩展来实现常驻内存的设计。

制定目标

将现有sdk迁移至php扩展实现,同时php升级至php7。

准备工作

我对PHP扩展开发是0基础,但是有c/c++功底,这是开发php扩展的必要前提。

为了快速了解php扩展的原理,我快速的扫了一下这个教程:《PHP扩展开发及内核应用》,对PHP扩展的知识体系有了初步认识。

在后续开发中,实际发现PHP7的Zend api有不少重要的调整,因此又回头读了一遍官方发布的《升级至PHP7》,大概了解PHP7的主要数据结构和接口变化。

PHP-X

开发php扩展需要用到zend api,也就是php的核心类库,它是c语言实现的,然而zend api真的非常庞杂,不同的数据结构对应若干长相类似的函数,又对应着若干宏定义来简化函数,初次接触上手难度还是挺高的。

好在已经有开源项目把这些数据结构进一步封装成C++的类,并提供非常简单友好的类方法操作数据,我这里使用的『php-x项目』,他是swoole项目的作者开源的,对php扩展开发有丰富经验。

另外一个重要意义在于,php-x通过封装可以让我们更加系统的学习zend api,算是一本教科书。

你可以下载《韩天峰——使用C++开发PHP7扩展.pdf》来快速的了解PHP-X项目,是个很有想法的项目。

启动项目

下载php-x代码成功编译后,可以将examples/cpp_ext项目作为模板,启动你的扩展开发工作。

因为扩展是c/c++编写的,所以难免有内存忘记释放或者重复释放等问题,所以一定要重新编译你的php7,开启它的debug模式(–enable-debug)。

另外如果php7开启了Opcache,一定要配置php.ini添加如下配置,当扩展试图修改来自zend解释器的内存时会报错:

另外,记得配置php.ini让其加载你的扩展so。

迭代方式

为了可以随时验证扩展是否正常工作,需要做好2件事情:

  • 每次编译产生的扩展so保存的路径最好与php.ini中配置的路径一致。
  • 利用命令行执行php脚本来调用扩展,因为php-fpm需要重启才能加载新的so。

扩展通常会遇到2类内存问题,追查的方式不同:

  • 从zend申请的内存没有释放/多次释放,这种在脚本执行结束后会打印错误到命令行,但是并不会对应到源码的行号,比较难排查。
  • 非zend申请的内存没有释放/多次释放,这种情况下php并不知情,因此也不会报错,需要用valgrind检测内存问题。

根据我的经验,为了尽量减少排错耗费的时间和精力,在编写扩展时应该步步为营,即采用MVP(最小可用产品)原则,每开发完一小块逻辑单元,就重新编译扩展并执行php脚本进行验证,如果报错那么一定说明是最近添加的代码引起,排错就很简单了。

debug方法

如果你已经遵循MVP原则开发扩展,但是仍旧无法定位内存报错发生在哪个环节,可以采用2种方法:

  • 这种方法最简单,也就是利用return提前返回函数,跳过你怀疑可能有内存问题的代码,逐步缩小排查范围。
  • 采用valgrind检查内存问题,因为默认zend自带内存池,并对分配的对象采用引用计数维护其生命期,所以直接用valgrind追踪只会定位到zend自身而不是你的扩展。为了解决这个问题,可以参考《内存泄露》,通过export导出环境变量USE_ZEND_ALLOC=0即可关闭zend内存池,此时valgrind可以准确定位到泄露的内存是从哪里分配而来。

除了内存问题,极有可能需要使用GDB跟踪代码执行来分析问题。

php-x项目使用了c++11,因此要求gcc > 4.8,在实际gdb跟踪项目的时候遇到了2个问题,大家应该也会遇到。

  1. 找不到扩展的源代码:因为gdb挂载的是php自身,再由php自身去加载扩展so,所以默认gdb只能break断点到php自身的源代码路径,找不到我们的扩展代码在哪里。需要在gdb中通过dir命令指定一下扩展的项目路径,指定后需要执行一次脚本,也就是在gdb中r test.php,这样php会加载一次我们的扩展so并根据dir指定的路径将对应的代码找到,此后就可以break断点到扩展源代码了。
  2. 看不到局部变量:通过bt命令虽然可以看到调用堆栈,但是p打印变量或者info locals都看不到任何变量具体内容。这是由于gcc4.8+引起的一个问题,需要在编译扩展时携带额外的参数(-gdwarf-2),具体可以参考《c++ debugging with gdb》。

如果对Zend api不熟悉,导致问题难以排查,那么给你2个思路:

  • 看php源码,因为网上对zend api的介绍少之又少。
  • 去github里,搜索框里输入api名称,可以在code标签下看到github里所有用到这个api的项目,学习它们怎么使用。

php-x的问题

在使用php-x过程中,我有几个感受:

  • 极容易上手:我在对zend api一知半解的情况下直接开发,也不会遇到太多的问题。
  • 晦涩的引用计数:php-x对zval的引用计数封装不是清晰一致的,类对象之间赋值和拷贝让人很不放心,导致我在开发过程中不停的翻看php-x的实现才能避免出现问题。
  • 潜伏bug:php-x是一个新项目(当前我是除了作者外唯一的贡献者),我在使用中遇到了几处bug,其他没有用到的功能肯定还有潜伏的问题。我发现bug后先自己修复,然后把bugfix通过pull request给到了作者,如果期望作者来发现和修复问题,那项目周期就不可控了。
  • 使用者少:与上一个问题直接相关,没有诸多的使用者,项目问题就暴露的慢,你要踩的坑就会多。

我认为后2个问题倒不是最重要的,只要公司的研发团队有能力处理也问题不大。

第2个问题是我比较头疼的,这需要了解到作者当初的设计思路,才能知其然之气所以然。

我认为既然采用c++11,那么可以考虑把move语义用起来,将拷贝和移动行为明确的分割开来设计,对资源引用计数提供一个更可信赖的机制,目前我对zend api了解还不够深入,也只能等待和作者交流一下思想了。

大家有PHP-X和扩展开发的问题,欢迎留言交流。

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~