CodingLabs - PHP Extension 開發基礎

原文地址:http://blog.codinglabs.org/articles/php-extension-dev-guide.html

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 最新版本 5.3.3,格式爲 tar.bz2 壓縮包。下載地址爲:http://cn.php.net/get/php-5.3.3.tar.bz2/from/a/mirror

下載後,將源代碼移動到合適的目錄並解壓。解壓命令爲:

tar -jxvf 源碼包名稱

若下載的是 tar.gz 壓縮包,解壓命令爲

tar -zxvf 源碼包名稱

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

 

 

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

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

生成擴展組件框架

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

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

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

./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 文件,內容大致如下:

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” 開頭的全是註釋,所以真正起作用沒幾行。這裏需要配置的只有下面幾行:

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”,會看到裏面有怎麼一行:

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” 框架代碼了。

/* {{{ 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 的具體代碼:

/* {{{ 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 定義函數體

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 定義參數信息

參數信息是函數所必要部分,這裏不做深究,直接給出相應代碼:

ZEND_BEGIN_ARG_INFO(arginfo_say_hello_func, 0) ZEND_END_ARG_INFO()

如需瞭解具體信息請閱讀相關宏定義。

使用宏 PHP_FE 將函數加入到 say_hello_functions 中

最後,我們需要將剛纔定義的函數和參數信息加入到 say_hello_functions 數組裏,代碼如下:

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 全部代碼:

/*
+----------------------------------------------------------------------+
| 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          |
| [email protected] 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 目錄下輸入下面命令:

/usr/bin/phpize
./configure
make
make install

這樣就完成了 say_hello 擴展的安裝(如果沒有報錯的話)。

這時如果你去放置 php 擴展的目錄下,會發現多了一個 say_hello.so 的文件。如下圖所示:

 

下面就是將其加入到 php.ini 配置中,然後重啓 Apache(如果需要的話)。這些都是 PHP 基本配置的內容,我就不詳述了。

擴展測試

如果上面順利完成,這時運行 phpinfo(),應該能看到如下信息:

 

這說明擴展已經安裝成功了。然後我們編寫一個測試用 PHP 腳本:

<?php say_hello_func('Zhang Yang'); ?>;

執行這個腳本,結果如下:

 

說明擴展已經正常工作了。

這篇文章主要用示例方法介紹 PHP Extension 的開發基礎。在 PHP 的使用中,也許是因爲需要支持新的組件(如新的數據庫),又或是業務需要或性能需要,幾乎都會遇到需要開發 PHP 擴展的地方。後續如果有機會,我會寫文章介紹一些關於擴展開發較爲深入的東西,如擴展模塊生命週期、INI 使用以及編寫面向對象的擴展模塊等等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章