首頁 後端開發 php教程 一次失败的PHP扩展开发之

一次失败的PHP扩展开发之

Jun 23, 2016 pm 01:44 PM

一次失败的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/

    本網站聲明
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

    熱AI工具

    Undresser.AI Undress

    Undresser.AI Undress

    人工智慧驅動的應用程序,用於創建逼真的裸體照片

    AI Clothes Remover

    AI Clothes Remover

    用於從照片中去除衣服的線上人工智慧工具。

    Undress AI Tool

    Undress AI Tool

    免費脫衣圖片

    Clothoff.io

    Clothoff.io

    AI脫衣器

    Video Face Swap

    Video Face Swap

    使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

    熱工具

    記事本++7.3.1

    記事本++7.3.1

    好用且免費的程式碼編輯器

    SublimeText3漢化版

    SublimeText3漢化版

    中文版,非常好用

    禪工作室 13.0.1

    禪工作室 13.0.1

    強大的PHP整合開發環境

    Dreamweaver CS6

    Dreamweaver CS6

    視覺化網頁開發工具

    SublimeText3 Mac版

    SublimeText3 Mac版

    神級程式碼編輯軟體(SublimeText3)

    在PHP API中說明JSON Web令牌(JWT)及其用例。 在PHP API中說明JSON Web令牌(JWT)及其用例。 Apr 05, 2025 am 12:04 AM

    JWT是一種基於JSON的開放標準,用於在各方之間安全地傳輸信息,主要用於身份驗證和信息交換。 1.JWT由Header、Payload和Signature三部分組成。 2.JWT的工作原理包括生成JWT、驗證JWT和解析Payload三個步驟。 3.在PHP中使用JWT進行身份驗證時,可以生成和驗證JWT,並在高級用法中包含用戶角色和權限信息。 4.常見錯誤包括簽名驗證失敗、令牌過期和Payload過大,調試技巧包括使用調試工具和日誌記錄。 5.性能優化和最佳實踐包括使用合適的簽名算法、合理設置有效期、

    會話如何劫持工作,如何在PHP中減輕它? 會話如何劫持工作,如何在PHP中減輕它? Apr 06, 2025 am 12:02 AM

    會話劫持可以通過以下步驟實現:1.獲取會話ID,2.使用會話ID,3.保持會話活躍。在PHP中防範會話劫持的方法包括:1.使用session_regenerate_id()函數重新生成會話ID,2.通過數據庫存儲會話數據,3.確保所有會話數據通過HTTPS傳輸。

    PHP 8.1中的枚舉(枚舉)是什麼? PHP 8.1中的枚舉(枚舉)是什麼? Apr 03, 2025 am 12:05 AM

    PHP8.1中的枚舉功能通過定義命名常量增強了代碼的清晰度和類型安全性。 1)枚舉可以是整數、字符串或對象,提高了代碼可讀性和類型安全性。 2)枚舉基於類,支持面向對象特性,如遍歷和反射。 3)枚舉可用於比較和賦值,確保類型安全。 4)枚舉支持添加方法,實現複雜邏輯。 5)嚴格類型檢查和錯誤處理可避免常見錯誤。 6)枚舉減少魔法值,提升可維護性,但需注意性能優化。

    描述紮實的原則及其如何應用於PHP的開發。 描述紮實的原則及其如何應用於PHP的開發。 Apr 03, 2025 am 12:04 AM

    SOLID原則在PHP開發中的應用包括:1.單一職責原則(SRP):每個類只負責一個功能。 2.開閉原則(OCP):通過擴展而非修改實現變化。 3.里氏替換原則(LSP):子類可替換基類而不影響程序正確性。 4.接口隔離原則(ISP):使用細粒度接口避免依賴不使用的方法。 5.依賴倒置原則(DIP):高低層次模塊都依賴於抽象,通過依賴注入實現。

    在PHPStorm中如何進行CLI模式的調試? 在PHPStorm中如何進行CLI模式的調試? Apr 01, 2025 pm 02:57 PM

    在PHPStorm中如何進行CLI模式的調試?在使用PHPStorm進行開發時,有時我們需要在命令行界面(CLI)模式下調試PHP�...

    如何在系統重啟後自動設置unixsocket的權限? 如何在系統重啟後自動設置unixsocket的權限? Mar 31, 2025 pm 11:54 PM

    如何在系統重啟後自動設置unixsocket的權限每次系統重啟後,我們都需要執行以下命令來修改unixsocket的權限:sudo...

    如何用PHP的cURL庫發送包含JSON數據的POST請求? 如何用PHP的cURL庫發送包含JSON數據的POST請求? Apr 01, 2025 pm 03:12 PM

    使用PHP的cURL庫發送JSON數據在PHP開發中,經常需要與外部API進行交互,其中一種常見的方式是使用cURL庫發送POST�...

    See all articles