php扩展开发中引用计数与GC

php,php扩展

Posted by hughnian on June 29, 2020

0x01

php扩展的开发现在有不少新的方式比如用zephir。是一种用类似php的语法写php扩展。当然主要的还是用纯C写扩展。在开发php扩展中,由于资料还是比较缺少的,走了不少弯路。 以此来做个记录总结。

我在开发nmid-php-ext扩展时,先查阅了些php扩展开发文档,目前比较好的可能就是盘古大叔的文档https://github.com/pangudashu/php7-internal,当然还有惠新宸的博客文章等。 还有就是查看了一些比较经典的php扩展源码,我看的有gearman,swoole1.8,swoole4.4.13。这其中也是有不少坑的地方,尤其是老版本的扩展代码基本上不适用与现在的php。我现在用的都是php7.1以上的版本。 但是可以借鉴一些思想了,我这里思想上大部分是借鉴了gearman的思想,写法上是借鉴了不少swoole4.4.13的写法。

在写扩展的时候,尤其需要注意的地方可能就是这个引用计数和GC的问题。这个东西虽然占用的代码很少,但是在出现问题时非常的坑爹。因为在一些情况下有时没有问题,但是如果php使用多了话,就会出现php的段错误异常等。 nmid-php-ext扩展遇到的主要问题就是php用户态空间变量或着方法传递到C层调用时候,会出现一些问题。用户空间会传递一个回调函数给C层的doCallBack函数,实际回调函数被调用的地方是C层,但是该回调本身是由用户空间 申请的,由Z_PARAM_FUNC_EX接收的php用户函数,并交由php内核GC管理的。如果扩展函数内不做任何处理操作,那么当切换到用户空间时php内核会判断该变量需要回收,然后扩展函数就会空指针异常等。 当扩展函数内php变量或着函数的生命周期使用结束后,任然需要考虑到php的GC问题。并不是在扩展函数中使用free的。free可以使用在C层自己创建的内存空间。而php的需要调用内核的引用计数接口等。 进行变量的回收以及交由php内核GC管理。这里主要看以下代码。

    #ifndef GC_ADDREF
    #define GC_ADDREF(ref) ++GC_REFCOUNT(ref)
    #define GC_DELREF(ref) --GC_REFCOUNT(ref)
    #endif
    
    #ifndef ZEND_CLOSURE_OBJECT
    #define ZEND_CLOSURE_OBJECT(op_array) \
	    ((zend_object*)((char*)(op_array) - sizeof(zend_object)))
    #endif

    static 
    void n_zend_fci_cache_persist(zend_fcall_info_cache *fci_cache)
    {
        if(fci_cache->object) {
            GC_ADDREF(fci_cache->object);
        }
        if(fci_cache->function_handler->op_array.fn_flags & ZEND_ACC_CLOSURE) {
            GC_ADDREF(ZEND_CLOSURE_OBJECT(fci_cache->function_handler));
        }
    }
    
    PHP_METHOD(workerext, add_function)
    {
        char *fname;
        size_t fnamel;
        Worker *w;
    
        zend_fcall_info fci = empty_fcall_info;
        zend_fcall_info_cache fci_cache = empty_fcall_info_cache;
    	
    	ZEND_PARSE_PARAMETERS_START(2, 2)
            Z_PARAM_STRING(fname, fnamel)
            Z_PARAM_FUNC_EX(fci, fci_cache, 1, 0)
        ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);
    	
    	if(fci.size) {
    		n_zend_fci_cache_persist(&fci_cache); //这里调用增加引用计数,而且这里的回调函数是闭包
    	}
        
        .............    	
    }

0x02

最后总结:用户空间申请的变量或者函数传递给扩展内函数使用,如果在返回给用户空间后还要使用的话,就需要引用计数+1,因为在返回给用户空间的时候本身用户空间GC会判断该变量或者 函数是否有继续引用,否则就要引用计数-1,用户空间回收该变量或函数。如果扩展内函数(C层的函数)仍然访问该已经注销的变量或者用户态函数,就会导致段错误或异常。在开发php扩展的时候 需要将这些变量的引用与GC做好才能使得扩展安全可靠。当然我本人对扩展还有很多需要学习的地方,目前对php的ZTS线程安全版的扩展开发还有些问题,nmid-php-ext目前也支持NTS非线程安全版php, 这也需要后期解决。