首頁  >  文章  >  後端開發  >  深入了解php的擴展開發

深入了解php的擴展開發

零到壹度
零到壹度原創
2018-04-14 16:54:231450瀏覽

PHP是目前應用非常廣泛的語言,從國外的Facebook、Twitter到國內的淘寶、騰訊、百度再到互聯網上林林總總的各種大中小型網站都能見到它的身影。 PHP的成功,應該說很大程度上依賴其開放的擴充API機制和豐富的擴充元件(PHP Extension),而正是這些擴充元件使得PHP從各種資料庫操作到XML、JSON、加密、檔案處理、圖形處理、Socket等領域無所不能。有時候開發人員可能需要開發自己的PHP擴展,目前PHP5的擴展機制是基於Zend API的,Zend API提供了豐富的介面和巨集定義,加上一些實用工具,使得PHP擴充開發起來難度並不算特別大。本文將介紹關於PHP擴展組件開發的基本知識,並透過一個實例展示開發PHP擴展的基本過程。

PHP擴充元件的開發過程在Unix和Windows環境下有所不同,但基本上是互通的,本文將基於Unix環境(具體使用Linux)。閱讀本文需要簡單了解Unix環境、PHP和C語言的一些基礎知識,只要簡單了解就行,我會盡量不涉及太過具體的操作系統和語言特性,並在必要的地方加以解釋,以便讀者閱讀。

本文的特定開發環境為Ubuntu 10.04 PHP 5.3.3。

下載PHP原始碼

要開發PHP擴展,第一步要下載PHP原始碼,因為裡面有開發擴充所需的工具。我下載的是PHP最新版本5.3.3,格式為tar.bz2壓縮包。

下載網址為:http://cn.php.net/get/php-5.3.3.tar.bz2/from/a/mirror

下載後,將原始程式碼移至合適的目錄並解壓縮。解壓縮指令為:

  1. tar -jxvf 原始碼套件名稱

若下載的是tar.gz壓縮包,解壓縮指令為

  1. tar ##- zxvf 原始碼包名稱

解壓縮後,在原始碼目錄中有個ext目錄,這裡便是和PHP擴充相關的目錄。進入目錄後用ls查看,可以看到許多已經存在的擴充。下圖是在我的環境下查看的結果:

其中藍色的均是擴充包目錄,其中可以看到我們很熟悉的mysql、iconv和gd等等。而ext_skel是Unix環境下用來自動產生PHP擴充框架的腳本工具,後面我們馬上會用到,ext_skel_win32.php是windows下對應的腳本。

開發自己的PHP擴充功能——say_hello

下面我們開發一個PHP擴充:say_hello。這個擴充很簡單,只是接受一個字串參數,然後輸出「Hello xxx!」。這個例子只是為了介紹PHP擴充組件的開發流程,不承擔實際功能。

產生擴充元件框架

PHP的擴充元件開發目錄和文件是有固定組織結構的,你可以隨便進入一個已有擴充元件目錄,查看其所有文件,我想你一定是眼花撩亂了。當然你可以選擇手工完成框架的搭建,不過我相信你更希望有什麼東西可以幫你完成。上文提到的ext_skel腳本就是用來自動建立擴充包框架的工具。 ext_skel的完整指令為:

  1. ext_skel --extname=module #[--proto=file#][-- stubs=file] [--xml [=file]] [--skel #=dir] [--full##-xml] [--no-help #]

作為初學者,我們不必了解所有指令參數,實際上,大多數情況下只需要提供第一個參數就可以了,也就是擴充模組的名字。因此,我們在ext目錄中鍵入以下命令:

  1. ./ext_skel ##--##extname =say_hello

    #(如果你希望詳細了解ext_skel的各項指令參數,請參考這裡)
這時再用ls查看,會發現多了一個「say_hello」目錄,進入這個目錄,會發現ext_skel已經為我們建立好了say_hello的基本框架,如下圖:

如果你懶得弄清楚PHP擴充包目錄結構的全部內容,那麼裡面有三個檔案你必須注意:

config.m4:這是Unix環境下的Build System設定文件,後面將會透過它產生配置和安裝。

php_say_hello.h:這個檔案是擴充模組的頭檔。遵循C語言一貫的作風,這個裡面可以放置一些自訂的結構體、全域變數等等。

say_hello.c:這個就是擴充模組的主程式檔案了,最終的擴充模組各個函式入口都在這裡。當然,你可以將所有程式碼都塞到這裡面,也可以遵循模組化思想,將各個功能模組放到不同檔案中。

下面的內容主要圍繞著這三個文件。

Unix Build System設定

開發PHP擴充元件的第一步不是寫實作程式碼,而是要先設定好Build System選項。由於我們是在Linux下開發,所以這裡的設定主要與config.m4有關。

關於Build System配置這一塊,如果寫起來能寫一大堆,而且與Unix系統很多東西相關,就算我有興趣寫估計大家也沒興趣看,所以這裡我們從略,只揀關鍵地方說一下,關於config.m4更多細節可以參考這裡。

開啟產生的config.m4文件,內容大致如下:

  1. dnl $Id$
    dnl config.m4 for extension say_hello
    dnl Comments in this file start with the string 'dnl'.
    dnl Remove where necessary. This file will not work
    dnl without editing.
     
    dnl If your extension references something external, use with:
    dnl PHP_ARG_WITH(say_hello, for say_hello support,
    dnl Make sure that the comment is aligned:
    dnl [ --with-say_hello Include say_hello support])
     
    dnl Otherwise use enable:
    dnl PHP_ARG_ENABLE(say_hello, whether to enable say_hello support,
    dnl Make sure that the comment is aligned:
    dnl [ --enable-say_hello Enable say_hello support])
     
    if test "$PHP_SAY_HELLO" != "no"; then
    dnl Write more examples of tests here...
    dnl # --with-say_hello -> check with-path
    dnl SEARCH_PATH="/usr/local /usr" # you might want to change this
    dnl SEARCH_FOR="/include/say_hello.h" # you most likely want to change this
    dnl if test -r $PHP_SAY_HELLO/$SEARCH_FOR; then # path given as parameter
    dnl SAY_HELLO_DIR=$PHP_SAY_HELLO
    dnl else # search default path list
    dnl AC_MSG_CHECKING([for say_hello files in default path])
    dnl for i in $SEARCH_PATH ; do
    dnl if test -r $i/$SEARCH_FOR; then
    dnl SAY_HELLO_DIR=$i
    dnl AC_MSG_RESULT(found in $i)
    dnl fi
    dnl done
    dnl fi
    dnl
    dnl if test -z "$SAY_HELLO_DIR"; then
    dnl AC_MSG_RESULT([not found])
    dnl AC_MSG_ERROR([Please reinstall the say_hello distribution])
    dnl fi
    dnl # --with-say_hello -> add include path
    dnl PHP_ADD_INCLUDE($SAY_HELLO_DIR/include)
    dnl # --with-say_hello -> check for lib and symbol presence
    dnl LIBNAME=say_hello # you may want to change this
    dnl LIBSYMBOL=say_hello # you most likely want to change this
    dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,
    dnl [
    dnl PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $SAY_HELLO_DIR/lib, SAY_HELLO_SHARED_LIBADD)
    dnl AC_DEFINE(HAVE_SAY_HELLOLIB,1,[ ])
    dnl ],[
    dnl AC_MSG_ERROR([wrong say_hello lib version or lib not found])
    dnl ],[
    dnl -L$SAY_HELLO_DIR/lib -lm
    dnl ])
    dnl
    dnl PHP_SUBST(SAY_HELLO_SHARED_LIBADD)
    PHP_NEW_EXTENSION(say_hello, say_hello.c, $ext_shared)
    fi

不要看這麼多,因為所有以「dnl」開頭的全是註釋,所以真正起作用沒幾行。這裡需要設定的只有下面幾行:

  1. dnl If your extension references something external, use with:
    dnl PHP_ARG_WITH(say_hello, for say_hello support,
    dnl Make sure that the comment is aligned:
    dnl [ --with-say_hello Include say_hello support])
     
    dnl Otherwise use enable:
    dnl PHP_ARG_ENABLE(say_hello, whether to enable say_hello support,
    dnl Make sure that the comment is aligned:
    dnl [ --enable-say_hello Enable say_hello support])

我想大家也都能看明白,意思就是「如果你的擴充引用了外部元件,使用…,否則使用…」。我們的say_hello擴充功能並沒有引用外部元件,所以將「Otherwise use enable」下面三行的「dnl」去掉,改為:

    ##
    dnl Otherwise use enable:
    PHP_ARG_ENABLE(say_hello, whether to enable say_hello support,
    Make sure that the comment is aligned:
    [ --enable-say_hello Enable say_hello support])
#儲存,這樣關於Build System配置就大功告成了。

PHP Extension及Zend_Module結構分析

以上可以看成是為開發PHP擴充功能而做的準備工作,下面就要寫核心程式碼了。上文說過,寫PHP擴充是基於Zend API和一些巨集的,所以如果要寫核心程式碼,我們要先弄清楚PHP Extension的結構。因為一個PHP Extension在C語言層面其實就是一個zend_module_entry結構體,這點可以從「php_say_hello.h」得到證實。打開“php_say_hello.h”,會看到裡面有怎麼一行:

  1. #extern zend_module_entry say_hello_module_entry;

say_hello_module_entry就是say_hello擴充的C語言對應元素,而關於其類型zend_module_entry的定義可以在PHP原始碼的「Zend/zend_modules.h」檔案裡找到,下面程式碼是zend_module_entry的定義:

    typedef struct _zend_module_entry zend_module_entry;
     
    struct _zend_module_entry {
        unsigned short size;
        unsigned int zend_api;
        unsigned char zend_debug;
        unsigned char zts;
        const struct _zend_ini_entry *ini_entry;
        const struct _zend_module_dep *deps;
        const char *name;
        const struct _zend_function_entry *functions;
        int (*module_startup_func)(INIT_FUNC_ARGS);
        int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
        int (*request_startup_func)(INIT_FUNC_ARGS);
        int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
        void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
        const char *version;
        size_t globals_size;
     
        #ifdef ZTS
        ts_rsrc_id* globals_id_ptr;
        #else
        void* globals_ptr;
        #endif
     
        void (*globals_ctor)(void *global TSRMLS_DC);
        void (*globals_dtor)(void *global TSRMLS_DC);
        int (*post_deactivate_func)(void);
        int module_started;
        unsigned char type;
        void *handle;
        int module_number;
        char *build_id;
    };
這個結構體可能看起來會讓人有點頭痛,不過我還是要解釋裡面的內容。因為這就是PHP Extension的原型,如果不搞清楚,就沒辦法開發PHP Extension了。當然,我就不一對每個字段進行解釋了,只揀關鍵的、這篇文章會用到的字段說,因為許多字段並不需要我們手工填寫,而是可以使用某些預定義的宏填充。

第7個欄位“name”,這個欄位是此PHP Extension的名字,在本例中就是“say_hello”。

第8個欄位“functions”,這個將存放我們在此擴充中定義的函數的引用,具體結構不再分析,有興趣的朋友可以閱讀_zend_function_entry的原始程式碼。具體編寫程式碼時這裡會有對應的巨集。

第9-12個欄位分別是四個函數指針,這四個函數會在相應時機被調用,分別是「擴充模組載入時」、「擴充模組卸載時」、「每個請求開始時」和「每個請求結束時」。這四個函數可以看成是一種攔截機制,主要用於對應時機的資源分配、釋放等相關操作。

第13個欄位「info_func」也是一個函數指針,這個指標指向的函數會在執行phpinfo()時被調用,用來顯示自訂模組資訊。

第14個欄位「version」是模組的版本。

(關於zend_module_entry更詳盡的介紹請參考這裡)

介紹完以上字段,我們可以看看“say_hello.c”中自動生成的“say_hello_module_entry”框架代碼了。

  1. /* {{{ say_hello_module_entry
    */
    zend_module_entry say_hello_module_entry = {
        #if ZEND_MODULE_API_NO >= 20010901
        STANDARD_MODULE_HEADER,
        #endif
        "say_hello",
        say_hello_functions,
        PHP_MINIT(say_hello),
        PHP_MSHUTDOWN(say_hello),
        PHP_RINIT(say_hello), /* Replace with NULL if there's nothing to do at request start */
        PHP_RSHUTDOWN(say_hello), /* Replace with NULL if there's nothing to do at request end */
        PHP_MINFO(say_hello),
        #if ZEND_MODULE_API_NO >= 20010901
        "0.1", /* Replace with version number for your extension */
        #endif
        STANDARD_MODULE_PROPERTIES
    };
    /* }}} */

首先,宏“STANDARD_MODULE_HEADER”会生成前6个字段,“STANDARD_MODULE_PROPERTIES ”会生成“version”后的字段,所以现在我们还不用操心。而我们关心的几个字段,也都填写好或由宏生成好了,并且在“say_hello.c”的相应位置也生成了几个函数的框架。这里要注意,几个宏的参数均为“say_hello”,但这并不表示几个函数的名字全为“say_hello”,C语言中也不可能存在函数名重载机制。实际上,在开发PHP Extension的过程中,几乎处处都要用到Zend里预定义的各种宏,从全局变量到函数的定义甚至返回值,都不能按照“裸写”的方式来编写C语言,这是因为PHP的运行机制可能会导致命名冲突等问题,而这些宏会将函数等元素变换成一个内部名称,但这些对程序员都是透明的(除非你去阅读那些宏的代码),我们通过各种宏进行编程,而宏则为我们处理很多内部的东西。

写到这里,我们的任务就明了了:第一,如果需要在相应时机处理一些东西,那么需要填充各个拦截函数内容;第二,编写say_hello的功能函数,并将引用添加到say_hello_functions中。

编写phpinfo()回调函数

因为say_hello扩展在各个生命周期阶段并不需要做操作,所以我们只编写info_func的内容,上文说过,这个函数将在phpinfo()执行时被自动调用,用于显示扩展的信息。编写这个函数会用到四个函数:

php_info_print_table_start()——开始phpinfo表格。无参数。

php_info_print_table_header()——输出表格头。第一个参数是整形,指明头的列数,然后后面的参数是与列数等量的(char*)类型参数用于指定显示的文字。

php_info_print_table_row()——输出表格内容。第一个参数是整形,指明这一行的列数,然后后面的参数是与列数等量的(char*)类型参数用于指定显示的文字。

php_info_print_table_end()——结束phpinfo表格。无参数。

下面是“say_hello.c”中需要编写的info_func的具体代码:

  1. /* {{{ PHP_MINFO_FUNCTION
    */
    PHP_MINFO_FUNCTION(say_hello)
    {
        php_info_print_table_start();
        php_info_print_table_header(2, "say_hello support", "enabled");
        php_info_print_table_row(2, "author", "Zhang Yang"); /* Replace with your name */
        php_info_print_table_end();
        /* Remove comments if you have entries in php.ini
        DISPLAY_INI_ENTRIES();
        */
    }
    /* }}} */

可以看到我们编写了两行内容、组件是否可用以及作者信息。

编写核心函数

编写核心函数,总共分为三步:1、使用宏PHP_FUNCTION定义函数体;2、使用宏ZEND_BEGIN_ARG_INFO和ZEND_END_ARG_INFO定义参数信息;3、使用宏PHP_FE将函数加入到say_hello_functions中。下面分步说明。

使用宏PHP_FUNCTION定义函数体

  1. PHP_FUNCTION(say_hello_func)
    {
        char *name;
        int name_len;
        if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &name_len) == FAILURE)
        {
            return;
        }
        php_printf("Hello %s!", name);
     
        RETURN_TRUE;
    }

上文说过,编写PHP扩展时几乎所有东西都不能裸写,而是必须使用相应的宏。从上面代码可以清楚看到这一点。总体来说,核心函数代码一般由如下几部分构成:

定义函数,这一步通过宏PHP_FUNCTION实现,函数的外部名称就是宏后面括号里面的名称。

声明并定义局部变量。

解析参数,这一步通过zend_parse_parameters函数实现,这个函数的作用是从函数用户的输入栈中读取数据,然后转换成相应的函数参数填入变量以供后面核心功能代码使用。zend_parse_parameters的第一个参数是用户传入参数的个数,可以由宏“ZEND_NUM_ARGS() TSRMLS_CC”生成;第二个参数是一个字符串,其中每个字母代表一个变量类型,我们只有一个字符串型变量,所以第二个参数是“s”;最后各个参数需要一些必要的局部变量指针用于存储数据,下表给出了不同变量类型的字母代表及其所需要的局部变量指针。

参数解析完成后就是核心功能代码,我们这里只是输出一行字符,php_printf是Zend版本的printf。

最后的返回值也是通过宏实现的。RETURN_TRUE宏是返回布尔值“true”。

使用宏ZEND_BEGIN_ARG_INFO和ZEND_END_ARG_INFO定义参数信息

参数信息是函数所必要部分,这里不做深究,直接给出相应代码:

  1. ZEND_BEGIN_ARG_INFO(arginfo_say_hello_func, 0) ZEND_END_ARG_INFO()

如需了解具体信息请阅读相关宏定义。

使用宏PHP_FE将函数加入到say_hello_functions中

最后,我们需要将刚才定义的函数和参数信息加入到say_hello_functions数组里,代码如下:

  1. const zend_function_entry say_hello_functions[] = {
    PHP_FE(say_hello_func, arginfo_say_hello_func)
        {NULL, NULL, NULL}
    };

这一步就是通过PHP_EF宏实现,注意这个数组最后一行必须是{NULL, NULL, NULL} ,请不要删除。

下面是编写完成后的say_hello.c全部代码:

  1. /*
    +----------------------------------------------------------------------+
    | PHP Version 5                                                        |
    +----------------------------------------------------------------------+
    | Copyright (c) 1997-2010 The PHP Group                                |
    +----------------------------------------------------------------------+
    | This source file is subject to version 3.01 of the PHP license,      |
    | that is bundled with this package in the file LICENSE, and is        |
    | available through the world-wide-web at the following url:           |
    | http://www.php.net/license/3_01.txt                                  |
    | If you did not receive a copy of the PHP license and are unable to   |
    | obtain it through the world-wide-web, please send a note to          |
    | license@php.net so we can mail you a copy immediately.               |
    +----------------------------------------------------------------------+
    | Author: ZhangYang                          |
    +----------------------------------------------------------------------+
    */
    /* $Id: header 297205 2010-03-30 21:09:07Z johannes $ */
    #ifdef HAVE_CONFIG_H
    #include "config.h"
    #endif
    #include "php.h"
    #include "php_ini.h"
    #include "ext/standard/info.h"
    #include "php_say_hello.h"
    /* If you declare any globals in php_say_hello.h uncomment this:
    ZEND_DECLARE_MODULE_GLOBALS(say_hello)
    */
    /* True global resources - no need for thread safety here */
    static int le_say_hello;
    /* {{{ PHP_FUNCTION
    */
    PHP_FUNCTION(say_hello_func)
    {
        char *name;
        int name_len;
        if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &name_len) == FAILURE)
        {
            return;
        }
        php_printf("Hello %s!", name);
        RETURN_TRUE;
    }
    ZEND_BEGIN_ARG_INFO(arginfo_say_hello_func, 0)
    ZEND_END_ARG_INFO()
    /* }}} */
    /* {{{ say_hello_functions[]
    *
    * Every user visible function must have an entry in say_hello_functions[].
    */
    const zend_function_entry say_hello_functions[] = {
        PHP_FE(say_hello_func, arginfo_say_hello_func)
        {NULL, NULL, NULL} /* Must be the last line in say_hello_functions[] */
    };
    /* }}} */
    /* {{{ say_hello_module_entry
    */
    zend_module_entry say_hello_module_entry = {
        #if ZEND_MODULE_API_NO >= 20010901
        STANDARD_MODULE_HEADER,
        #endif
        "say_hello",
        say_hello_functions,
        NULL,
        NULL,
        NULL,
        NULL,
        PHP_MINFO(say_hello),
        #if ZEND_MODULE_API_NO >= 20010901
        "0.1", /* Replace with version number for your extension */
        #endif
        STANDARD_MODULE_PROPERTIES
    };
    /* }}} */
    #ifdef COMPILE_DL_SAY_HELLO
    ZEND_GET_MODULE(say_hello)
    #endif
    /* {{{ PHP_MINFO_FUNCTION
    */
    PHP_MINFO_FUNCTION(say_hello)
    {
        php_info_print_table_start();
        php_info_print_table_header(2, "say_hello support", "enabled");
        php_info_print_table_row(2, "author", "Zhang Yang"); /* Replace with your name */
        php_info_print_table_end();
        /* Remove comments if you have entries in php.ini
        DISPLAY_INI_ENTRIES();
        */
    }
    /* }}} */

编译并安装扩展

在say_hello目录下输入下面命令:

  1. /usr/bin/phpize

  2. ./configure

  3. make

  4. make install

这样就完成了say_hello扩展的安装(如果没有报错的话)。

这时如果你去放置php扩展的目录下,会发现多了一个say_hello.so的文件。如下图所示:

下面就是将其加入到php.ini配置中,然后重启Apache(如果需要的话)。这些都是PHP基本配置的内容,我就不详述了。

扩展测试

如果上面顺利完成,这时运行phpinfo(),应该能看到如下信息:

这说明扩展已经安装成功了。然后我们编写一个测试用PHP脚本:

  1. <?php say_hello_func(&#39;Zhang Yang&#39;); ?>;

执行这个脚本,结果如下:

说明扩展已经正常工作了。

总结

这篇文章主要用示例方法介绍PHP Extension的开发基础。在PHP的使用中,也许是因为需要支持新的组件(如新的数据库),又或是业务需要或性能需要,几乎都会遇到需要开发PHP扩展的地方。后续如果有机会,我会写文章介绍一些关于扩展开发较为深入的东西,如扩展模块生命周期、INI使用以及编写面向对象的扩展模块等等。

以上是深入了解php的擴展開發的詳細內容。更多資訊請關注PHP中文網其他相關文章!

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