[翻譯][php擴展開發和嵌入式]第5章-您的第一個擴展

全部翻譯內容pdf文檔下載地址: http://download.csdn.net/detail/lgg201/5107012

本書目前在github上由laruence(http://www.laruence.com)和walu(http://www.walu.cc)兩位大牛組織翻譯. 該翻譯項目地址爲: https://github.com/walu/phpbook

原書名: <Extending and Embedding PHP>

原作者: Sara Golemon

譯者: goosman.lei(雷果國)

譯者Email: [email protected]

譯者Blog: http://blog.csdn.net/lgg201

你的第一個擴展

每一個php擴展的構建至少需要兩個文件: 一個configuration文件, 它告訴編譯期要構建哪些文件以及需要什麼外部的庫, 還需要至少一個源文件, 它執行實際的工作.

剖析擴展

實際上, 通常會有第二個或第三個配置文件, 以及一個或多個頭文件. 對於你的第一個擴展, 你需要添加每種類型的一個文件並使用它們工作.

配置文件

要開始了, 首先在你的php源代碼目錄樹的ext/目錄下創建名爲sample的目錄. 實際上這個新的目錄可以放在任何地方, 但是爲了在本章後面演示win32和靜態構建選項, 我們還是先建立在源代碼目錄下吧.

下一步, 進入這個目錄, 創建一個名爲config.m4的文件, 鍵入下面內容:

PHP_ARG_ENABLE(sample,
  [Whether to enable the "sample" extension],
  [  enable-sample        Enable "sample" extension support])

if test $PHP_SAMPLE != "no"; then
  PHP_SUBST(SAMPLE_SHARED_LIBADD)
  PHP_NEW_EXTENSION(sample, sample.c, $ext_shared)
fi

這是./configure時能夠調用enable-sample選項的最低要求.PHP_ARG_ENABLE的第二個參數將在./configure處理過程中到達這個擴展的配置文件時顯示. 第三個參數將在終端用戶執行./configure --help時顯示爲幫助信息.

有沒有想過爲什麼有的擴展配置使用enable-extname, 而有的擴展則使用with-extname? 功能上兩者沒有區別. 實際上, enable表示啓用這個特性不需要其他任何第三方庫, 相比之下, with則表示要使用這個特性還有其他先決條件

現在, 你的sample擴展並不需要和其他庫鏈接, 因此只需要使用enable版本. 在第17章"外部庫"中, 我們將介紹使用with並指示編譯器使用額外的CFLAGS和LDFLAGS設置.

如果終端用戶使用enable-sample選項調用了./configure, 那麼本地的環境變量$PHP_SAMPLE, 將被設置爲yes. PHP_SUBST()是標準autoconf的AC_SUBST()宏的php修改版, 它在將擴展構建爲共享模塊時需要.

最後但並不是不重要的, PHP_NEW_EXTENSION()定義了模塊並枚舉了所有必須作爲擴展的一部分編譯的源文件. 如果需要多個文件, 它可以在第二個參數中使用空格分隔列舉, 例如:

PHP_NEW_EXTENSION(sample, sample.c sample2.c sample3.c, $ext_shared)

最後一個參數是對應於PHP_SUBST(SAMPLE_SHARED_LIBADD)命令的, 在構建共享模塊的時候同樣需要它.

頭文件

當使用C開發的時候, 將數據的類型定義放到外部的頭文件中隔離起來, 由源文件包含是常見的做法. 儘管php並不要求這樣, 但是這樣做在模塊增長到不能放到單個源文件時是有簡化作用的.

在你的php_sample.h頭文件中, 以下面內容開始:

#ifndef PHP_SAMPLE_H
/* 防止重複包含 */
#define PHP_SAMPLE_H

/* 定義擴展的屬性 */
#define PHP_SAMPLE_EXTNAME    "sample"
#define PHP_SAMPLE_EXTVER    "1.0"

/* 在php源碼樹外面構建時引入配置選項 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

/* 包含php的標準頭文件 */
#include "php.h"

/* 定義入口點符號, Zend在加載這個模塊的時候使用 */
extern zend_module_entry sample_module_entry;
#define phpext_sample_ptr &sample_module_entry

#endif /* PHP_SAMPLE_H */

這個頭文件完成了兩個主要的任務: 如果擴展使用phpize工具構建(本書通常都使用這種方式), 那麼HAVE_CONFG_H就是已定義的, 這樣config.h就會被正常的包含進來. 無論擴展怎樣編譯, 都會從php源碼樹中包含php.h. 這個頭文件中包含了php源碼中訪問大部分PHPAPI要使用的其他頭文件.

接下來, 你的擴展使用的zend_module_entry結構定義爲外部的, 這樣當這個模塊使用extension=xxx加載時, 就可以被Zend使用dlopen和dlsym()取到.

譯註: 關於模塊的加載過程, 請參考譯者的一篇博客<dl('xxx.so');函數分析PHP模塊開發>(http://blog.csdn.net/lgg201/article/details/6584095)

頭文件中還會包含一些預處理, 定義將在原文件中使用的信息.

源代碼

最後, 最重要的你需要在sample.c中創建一個簡單的源碼骨架:

#include "php_sample.h"

zend_module_entry sample_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
     STANDARD_MODULE_HEADER,
#endif
    PHP_SAMPLE_EXTNAME,
    NULL, /* Functions */
    NULL, /* MINIT */
    NULL, /* MSHUTDOWN */
    NULL, /* RINIT */
    NULL, /* RSHUTDOWN */
    NULL, /* MINFO */
#if ZEND_MODULE_API_NO >= 20010901
    PHP_SAMPLE_EXTVER,
#endif
    STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_SAMPLE
ZEND_GET_MODULE(sample)
#endif

就這樣簡單. 這三個文件是創建一個模塊骨架所需的一切.但是, 它沒有任何功能, 不過作爲你在本節後面填充功能的模板是不錯的選擇. 不過先讓我們看看究竟發生了什麼.

開始的一行非常簡單, 包含了你剛纔創建的頭文件, 通過擴展得到了php源碼樹中的其他內核頭文件.

接下來, 創建你在頭文件中定義的zend_module_entry結構. 你應該注意到了, zend_module_entry的第一個元素是一個條件表達式, 給予當前的ZEND_MODULE_API_NO定義. 這個API編號大概是php4.2.0的, 如果你確定你的擴展不會安裝在比這還古老的版本, 你可以砍掉#ifdef部分, 直接包含STANDARD_MODULE_HEADER元素.

考慮一下, 無論如何, 它都會在編譯期耗費一點時間, 而不會對結果產生的二進制或處理需要的時間產生影響, 因此多數情況下最好直接砍掉這個條件. 這同樣適用於下面的版本屬性.

其他的6個元素現在初始設置爲NULL; 你可以在它後面的註釋中看到它的用途.

最後, 在最底下你可以看到每個可以編譯爲共享模塊的php擴展都會有的一個公共元素. 這個簡短的條件在你動態加載時由Zend增加一個引用. 不要關心它的細節, 你只需要保證它的存在, 否則下一節可能就無法工作了.

構建你的第一個擴展

現在你擁有了所有的文件, 是時候編譯安裝了. 相比編譯主php二進制, 步驟上略有不同.

在*nix上構建

第一步是使用config.m4中的信息作爲末班生成./configure腳本. 這可以運行在你安裝主php二進制時附帶安裝的phpize程序來完成:

$ phpize
PHP Api Version: 20041225
Zend Module Api No: 20050617
Zend Extension Api No: 220050617

Zend Extension Api No前面多出來的2並不是印刷錯誤; 它對應於Zend引擎2這個版本號, 一般認爲要保持這個API編號大於它對應的ZE1版本.

如果你此時查看當前目錄 你會注意到比剛纔的3個文件多了不少文件. phpize程序結合你擴展的config.m4文件以及從你的php構建中收集的信息和所有讓編譯發生所需的一切. 這意味着你不用糾纏在makefile和定位php頭上面. php已經幫你做了這個工作.

下一步就簡單了, 執行./configure. 這裏你只要配置你的擴展, 因此你需要做的如下:

$ ./configure --enable-sample

注意這裏沒有使用enable-debug和enable-maintainer-zts. 這是因爲phpize已經將它們的值從主php構建中拿過來並應用到你的擴展的./configure腳本中了.

現在, 構建它! 和其他任何的包一樣, 你只需要鍵入make, 生成的腳本文件就會處理剩下的事情.

構建處理完成後, 你會得到一個消息指出sample.so已經編譯並放在了當前構建目錄下一個名爲"modules"的目錄中.

在windows上構建

譯者不熟悉windows平臺, 因此略過.

將構建的擴展作爲共享模塊加載

在請求的時候爲了讓php找到這個模塊, 需要將它放到php.ini中extension_dir設置的目錄下. 默認的php.ini放在/usr/local/lib/php.ini; 不過這個默認值可能會因爲包管理系統而不同. 檢查php -i的輸出可以看到你這個配置文件在哪裏.

如果php.ini中的這個設置沒有修改過, 它的值默認是"PHP_HOME/lib/php/extensions/debug-zts-20100525", 後面的debug-zts-20100525分別是是否啓用調試, 是否啓用zts, PHPAPI編號. 如果你還沒有已加載的擴展, 或者說除了sample.so沒有其他擴展, 可以將這個值修改到你make產生模塊的路徑. 否則, 直接將產生的sample.so拷貝到這個設置的目錄下.(譯註: php -i | grep extension_dir查找你的extension_dir設置)

在extension_dir指向正確的位置後, 有兩種方式告訴php去加載你的模塊. 第一種是在腳本中使用dl()函數:

<?php
    dl('sample.so');
    var_dump(get_loaded_modules());
?>

如果腳本沒有顯示sample已加載, 則表示有哪裏出了問題. 查看輸出上面的錯誤消息作爲線索, 或者如果在php.ini中進行了相應的設置就參考error_log.

第二種方式, 也是更常用的方式, 在php.ini中使用extension指令指定要加載的模塊. 這個指令在php.ini的設置中是比較特殊的, 可以多次以不同的值使用它. 因此如果你已經在php.ini中設置了一個擴展, 不要在同一行使用分隔符的方式列舉, 而是插入新的一行: extension=sample.so. 此時你的php.ini看起來是這樣的:

extension_dir=/usr/local/lib/php/modules/
extension=sample.so

現在你可以不使用dl()運行相同的腳本, 或者直接執行php -m命令, 就可以在已加載模塊列表中看到sample了.

所有本章剩餘以及以後章節的代碼, 都假設你已經按照這裏描述的方法加載了當前擴展. 如果你計劃使用dl(), 請確認在測試腳本中加入加載的代碼(dl()).

靜態構建

在已加載模塊列表中, 你可能注意到了一些在php.ini中並沒有使用extension指令包含的模塊. 這些模塊是直接構建到php中的, 它們作爲主php程序的一部分被編譯進php中.

在*nix下靜態構建

現在, 如果你現在進入到php源碼樹的根目錄, 運行./configure --help, 你會看到雖然你的擴展和所有其他模塊都在ext/目錄下, 但它並沒有作爲一個選項列出. 這是因爲, 此刻, ./configure腳本已經生成了, 而你的擴展並不知道. 要重新生成./configure並讓它找到你的新擴展, 你需要做的只是執行一條命令:

$ ./buildconf

如果你使用產品發佈版的php做開發, 你會發現./buildconf自己不能工作. 這種情況下, 你需要執行: ./buildconf --force來繞過對./configure命令的一些保護.

現在你執行./configure --help就可以看到--enable-sample是一個可用選項了. 此時, 你就可以重新執行./configure, 使用你原來構建主php時使用的所有選項, 外加--enable-sample, 這樣構建出來的php二進制文件就是完整的, 包含你自己擴展的程序.

當然, 這樣做還有點早. 你的擴展除了佔用空間還應該做一些事情.

windows下靜態構建

譯者不熟悉windows環境, 因此略過.

功能函數

在用戶空間和擴展代碼之間最快捷的鏈接就是PHP_FUNCTION(). 首先在你的sample.c文件頂部, #include "php_sample.h"之後增加下面代碼:

PHP_FUNCTION(sample_hello_world)
{
    php_printf("Hello World!\n");
}

PHP_FUNCTION()宏函數就像一個普通的C函數定義, 因爲它按照下面方式展開:

#define PHP_FUNCTION(name) \
    void zif_##name(INTERNAL_FUNCTION_PARAMETERS)

這種情況下, 它就等價於:

void zif_sample_hello_world(zval *return_value,
    char return_value_used, zval *this_ptr TSRMLS_DC)

當然, 只定義函數還不夠. 引擎需要知道函數的地址以及應該暴露給用戶空間的函數名. 這通過下一個代碼塊完成, 你需要在PHP_FUNCTION()塊後面增加:

static function_entry php_sample_functions[] = {
    PHP_FE(sample_hello_world,        NULL)
    { NULL, NULL, NULL }
};

php_sample_functions向量是一個簡單的NULL結束向量, 它會隨着你向擴展中增加功能而變大. 每個你要暴露出去的函數都需要在這個向量中給出說明. 展開PHP_FE()宏如下:

{ "sample_hello_world", zif_sample_hello_world, NULL},

這樣提供了新函數的名字, 以及指向它的實現函數的指針. 這裏第三個參數用於提供暗示信息, 比如某些參數需要引用傳值. 在第7章"接受參數"你將看到這個特性.

現在, 你有了一個要暴露的函數列表, 但是仍然沒有連接到引擎. 這通過對sample.c的最後一個修改完成, 將你的sample_module_entry結構體中的NULL, /* Functions */一行用php_sample_functions替換(請確保留下那個逗號).

現在, 通過前面介紹的方式重新構建, 使用php命令行的-r選項進行測試, -r允許你不用創建文件直接在命令行運行簡單的代碼片段:

$ php -r 'sample_hello_world();

如果一切OK的話, 你將會看到輸出"Hello World!". 恭喜!!!

Zend內部函數

內部函數名前綴"zif_"是"Zend內部函數"的命名標準, 它用來避免可能的符號衝突. 比如, 用戶空間的strlen()函數並沒有實現爲void strlen(INTERNAL_FUNCTION_PARAMETERS), 因爲它會和C庫的strlen衝突.

zif_的前綴也並不能完全避免名字衝突的問題. 因此, php提供了可以使用任意名字定義內部函數的宏: PHP_NAMED_FUNCTION(); 例如PHP_NAMED_FUNCTION(zif_sample_hello_world)等同於前面使用的PHP_FUNCTION(sample_hello_world)

當使用PHP_NAMED_FUNCTION定義實現時, 在function_entry向量中, 可以對應使用PHP_NAMED_FE()宏. 因此, 如果你定義了自己的函數PHP_NAMED_FUNCTION(purplefunc), 就要使用PHP_NAMED_FE(sample_hello_world, purplefunc, NULL), 而不是使用PHP_FE(sample_hello_world, NULL).

我們可以在ext/standard/file.c中查看fopen()函數的實現, 它實際上使用PHP_NAMED_FUNCTION(php_if_fopen)定義.從用戶空間角度來看, 它並不關心函數是什麼東西, 只是簡單的調用fopen(). 

函數別名

有時一個函數可能會有不止一個名字. 回想一下, 普通的函數內部定義是用戶空間函數名加上zif_前綴, 我們可以看到用PHP_NAMED_FE()宏可以很容易的創建這個可選映射.

PHP_FE(sample_hello_world,    NULL)
PHP_NAMED_FE(sample_hi,    zif_sample_hello_world,     NULL)

PHP_FE()宏將用戶空間函數名sample_hello_world和PHP_FUNCTION(sample_hello_world)展開而來的zif_sample_hello_world關聯起來. PHP_NAMED_FE()宏則將用戶空間函數名sample_hi和同一個內部實現關聯起來.

現在, 假設Zend引擎發生了一個大的變更, 內部函數的前綴從zif_修改爲pif_了. 這個時候, 你的擴展就不能工作了, 因爲當到達PHP_NAMED_FE()時, 發現zif_sample_hello_world沒有定義.

這種情況並不常見, 但非常麻煩, 可以使用PHP_FNAME()宏展開sample_hello_world避免這個問題:

PHP_NAMED_FE(sample_hi, PHP_FNAME(sample_hello_world), NULL)

這種情況下, 即便函數前綴被修改, 擴展的zend_function_entry也會使用宏擴展自動的更新.

現在, 你這樣做已經可以工作了, 但是我們已經不需要這樣做了. php暴露了另外一個宏, 專門設計用於創建函數別名. 前面的例子可以如下重寫:

PHP_FALIAS(sample_hi, sample_hello_world, NULL)

實際上這是官方的創建函數別名的方法, 在php源碼樹中你時常會看到它.

小結

本章你創建了一個簡單的可工作的php擴展, 並學習了在主要平臺上構建它需要的步驟. 在以後的章節中, 你將會繼續豐滿這個擴展, 最終讓它包含php的所有特性.

php源碼樹和它編譯/構建在各個平臺上依賴的工具經常會發生變化, 如果本章介紹的某些地方不能正常工作, 請參考php.net在線手冊的Installation一掌, 查看你使用的版本的特殊需求.



目錄

上一章: 安裝構建環境

下一章: 返回值

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