Home >Backend Development >PHP Tutorial >一次失败的PHP扩展开发之

一次失败的PHP扩展开发之

WBOY
WBOYOriginal
2016-06-23 13:44:54924browse

一次失败的PHP 扩展开发之旅

By warezhou 2014.11.19


缘起

经过不断的持续迭代,我们部门的协程版网络框架(CoSvrFrame)终于出炉了!这本来是件喜大普奔的事情,但是随着新业务的不断接入,很多固有缺陷也逐渐浮出水面:

  • 不支持“TCP连接池”
  • 不支持“Dispatcher-Workers模型”
  • 不支持“过载保护”
  • 不支持“热重启”
  • 不支持“64Bit”
  • ... ...
  • 对于资深后台开发而言,上面罗列的问题大多数都难入法眼,之所以成为问题,很有点“温水煮青蛙”的味道:迭代过程缺乏宏观视野,引入过多业务特性,导致整体架构不合理。最近的“协程版本”最初也是我个人业余之作,仅仅为了能够愉快地写业务代码,为了快点出活,底层直接复用原有SvrFrame,结果可想而知:根基不牢,地动山摇!以最极端的64Bit为例,相信大家秒懂了。

    经过多番调研与讨论,最终我们给出了如下前进方向:

  • 引入公司内部开源的SPP3.0框架,吸收它的基础周边设施,进行业务二次开发
  • 对于SPP进行扩展,支持PHP作为脚本语言进行嵌入式编程,同时以C扩展形式给PHP提供协程能力(从此PHPer也可以愉快地书写异步代码了,妈妈再也不用担心我的callback了!)
  • 叨逼叨?嗦了这么久,下面可以切入主题了:如何实现C++/PHP混合编程?

    免责申明:由于本人属于半路出家,接触PHP扩展开发尚未足周,因此无法深入到WHY,仅能停留在HOW,仅作记录之用,望高手见谅!

    开场

    嵌入式PHP

    业内C++/PHP的结合,一般是出于“性能”考虑,在PHP代码里调用C/C++扩展,从而解决特定的性能瓶颈(如PB序列化等)。

    作为C/C++开发出身,“开发效率”相对于“性能”的诱惑显然更大,因此,我们的思路是:将PHP作为脚本语言,快速开发业务逻辑,插入到SPP框架运行。

    1. 以RTLD_GLOBAL方式打开php动态库

    void *php_handler = dlopen("libphp5.so", RTLD_LAZY | RTLD_GLOBAL);if (!php_handler) {    base->log_.LOG_P_PID(LOG_FATAL, "%s\n", dlerror());    return -1; }   dlclose(php_handler);

    2. 通过php_embed_init进行初始化

    php_embed_module.php_ini_path_override = "../php/php.ini";php_embed_init(0, NULL);

    3. 通过zend_eval_string引入PHP脚本

    zend_first_try {    char exec_str[256];    snprintf(exec_str, sizeof(exec_str), "include '%s';", "../php/demo_handler.php");    if (int ret = zend_eval_string(exec_str, NULL, exec_str TSRMLS_CC)) {        base->log_.LOG_P_PID(LOG_FATAL, "zend_eval_string fail. ret=%d\n", ret);        return -1;     }    base->log_.LOG_P_PID(LOG_DEBUG, "zend_eval_string succ.\n");} zend_catch {    base->log_.LOG_P_PID(LOG_FATAL, "zend_eval_string catch.\n");} zend_end_try ();

    4. 通过call_user_function回调PHP函数

    zval z_funcname;ZVAL_STRING(&z_funcname, "EchoDemo::init", 1);zval *zp_svr;MAKE_STD_ZVAL(zp_svr);ZVAL_LONG(zp_svr, (long)base);zval *zp_etc;MAKE_STD_ZVAL(zp_etc);ZVAL_STRING(zp_etc, etc, 1);zval z_retval;zval *z_params[] = {zp_svr, zp_etc};int call_ret = call_user_function(CG(function_table), NULL, &z_funcname, &z_retval, sizeof(z_params) / sizeof(z_params[0]), z_params TSRM convert_to_long(&z_retval);int func_ret = Z_LVAL_P(&z_retval);zval_ptr_dtor(&zp_etc);zval_dtor(&z_funcname);zval_dtor(&z_retval);if (call_ret log_.LOG_P_PID(LOG_FATAL, "call_user_function fail. call_ret=%d func_ret=%d\n", call_ret, func_ret);    return -1;}

    5. 通过php_embed_shutdown进行清理

    php_embed_shutdown(TSRMLS_C);

    PHP扩展

    网络上关于PHP的C扩展开发文章可以说已经到泛滥的地步了,有兴趣的读者可以深入阅读文末的附录。

    1. 下载php源码包,进行手动编译,为了配合上述嵌入式使用,需要打开?enable-embed选项

    ./configure --enable-embedmakemake install(可选)

    2. 进入php源码包的ext目录,借助ext_skel工具生成插件架子代码

    cd ext./ext_skel --extname=demo

    3. 编辑config.m4,打开PHP_ARG_WITH或者PHP_ARG_ENABLE选项(说实话区别仍没搞清楚,求达人指点),添加C++支持、依赖路径等

    PHP_ARG_ENABLE(demo, whether to enable demo support,    [  --enable-demo           Enable demo support])if test "$PHP_DEMO" != "no"; then  PHP_REQUIRE_CXX()  PHP_ADD_LIBRARY(stdc++, 1, EXTRA_LDFLAGS)  PHP_ADD_INCLUDE(/root/spp/module/include/)  PHP_ADD_INCLUDE(/root/spp/module/include/spp_incl/)  PHP_NEW_EXTENSION(demo, demo.cpp, $ext_shared)fi

    4. 编辑demo.cpp,添加扩展定义和实现(函数、类、变量 ...),这里仅仅给出函数定义示例,类相关的有兴趣的读者自行根据附录摸索。这里给出的sendrecv函数定义比较有代表性,其中第3个参数rsp为引用参数,负责将接收到的数据返回给PHP调用方

    ZEND_BEGIN_ARG_INFO_EX(arginfo_sendrecv, 0, 0, 7)    ZEND_ARG_INFO(0, req)    ZEND_ARG_INFO(0, req_len)    ZEND_ARG_INFO(1, rsp)    ZEND_ARG_INFO(0, rsp_len)    ZEND_ARG_INFO(0, ip)    ZEND_ARG_INFO(0, port)    ZEND_ARG_INFO(0, timeout)ZEND_END_ARG_INFO()PHP_FUNCTION(sendrecv){    char *req = NULL;    int req_str_len = 0;    long req_len = 0;    zval *rsp = NULL;    long rsp_len = 0;    char *ip = NULL;    int ip_str_len = 0;    long port = 0;    long timeout = 0;if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "slzlsll", &req, &req_str_len,&req_len, &rsp, &rsp_len, &ip, &ip_str_len, &port, &timeout) == FAILURE) {        return;}       struct sockaddr_in addr;    memset(&addr, 0, sizeof(addr));    addr.sin_family = AF_INET;    addr.sin_addr.s_addr = inet_addr(std::string(ip, ip_str_len).c_str());    addr.sin_port = htons(port);    char *rsp_buf = (char *)emalloc(rsp_len);    int rsp_buf_len = rsp_len;    if (int ret = mt_udpsendrcv(&addr, req, req_len > req_str_len ? req_str_len : req_len, rsp_buf, rsp_buf_len, timeout)) {        efree(rsp_buf);        RETURN_LONG(ret);    }    zval_dtor(rsp);    ZVAL_STRINGL(rsp, rsp_buf, rsp_buf_len, 0);    RETURN_LONG(0);}
    const zend_function_entry demo_functions[] = {    PHP_FE(sendrecv, arginfo_sendrecv)    PHP_FE_END  /* Must be the last line in demo_functions[] */};

    5. 一切准备就绪,可以编译扩展了,我个人比较喜欢动态编译(静态编译需要重新编译php源码,太耗时费力),生成的.so位于当前扩展的modules目录下

    /usr/local/bin/phpize./configure --with-php-config=/usr/local/bin/php-configmake

    6. 编辑php.ini文件,添加新的扩展,然后就可以愉快地在PHP代码中调用新扩展了

    extension_dir="/somewhere/modules"extension="demo.so"extension="xxxx.so"

    高潮

    终于到了组装成型的时刻了,通过telnet玩了几把EchoDemo,看到一行一行的回显,不禁心情大好。

    <?php class EchoDemo {        public static function init($server, $conf) {            log_debug($server, "init in php.\n");            return true;        }           public static function input($server, $req, $ext_info = array()) {            log_debug($server, "input in php.\n");            return strlen($req);        }           public static function route($server, $req, $ext_info = array()) {            log_debug($server, "route in php.\n");            return 1;        }           public static function process($server, $req, $ext_info = array()) {            log_debug($server, "process in php.\n");            $ret = sendrecv($req, strlen($req), $rsp, 65535, "127.0.0.1", 2345, 500);            if ($ret != 0) {                log_debug($server, "sendrecv fail. ret=$ret");                return false;            }               log_debug($server, "sendrecv finish. rsp=$rsp");            return true;        }           public static function fini($server) {            log_debug($server, "fini in php.\n");        }       }?>

    这里最值得赞叹的就是process函数对于sendrecv扩展调用,这里背后通过协程其实已经实现了一次异步网络交互:既能像同步CGI般书写逻辑代码,又能无痛地享受异步的高并发。

    愿望是美好的,现实是残酷的!

    我这时突然心血来潮:来压测一把性能吧,看看相比于原生C++代码有多大的性能衰减。单次请求1KB,施以1w/s的压力,压了一会coredump了。

    内存泄漏?协程栈溢出?...

    期间各种折腾:GDB,修改协程栈大小,Google,咨询PHPer ...

    很快到了晚上,该查的都查过了,该问的都问过了,实在没辙了,停下来喝杯茶:“call_user_function可重入么”?想到这一层,相信了解协程本质的兄弟又秒懂了:你妹的,人家实现Zend的时候怎么知道调用线程还会玩协程进行用户态调度啊,这个黑盒里面一切皆有可能啊!全局变量、静态变量 ...

    好吧,去掉sendrecv这类基于协程的扩展,重新压测,单worker对于3w/s的echo还是轻松无压力的。

    结局

    虽然这次最吸引人的一个Feature最终未能实现,不过我还是很开心,因为再次印证了一个观点:思考往往比蛮干高效百倍,尤其在处理棘手问题时,无头苍蝇般乱闯乱撞往往费力不讨好,此时,如果能够冷静下来,尽力搜集现有知识储备,说不定灵感就来光顾你了。

    未来可能的方向:PHP从5.5版本引入了yield,感觉如果挖掘出来Zend对于yield的支持细节,说不定有希望和我们的C框架很好的融合,但是总觉得是个填不平的大坑。如果抛开其它因素,也许我还是希望选择Golang一类语言直接享受goroutine的优势吧,哈哈!

    附录

    PHP扩展开发及内核应用

    http://www.walu.cc/phpbook/preface.md

    编译PHP扩展的两种方式

    http://521-wf.com/archives/227.html

    如何使用C++开发PHP扩展(上)

    http://521-wf.com/archives/241.html

    如何使用C++开发PHP扩展(下)

    http://521-wf.com/archives/245.html

    Wrapping C++ Classes in a PHP Extension

    http://devzone.zend.com/1435/wrapping-c-classes-in-a-php-extension/

    Statement:
    The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn