Zend API:深入 PHP 內核


Zend API:深入 PHP 內核  本文轉載自(http://www.woxihuan.com/17707858/1317300211103943.shtml)


網上關於 PHP 的資料多如牛毛,關於其核心 Zend Engine 的卻少之又少。PHP 中文手冊出現已 N 年,但 Zend API 的翻譯卻仍然不見動靜,小弟自覺對 Zend Engine 略有小窺,並且翻譯也有助於強迫自己對文章的進一步理解,於是嘗試翻譯此章,英文不好,恭請方家指點校覈。轉載請註明來自撫琴居(譯者主頁):http://www.yAnbiN.org

PHP 中文手冊《Zend API: 深入PHP內核》目錄

(一)摘要

知者不言,言者不知。 ――老子《道德經》五十六章

有時候,單純依靠 PHP “本身”是不行的。儘管普通用戶很少遇到這種情況,但一些專業性的應用則經常需要將 PHP 的性能發揮到極致(這裏的性能是指速度或功能)。由於受到 PHP 語言本身的限制,同時還可能不得不把龐大的庫文件包含到每個腳本當中。因此,某些新功能並不是總能被順利實現,所以我們必須另外尋找一些方法來克服 PHP 的這些缺點。

瞭解到了這一點,我們就應該接觸一下 PHP 的心臟並探究一下它的內核-可以編譯成 PHP 並讓之工作的 C 代碼-的時候了。

(二)概述

“擴展 PHP”說起來容易做起來難。PHP 現在已經發展成了一個具有數兆字節源代碼的非常成熟的系統。要想深入這樣的一個系統,有很多東西需要學習和考慮。在寫這一章節的時候,我們最終決定採用“邊學邊做”的方式。這也許並不是最科學和專業的方式,但卻應該是最有趣和最有效的一種方式。在下面的小節裏,你首先會非常快速的學習到如何寫一個雖然很基礎但卻能立即運行的擴展,然後將會學習到有關 Zend API 的高級功能。另外一個選擇就是將其作爲一個整體,一次性的講述所有的這些操作、設計、技巧和訣竅等,並且可以讓我們在實際動手前就可以得到一副完整的願景。這看起來似乎是一個更好的方法,也沒有死角,但它卻枯燥無味、費時費力,很容易讓人感到氣餒。這就是我們爲什麼要採用非常直接的講法的原因。

注意,儘管這一章會儘可能多講述一些關於 PHP 內部工作機制的知識,但要想真的給出一份在任何時間任何情況下的PHP 擴展指南,那簡直是不可能的。PHP 是如此龐大和複雜,以致於只有你親自動手實踐一下才有可能真正理解它的內部工作機制,因此我們強烈推薦你隨時參考它的源代碼來進行工作。

Zend 是什麼? PHP 又是什麼?

Zend 指的是語言引擎,PHP 指的是我們從外面看到的一套完整的系統。這聽起來有點糊塗,但其實並不複雜(見圖3-1 PHP 內部結構圖)。爲了實現一個 WEB 腳本的解釋器,你需要完成以下三個部分的工作:

解釋器部分:負責對輸入代碼的分析、翻譯和執行;

功能性部分:負責具體實現語言的各種功能(比如它的函數等等);

接口部分:負責同 WEB 服務器的會話等功能。

Zend 包括了第一部分的全部和第二部分的局部,PHP 包括了第二部分的局部和第三部分的全部。他們合起來稱之爲 PHP 包。Zend 構成了語言的核心,同時也包含了一些最基本的 PHP 預定義函數的實現。PHP 則包含了所有創造出語言本身各種顯著特性的模塊。



圖3-1 PHP 內部結構圖


下面將要討論PHP 允許在哪裏擴展以及如何擴展。

(三)可擴展性

正如上圖(圖3-1 PHP 內部結構圖)所示,PHP 主要以三種方式來進行擴展:外部模塊,內建模塊和 Zend 引擎。下面我們將分別討論這些方式:

外部模塊

外部模塊可以在腳本運行時使用 dl() 函數載入。這個函數從磁盤載入一個共享對象並將它的功能與調用該函數的腳本進行綁定並使之生效。腳本終止後,這個外部模塊將在內存中被丟棄。這種方式有利有弊,如下表所示:

優點 缺點
外部模塊不需要重新對 PHP 進行編譯。 共享對象在每次腳本調用時都需要對其進行加載,速度較慢。
PHP通過“外包”方式來讓自身的體積保持很小。 附加的外部模塊文件會讓磁盤變得比較散亂。
  每個想使用該模塊功能的腳本都必須使用dl() 函數手動加載,或者在 php.ini 文件當中添加一些擴展標籤(這並不總是一個恰當的解決方案)。

綜上所述,外部模塊非常適合開發第三方產品,較少使用的附加的小功能或者僅僅是調試等這些用途。爲了迅速開發一些附加功能,外部模塊是最佳方式。但對於一些經常使用的、實現較大的,代碼較爲複雜的應用,那就有些得不償失了。

第三方可能會考慮在 php.ini 文件中使用擴展標籤來創建一個新的外部模塊。這些外部模塊完全同主PHP 包分離,這一點非常適合應用於一些商業環境。商業性的發行商可以僅發送這些外部模塊而不必再額外創建那些並不允許綁定這些商業模塊的PHP 二進制代碼。

內建模塊

內建模塊被直接編譯進 PHP 並存在於每一個 PHP 處理請求當中。它們的功能在腳本開始運行時立即生效。和外部模塊一樣,內建模塊也有一下利弊:

優點 缺點
無需專門手動載入,功能即時生效。 修改內建模塊時需要重新編譯PHP。
無需額外的磁盤文件,所有功能均內置在 PHP 二進制代碼當中。 PHP 二進制文件會變大並且會消耗更多的內存。

Zend 引擎

當然,你也能直接在 Zend 引擎裏面進行擴展。如果你需要在語言特性方面做些改動或者是需要在語言核心內置一些特別的功能,那麼這就是一種很好的方式。但一般情況下應該盡力避免對 Zend 引擎的修改。這裏面的改動會導致和其他代碼的不兼容,而且幾乎沒有人會適應打過特殊補丁的 Zend 引擎。況且這些改動與主 PHP 源代碼是不可分割的,因此就有可能在下一次的官方的源代碼更新中被覆蓋掉。因此,這種方式通常被認爲是“不良的習慣”。由於使用極其稀少,本章將不再對此進行贅述。

(四)源碼佈局

在我們開始討論具體編碼這個話題前,你應該讓自己熟悉一下 PHP 的源代碼樹以便可以迅速地對各個源文件進行定位。這也是編寫和調試 PHP 擴展所必須具備的一種能力。

下表列出了一些主要目錄的內容:

目錄 內容
php-src 包含了PHP主源文件和主頭文件;在這裏你可以找到所有的 PHP API 定義、宏等內容。(重要). 其他的一些東西你也可以在這裏找到。
php-src/ext 這裏是存放動態和內建模塊的倉庫;默認情況下,這些就是被集成於主源碼樹中的“官方” PHP 模塊。自 PHP 4.0開始,這些PHP標準擴展都可以編譯爲動態可載入的模塊。(至少這些是可以的)。
php-src/main 這個目錄包含主要的 PHP 宏和定義。 (重要)
php-src/pear 這個目錄就是“PHP 擴展與應用倉庫”的目錄。包含了PEAR 的核心文件。
php-src/sapi 包含了不同服務器抽象層的代碼。
TSRM Zend 和 PHP的 “線程安全資源管理器” (TSRM) 目錄。

除此之外,你也應該注意一下這些文件所包含的一些文件。舉例來說,哪些文件與 Zend 執行器有關,哪些文件又爲 PHP 初始化工作提供了支持等等。在閱讀完這些文件之後,你還可以花點時間再圍繞PHP包來看一些文件,瞭解一下這些文件和模塊之間的依賴性――它們之間是如何依賴於別的文件又是如何爲其他文件提供支持的。同時這也可以幫助你適應一下 PHP 創作者們代碼的風格。要想擴展 PHP,你應該儘快適應這種風格。

擴展規範

Zend 是用一些特定的規範構建的。爲了避免破壞這些規範,你應該遵循以下的幾個規則:

1. 宏

幾乎對於每一項重要的任務,Zend 都預先提供了極爲方便的宏。在下面章節的圖表裏將會描述到大部分基本函數、結構和宏。這些宏定義大多可以在 Zend.h 和 Zend_API.h 中找到。我們建議您在學習完本節之後仔細看一下這些文件。(當然你也可以現在就閱讀這些文件,但你可能不會留下太多的印象。)

2. 內存管理

資源管理仍然是一個極爲關鍵的問題,尤其是對服務器軟件而言。資源裏最具寶貴的則非內存莫屬了,內存管理也必須極端小心。內存管理在 Zend 中已經被部分抽象,而且你也應該堅持使用這些抽象,原因顯而易見:由於得以抽象,Zend 就可以完全控制內存的分配。Zend 可以確定一塊內存是否在使用,也可以自動釋放未使用和失去引用的內存塊,因此就可以避免內存泄漏。下表列出了一些常用函數:

函數 描述
emalloc() 用於替代 malloc()。
efree() 用於替代 free()。
estrdup() 用於替代 strdup()。
estrndup() 用於替代strndup()。速度要快於 estrdup() 而且是二進制安全的。如果你在複製之前預先知道這個字符串的長度那就推薦你使用這個函數。
ecalloc() 用於替代 calloc()。
erealloc() 用於替代 realloc()。

emalloc(), estrdup(), estrndup(), ecalloc(), 和 erealloc() 用於申請內部的內存,efree() 則用來釋放這些前面這些函數申請的內存。e*() 函數所用到的內存僅對當前本地的處理請求有效,並且會在腳本執行完畢,處理請求終止時被釋放。

Zend 還有一個線程安全資源管理器,這可以爲多線程WEB 服務器提供更好的本地支持。不過這需要你爲所有的全局變量申請一個局部結構來支持併發線程。但是因爲在寫本章內容時Zend 的線程安全模式仍未完成,因此我們無法過多地涉及這個話題。

3. 目錄與文件函數

下列目錄與文件函數應該在 Zend 模塊內使用。它們的表現和對應的 C 語言版本完全一致,只是在線程級提供了虛擬目錄的支持。

Zend 函數 對應的 C 函數
V_GETCWD() getcwd()
V_FOPEN() fopen()
V_OPEN() open()
V_CHDIR() chdir()
V_GETWD() getwd()
V_CHDIR_FILE() 將當前的工作目錄切換到一個以文件名爲參數的該文件所在的目錄。
V_STAT() stat()
V_LSTAT() lstat()
4. 字符串處理

在 Zend 引擎中,與處理諸如整數、布爾值等這些無需爲其保存的值而額外申請內存的簡單類型不同,如果你想從一個函數返回一個字符串,或往符號表新建一個字符串變量,或做其他類似的事情,那你就必須確認是否已經使用上面的 e*() 等函數爲這些字符串申請內存。(你可能對此沒有多大的感覺。無所謂,現在你只需在腦子裏有點印象即可,我們稍後就會再次回到這個話題)

5. 複雜類型

像數組和對象等這些複雜類型需要另外不同的處理。它們被出存在哈希表中,Zend 提供了一些簡單的 API 來操作這些類型。

(五)自動構建系統

PHP 提供了一套非常靈活的自動構建系統(automatic build system),它把所有的模塊均放在 Ext 子目錄下。每個模塊除自身的源代碼外,還都有一個用來配置該擴展的 config.m4 文件(詳情請參見http://www.gnu.org/software/m4/manual/m4.html)。

包括 .cvsignore 在內的所有文件都是由位於 Ext 目錄下的 ext_skel 腳本自動生成的,它的參數就是你想創建模塊的名稱。這個腳本會創建一個與模塊名相同的目錄,裏面包含了與該模塊對應的一些的文件。

下面是操作步驟:

:~/cvs/php4/ext:> ./ext_skel --extname=my_module

響應如下:

Creating directory my_module
Creating basic files: config.m4 .cvsignore my_module.c php_my_module.h CREDITS EXPERIMENTAL tests/001.phpt my_module.php [done].

To use your new extension, you will have to execute the following steps:
1. $ cd ..
2. $ vi ext/my_module/config.m4
3. $ ./buildconf
4. $ ./configure --[with|enable]-my_module
5. $ make
6. $ ./php -f ext/my_module/my_module.php
7. $ vi ext/my_module/my_module.c
8. $ make

Repeat steps 3-6 until you are satisfied with ext/my_module/config.m4 and step 6 confirms that your module is compiled into PHP. Then, start writing code and repeat the last two steps as often as necessary.

這些指令就會生成前面所說的那些文件。爲了能夠在自動配置文件和構建程序中包含新增加的模塊,你還需要再運行一次 buildconf 命令。這個命令會通過搜索 Ext 目錄和查找所有 config.m4 文件來重新生成 configure 腳本。默認情況下的的 config.m4 文件如例 3-1 所示,看起來可能會稍嫌複雜:

例3.1 默認的 config.m4 文件

如果你不太熟悉 M4 文件(現在毫無疑問是熟悉 M4 文件的大好時機),那麼就可能會有點糊塗。但是別擔心,其實非常簡單。

注意:凡是帶有 dnl 前綴的都是註釋,註釋是不被解析的。

config.m4 文件負責在配置時解析 configure 的命令行選項。這就是說它將檢查所需的外部文件並且要做一些類似配置與安裝的任務。

默認的配置文件將會在 configure 腳本中產生兩個配置指令:--with-my_module 和 --enable-my_module。當需要引用外部文件時使用第一個選項(就像用 --with-apache 指令來引用 Apache 的目錄一樣)。第二個選項可以讓用戶簡單的決定是否要啓用該擴展。不管你使用哪一個指令,你都應該註釋掉另外一個。也就是說,如果你使用了 --enable-my_module,那就應該去掉--with-my_module。反之亦然。

默認情況下,通過 ext_skel 創建的 config.m4 都能接受指令,並且會自動啓用該擴展。啓用該擴展是通過 PHP_EXTENSION 這個宏進行的。如果你要改變一下默認的情況,想讓用戶明確的使用 --enable-my_module 或 --with-my_module 指令來把擴展包含在 PHP 二進制文件當中,那麼將 if test "$PHP_MY_MODULE" != "no"改爲if test "$PHP_MY_MODULE" == "yes"即可。

if test "$PHP_MY_MODULE" == "yes"; then dnl
Action.. PHP_EXTENSION(my_module, $ext_shared)
fi

這樣就會導致在每次重新配置和編譯 PHP 時都要求用戶使用 --enable-my_module 指令。

另外請注意在修改 config.m4 文件後需要重新運行 buildconf 命令。

(六)開始創建擴展

我們先來創建一個非常簡單的擴展,這個擴展除了一個將其整形參數作爲返回值的函數外幾乎什麼都沒有。下面(“例3-2 一個簡單的擴展”)就是這個樣例的代碼:

例3.2 一個簡單的擴展

這段代碼已經包含了一個完整的 PHP 模塊。稍後我們會詳細解釋這段代碼,現在讓我們先討論一下構建過程。(在我們討論 API 函數前,這可以讓心急的人先實驗一下。)

模塊的編譯

模塊的編譯基本上有兩種方法:

在 Ext 目錄內使用“make” 機制,這種機制也可以編譯出動態可加載模塊。

手動編譯源代碼。

第一種方法明顯受到人們的偏愛。自 PHP 4.0 以來,這也被標準化成了一個的複雜的構建過程。這種複雜性也導致了難於被理解這個缺點。在本章最後我們會更詳細的討論這些內容,但現在還是讓我們使用默認的 make 文件吧。

第二種方法很適合那些(因爲某種原因而)沒有完整 PHP 源碼樹的或者是很喜歡敲鍵盤的人。雖然這些情況是比較罕見,但爲了內容的完整性我們也會介紹一下這種方法。

使用 make 進行編譯

爲了能夠使用這種標準機制流程來編譯這些代碼,讓我們把它所有的子目錄都複製到 PHP 源碼樹的 Ext 目錄下。然後運行 buildconf 命令,這將會創建一個新的包含了與我們的擴展相對應的選項的 configure 腳本。默認情況下,樣例中的所有代碼都是未激活的,因此你不用擔心會破壞你的構建程序。在 buildconf 執行完畢後,再使用 configure --help 命令就會顯示出下面的附加模塊:

--enable-array_experiments BOOK: Enables array experiments
--enable-call_userland BOOK: Enables userland module
--enable-cross_conversion BOOK: Enables cross-conversion module
--enable-first_module BOOK: Enables first module
--enable-infoprint BOOK: Enables infoprint module
--enable-reference_test BOOK: Enables reference test module
--enable-resource_test BOOK: Enables resource test module
--enable-variable_creation BOOK: Enables variable-creation module

前面樣例(“例3-2 一個簡單的擴展”)中的模塊(first_module)可以使用 --enable-first_module 或 enable-first_module=yes 來激活。

手動編譯

手動編譯需要運行以下命令:

動作 命令
編譯 cc -fpic -DCOMPILE_DL_FIRST_MODULE=1 -I/usr/local/include -I. -I.. -I../Zend -c -o <your_object_file> <your_c_file>
連接 cc -shared -L/usr/local/lib -rdynamic -o <your_module_file> <your_object_file(s)>

    編譯命令只是簡單的讓編譯器產生一些中間代碼(不要忽略了--fpic 參數),然後又定義了COMPILE_DL 常量來通知代碼這是要編譯爲一個動態可加載的模塊(通常用來測試,我們稍後會討論它)。這些選項後面是一些編譯這些源代碼所必須包含的庫文件目錄。

    注意:
    本例中所有 include 的路徑都是都是 Ext 目錄的相對路徑。如果您是在其他目錄編譯的這些源文件,那麼還要相應的修改路徑名。編譯所需要的目錄有 PHP 目錄,Zend 目錄和模塊所在的目錄(如果有必要的話)。

    連接命令也是一個非常簡單的把模塊連接成一個動態模塊的命令。

你可以在編譯指令中加入優化選項,儘管這些已經在樣例中忽略了(不過你還是可以從前面討論的 make 模版文件中發現一些)。

注意:
手動將模塊靜態編譯和連接到 PHP 二進制代碼的指令很長很長,因此我們在這裏不作討論。(手動輸入那些指令是很低效的。)

(七)使用擴展

根據你所選擇的不同的構建過程,你要麼把擴展編譯進一個新的PHP 的二進制文件,然後再連接到 Web 服務器(或以CGI 模式運行),要麼將其編譯成一個 .so (共享庫)文件。如果你將上面的樣例文件 first_module.c 編譯成了一個共享庫,那麼編譯後的文件應該是 first_module.so。要想使用它,你就必須把他複製到一個 PHP 能訪問到的地方。如果僅僅是爲了測試的話,簡單起見,你可以把它複製到你的 htdocs 目錄下,然後用“例3.3 first_module.so 的一個測試文件”中的代碼來進行一下測試。如果你將其直接編譯編譯進 PHP 二進制文件的話,那就不用調用 dl() 函數了,因爲這個模塊的函數在腳本一開始運行就生效了。

警告:
爲了安全起見,你不應該將你的動態模塊放入一個公共目錄。即使是一個簡單的測試你可以那麼做,那也應該把它放進產品環境中的一個隔離的目錄。

例3.3 first_module.so 的一個測試文件

調用這個測試文件,結果應該輸出爲:

We sent '2' and got '2'。

若有需要,你可以調用 dl() 函數來載入一個動態可加載模塊。這個函數負責尋找指定的共享庫並進行加載使其函數在 PHP 中生效。這個樣例模塊僅輸出了一個函數 first_module(),這個函數僅接受一個參數,並將其轉換爲整數作爲函數的結果返回。

如果你已經進行到了這一步,那麼,恭喜你,你已經成功創建了你的第一個 PHP 擴展!

(八)故障處理

實際上,在對靜態或動態模塊進行編譯時沒有太多故障處理工作要做。唯一可能的問題就是編譯器會警告說找不到某些定義或者類似的事情。如果出現這種情況,你應該確認一下所有的頭文件都是可用的並且它們的路徑都已經在編譯命令中被指定。爲了確保每個文件都能被正確地定位,你可以先提取一個乾淨的 PHP 源碼樹,然後在 Ext 目錄使用自動構建工具來創建這些文件。用這種方法就可以確保一個安全的編譯環境。假如這樣也不行,那就只好試試手動編譯了。

PHP 也可能會警告說在你的模塊裏面有一些未定義的函數。(如果你沒有改動樣例文件的話這種情況應該不會發生。)假如你在模塊中拼錯了一些你想訪問的外部函數的名字,那麼它們就會在符號表中顯示爲“未能連接的符號”。這樣在 PHP 動態加載或連接時,它們就不會運行--在二進制文件中沒有相應的符號。爲了解決這個問題,你可以在你的模塊文件中找一下錯誤的聲明或外部引用。注意,這個問題僅僅發生在動態可加載模塊身上。而在靜態模塊身上則不會發生,因爲靜態模塊在編譯時就會拋出這些錯誤。

(九)關於模塊代碼的討論

OK,現在你已經有了一個安全的構建環境,也可以把模塊編譯進 PHP 了。那麼,現在就讓我們開始詳細討論一下這裏面究竟是如何工作的吧~

模塊結構

所有的 PHP 模塊通常都包含以下幾個部分:

包含頭文件(引入所需要的宏、API定義等);

聲明導出函數(用於 Zend 函數塊的聲明);

聲明 Zend 函數塊;

聲明 Zend 模塊;

實現 get_module() 函數;

實現導出函數。

包含頭文件

模塊所必須包含的頭文件僅有一個 php.h,它位於 main 目錄下。這個文件包含了構建模塊時所必需的各種宏和API 定義。

小提示: 
專門爲模塊創建一個含有其特有信息的頭文件是一個很好的習慣。這個頭文件應該包含 php.h 和所有導出函數的定義。如果你是使用 ext_skel 來創建模塊的話,那麼你可能已經有了這個文件,因爲這個文件會被 ext_skel 自動生成。

聲明導出函數

爲了聲明導出函數(也就是讓其成爲可以被 PHP 腳本直接調用的原生函數),Zend 提供了一個宏來幫助完成這樣一個聲明。代碼如下:

ZEND_FUNCTION ( my_function );

ZEND_FUNCTION 聲明瞭一個使用 Zend 內部 API 來編譯的新的C 函數。這個 C 函數是 void 類型,以 INTERNAL_FUNCTION_PARAMETERS (這是另一個宏)爲參數,而且函數名字以 zif_ 爲前綴。把上面這句聲明展開可以得到這樣的代碼:

voidzif_my_function ( INTERNAL_FUNCTION_PARAMETERS );

接着再把 INTERNAL_FUNCTION_PARAMETERS 展開就會得到這樣一個結果:

在解釋器(interpreter)和執行器(executor)被分離出PHP 包後,這裏面(指的是解釋器和執行器)原有的一些 API 定義及宏也漸漸演變成了一套新的 API 系統:Zend API。如今的 Zend API 已經承擔了很多原來(指的是分離之前)本屬於 PHP API 的職責,大量的 PHP API 被以別名的方式簡化爲對應的 Zend API。我們推薦您應該儘可能地使用 Zend API,PHP API 只是因爲兼容性原因才被保留下來。舉例來說, zval 和 pval 其實是同一類型,只不過 zval 定義在 Zend 部分,而 pval 定義在 PHP 部分(實際上 pval 根本就是 <code>zval 的一個別名)。但由於 INTERNAL_FUNCTION_PARAMETERS 是一個 Zend 宏,因此我們在上面的聲明中使用了 zval 。在編寫代碼時,你也應該總是使用 zval 以遵循新的 Zend API 規範。

這個聲明中的參數列表非常重要,你應該牢記於心。(表 3.1 “PHP 調用函數的 Zend 參數”詳細介紹了這些參數)

表3.1 PHP 調用函數的 Zend 參數

參數 說明
ht 這個參數包含了Zend 參數的個數。但你不應該直接訪問這個值,而是應該通過 ZEND_NUM_ARGS() 宏來獲取參數的個數。
return_value 這個參數用來保存函數向 PHP 返回的值。訪問這個變量的最佳方式也是用一系列的宏。後面我們會有詳細說明。
this_ptr 根據這個參數你可以訪問該函數所在的對象(換句話說,此時這個函數應該是一個類的“方法”)。推薦使用函數 getThis() 來得到這個值。
return_value_used 這個值主要用來標識函數的返回值是否爲腳本所使用。0 表示腳本不使用其返回值,而 1 則相反。通常用於檢驗函數是否被正確調用以及速度優化方面,這是因爲返回一個值是一種代價很昂貴的操作(可以在 array.c 裏面看一下是如何利用這一特性的)。
executor_globals 這個變量指向 Zend Engine 的全局設置,在創建新變量時這個這個值會很有用。我們也可以函數中使用宏 TSRMLS_FETCH() 來引用這個值。

聲明 Zend 函數塊

現在你已經聲明瞭導出函數,除此之外你還必須得將其引入 Zend 。這些函數的引入是通過一個包含有 N 個 zend_function_entry 結構的數組來完成的。數組的每一項都對應於一個外部可見的函數,每一項都包含了某個函數在 PHP 中出現的名字以及在 C 代碼中所定義的名字。zend_function_entry 的內部定義如“例3.4 zend_function_entry 的內部聲明”所示:

例3.4 zend_function_entry 的內部聲明

字段 說明
fname 指定在 PHP 裏所見到的函數名(比如:fopen、mysql_connect 或者是我們樣例中的 first_module)。
handler 指向對應 C 函數的句柄。樣例可以參考前面使用宏INTERNAL_FUNCTION_PARAMETERS 的函數聲明。
func_arg_types 用來標識一些參數是否要強制性地按引用方式進行傳遞。通常應將其設定爲 NULL。

對於上面的例子,我們可以這樣來聲明:

zend_function_entryfirstmod_functions[] =
{
    ZEND_FE(first_module, NULL)
    {NULL, NULL, NULL}
};

你可能已經看到了,這個結構的最後一項是 {NULL, NULL, NULL} 。事實上,這個結構的最後一項也必須始終是 {NULL, NULL, NULL},因爲 Zend Engine 需要靠它來確認這些導出函數的列表是否列舉完畢。

注意:
你不應該使用一個預定義的宏來代替列表的結尾部分(即{NULL, NULL, NULL}),因爲編譯器會盡量尋找一個名爲 NULL 的函數的指針來代替 NULL !

宏 ZEND_FE(Zend Function Entry的簡寫)將簡單地展開爲一個 zend_function_entry 結構。不過需要注意,這些宏對函數採取了一種很特別的命名機制:把你的C函數前加上一個 zif_ 前綴。比方說,ZEND_FE(first_module) 其實是指向了一個名爲 zif_first_module() 的 C 函數。如果你想把宏和一個手工編碼的函數名混合使用時(這並不是一個好的習慣),請你務必注意這一點。

小提示:
如果出現了一些引用某個名爲 zif_*() 函數的編譯錯誤,那十有八九與 ZEND_FE 所定義的函數有關。

“表 3.2 可用來定義函數的宏”給出了一個可以用來定義一個函數的所有宏的列表:

表3.2 可用來定義函數的宏

說明
ZEND_FE(name, arg_types) 定義了一個zend_function_entry 內字段name爲 “name” 的函數。arg_types 應該被設定爲 NULL。這個聲明需要有一個對應的 C 函數,該這個函數的名稱將自動以 zif_ 爲前綴。舉例來說, ZEND_FE(“first_module”, NULL) 就引入了一個名爲 first_module() 的 PHP 函數,並被關聯到一個名爲 zif_first_module() 的C函數。這個聲明通常與 ZEND_FUNCTION 搭配使用。
ZEND_NAMED_FE(php_name, name, arg_types) 定義了一個名爲 php_name 的 PHP 函數,並且被關聯到一個名爲 name 的 C 函數。arg_types 應該被設定爲 NULL。 如果你不想使用宏 ZEND_FE 自動創建帶有 zif_ 前綴的函數名的話可以用這個來代替。通常與 ZEND_NAMED_FUNCTION搭配使用。
ZEND_FALIAS(name, alias, arg_types) 爲 name 創建一個名爲 alias 的別名。arg_types 應該被設定爲 NULL。這個聲明不需要有一個對應的 C 函數,因爲它僅僅是創建了一個用來代替 name 的別名而已。
PHP_FE(name, arg_types) 以前的 PHP API,等同於 ZEND_FE 。僅爲兼容性而保留,請儘量避免使用。
PHP_NAMED_FE(runtime_name, name, arg_types) 以前的 PHP API,等同於ZEND_NAMED_FE 。僅爲兼容性而保留,請儘量避免使用。

注意:
你不能將 ZEND_FE 和 PHP_FUNCTION 混合使用,也不能將 PHP_FE 和 ZEND_FUNCTION 混合使用。但是將 ZEND_FE + ZEND_FUNCTION 和 PHP_FE + PHP_FUNCTION 一起混合使用是沒有任何問題的。當然我們並不推薦這樣的混合使用,而是建議你全部使用 ZEND_* 系列的宏。

聲明 Zend 模塊

Zend 模塊的信息被保存在一個名爲zend_module_entry 的結構,它包含了所有需要向 Zend 提供的模塊信息。你可以在“例 3.5 zend_module_entry 的內部聲明”中看到這個 Zend 模塊的內部定義:

例3.5 zend_module_entry 的內部聲明

字段 說明
size, zend_api, zend_debug and zts 通常用 “STANDARD_MODULE_HEADER” 來填充,它指定了模塊的四個成員:標識整個模塊結構大小的 size ,值爲 ZEND_MODULE_API_NO 常量的 zend_api,標識是否爲調試版本(使用 ZEND_DEBUG 進行編譯)的 zend_debug,還有一個用來標識是否啓用了 ZTS (Zend 線程安全,使用 ZTS 或 USING_ZTS 進行編譯)的 zts。
name 模塊名稱 (像“File functions”、“Socket functions”、“Crypt”等等). 這個名字就是使用 phpinfo() 函數後在“Additional Modules”部分所顯示的名稱。
functions Zend 函數塊的指針, 這個我們在前面已經討論過。
module_startup_func 模塊啓動函數。這個函數僅在模塊初始化時被調用,通常用於一些與整個模塊相關初始化的工作(比如申請初始化的內存等等)。如果想表明模塊函數調用失敗或請求初始化失敗請返回 FAILURE,否則請返回 SUCCESS。可以通過宏 ZEND_MINIT 來聲明一個模塊啓動函數。如果不想使用,請將其設定爲 NULL。
module_shutdown_func 模塊關閉函數。這個函數僅在模塊卸載時被調用,通常用於一些與模塊相關的反初始化的工作(比如釋放已申請的內存等等)。這個函數和 module_startup_func() 相對應。如果想表明函數調用失敗或請求初始化失敗請返回 FAILURE,否則請返回 SUCCESS。可以通過宏ZEND_MSHUTDOWN 來聲明一個模塊關閉函數。如果不想使用,請將其設定爲 NULL。
request_startup_func 請求啓動函數。這個函數在每次有頁面的請求時被調用,通常用於與該請求相關的的初始化工作。如果想表明函數調用失敗或請求初始化失敗請返回 FAILURE,否則請返回 SUCCESS。注意: 如果該模塊是在一個頁面請求中被動態加載的,那麼這個模塊的請求啓動函數將晚於模塊啓動函數的調用(其實這兩個初始化事件是同時發生的)。可以使用宏 ZEND_RINIT 來聲明一個請求啓動函數,若不想使用,請將其設定爲 NULL。
request_shutdown_func 請求關閉函數。這個函數在每次頁面請求處理完畢後被調用,正好與 request_startup_func() 相對應。如果想表明函數調用失敗或請求初始化失敗請返回 FAILURE,否則請返回 SUCCESS。注意: 當在頁面請求作爲動態模塊加載時, 這個請求關閉函數先於模塊關閉函數的調用(其實這兩個反初始化事件是同時發生的)。可以使用宏 ZEND_RSHUTDOWN 來聲明這個函數,若不想使用,請將其設定爲 NULL 。
info_func 模塊信息函數。當腳本調用 phpinfo() 函數時,Zend 便會遍歷所有已加載的模塊,並調用它們的這個函數。每個模塊都有機會輸出自己的信息。通常情況下這個函數被用來顯示一些環境變量或靜態信息。可以使用宏 ZEND_MINFO 來聲明這個函數,若不想使用,請將其設定爲 NULL 。
version 模塊的版本號。如果你暫時還不想給某塊設置一個版本號的話,你可以將其設定爲 NO_VERSION_YET。但我們還是推薦您在此添加一個字符串作爲其版本號。版本號通常是類似這樣: “2.5-dev”, “2.5RC1”, “2.5” 或者 “2.5pl3” 等等。
Remaining structure elements 這些字段通常是在模塊內部使用的,通常使用宏STANDARD_MODULE_PROPERTIES 來填充。而且你也不應該將他們設定別的值。STANDARD_MODULE_PROPERTIES_EX 通常只會在你使用了全局啓動函數(ZEND_GINIT)和全局關閉函數(ZEND_GSHUTDOWN)時纔用到,一般情況請直接使用 STANDARD_MODULE_PROPERTIES 。

在我們的例子當中,這個結構被定義如下:

這基本上是你可以設定最簡單、最小的一組值。該模塊名稱爲“First Module”,然後是所引用的函數列表,其後所有的啓動和關閉函數都沒有使用,均被設定爲了 NULL。

作爲參考,你可以在表 3.3 “所有可聲明模塊啓動和關閉函數的宏”中找到所有的可設置啓動與關閉函數的宏。這些宏暫時在我們的例子中還尚未用到,但稍後我們將會示範其用法。你應該使用這些宏來聲明啓動和關閉函數,因爲它們都需要引入一些特殊的變量( INIT_FUNC_ARGS 和 SHUTDOWN_FUNC_ARGS ),而這兩個參數宏將在你使用下面這些預定義宏時被自動引入(其實就是圖個方便)。如果你是手工聲明的函數或是對函數的參數列表作了一些必要的修改,那麼你就應該修改你的模塊相應的源代碼來保持兼容。

表3.3 所有可聲明模塊啓動和關閉函數的宏

描述
ZEND_MINIT(module) 聲明一個模塊的啓動函數。函數名被自動設定爲zend_minit_<module> (比如:zend_minit_first_module)。通常與ZEND_MINIT_FUNCTION 搭配使用。
ZEND_MSHUTDOWN(module) 聲明一個模塊的關閉函數。函數名被自動設定爲zend_mshutdown_<module> (比如:zend_mshutdown_first_module)。通常與ZEND_MSHUTDOWN_FUNCTION搭配使用。
ZEND_RINIT(module) 聲明一個請求的啓動函數。函數名被自動設定爲zend_rinit_<module> (比如:zend_rinit_first_module)。通常與ZEND_RINIT_FUNCTION搭配使用。
ZEND_RSHUTDOWN(module) 聲明一個請求的關閉函數。函數名被自動設定爲zend_rshutdown_<module> (比如:zend_rshutdown_first_module)。通常與ZEND_RSHUTDOWN_FUNCTION 搭配使用。
ZEND_MINFO(module) 聲明一個輸出模塊信息的函數,用於 phpinfo()。函數名被自動設定爲zend_info_<module> (比如:zend_info_first_module)。通常與 ZEND_MINFO_FUNCTION 搭配使用。

實現 get_module() 函數

這個函數只用於動態可加載模塊。我們先來看一下如何通過宏 ZEND_GET_MODULE 來創建這個函數:

#if COMPILE_DL_FIRSTMOD
ZEND_GET_MODULE(firstmod)
#endif

這個函數的實現被一條件編譯語句所包圍。這是很有必要的,因爲 get_module() 函數僅僅在你的模塊想要編譯成動態模塊時纔會被調用。通過在編譯命令行指定編譯條件:COMPILE_DL_FIRSTMOD (也就是上面我們設置的那個預定義)的打開與否,你就可以決定是編譯成一個動態模塊還是編譯成一個內建模塊。如果想要編譯成內建模塊的話,那麼這個 get_module() 將被移除。

get_module() 函數在模塊加載時被 Zend 所調用,你也可以認爲是被你 PHP 腳本中的 dl() 函數所調用。這個函數的作用就是把模塊的信息信息塊傳遞 Zend 並通知 Zend 獲取這個模塊的相關內容。

如果你沒有在一個動態可加載模塊中實現 get_module() 函數,那麼當你在訪問它的時候 Zend 就會向你拋出一個錯誤信息。

實現導出函數

導出函數的實現是我們構建擴展的最後一步。在我們的 first_module 例子中,函數被實現如下:

這個函數是用宏 ZEND_FUNCTION 來聲明的,和前面我們討論的 Zend 函數塊中的 ZEND_FE 聲明相對應。在函數的聲明之後,我們的代碼便開始檢查和接收這個函數的參數。在將參數進行轉換後將其值返回。(參數的接收和處理我們馬上會在下一節中講到)。

小結

一切基本上就這樣了 ―― 我們在實現一個模塊時不會再遇到其他方面的事了。內建模塊也基本上同動態模塊差不多。因此,有了前面幾節我們所掌握的信息,再在你遇到 PHP 源代碼的時候你就有能力去搞定這些小麻煩。

在下面的幾個小節裏,我們將會學習到如何利用 PHP 內核來創建一個更爲強大的擴展!

(十)接收參數

對於擴展來說,最重要的一件事就是如何接收和處理那些通過函數參數傳遞而來的數據。大多數擴展都是用來處理某些特定的輸入數據(或者是根據參數來決定進行某些特定的動作),而函數的參數則是 PHP 代碼層和 C 代碼層之間交換數據的唯一途徑。當然,你也可以通過事先定義好的全局變量來交換數據(這個我們稍後會談到),不過這種習慣可不太好,我們應該儘量避免。

在 PHP 中並不需要做任何顯式的函數聲明,這也就是我們爲什麼說 PHP 的調用語法是動態的而且 PHP 從不會檢查任何錯誤的原因。調用語法是否正確完全是留給用戶自己的工作。也就是說,在調用一個函數時完全有可能這次用一個參數而下次用 4 個參數,而且兩種情況在語法上都是正確的。

取得參數數量

因爲 PHP 不但沒法根據函數的顯式聲明來對調用進行語法檢查,而且它還支持可變參數,所以我們就不得不在所調用函數的內部來獲取參數個數。這個工作可以交給宏 ZEND_NUM_ARGS 來完成。在(PHP4)以前,這個宏(在 PHP3 中應該指的是宏 ARG_COUNT,因爲 ZEND_NUM_ARGS 宏是直到 PHP 4.0 纔出現的,並且其定義一直未變。PHP4 及以後雖也有 ARG_COUNT 宏定義,但卻僅僅是爲兼容性而保留的,並不推薦使用,譯者注)是利用所調用的 C 函數中的變量 ht(就是定義在宏 INTERNAL_FUNCTION_PARAMETERS 裏面的那個,HashTable * 類型)來獲取參數個數的,而現在變量 ht 就只包含函數的參數個數了(int 類型)。與此同時還定義了一個啞宏:ZEND_NUM_ARGS(直接等於 ht,見 Zend.h)。儘量地採用 ZEND_NUM_ARGS 是個好習慣,因爲這樣可以保證在函數調用接口上的兼容性。

下面的代碼展示瞭如何檢查傳入函數的參數個數的正確性:

if(ZEND_NUM_ARGS() != 2)
{
    WRONG_PARAM_COUNT;
}

如果沒有爲該函數傳入兩個參數,那麼就會退出該函數並且發出一個錯誤消息。在這段代碼中我們使用了一個工具宏:WRONG_PARAM_COUNT,它主要用來拋出一個類似

"Warning: Wrong parameter count for firstmodule() in /home/www/htdocs/firstmod.php on line 5"

這樣的錯誤信息。

這個宏會主要負責拋出一個默認的錯誤信息,然後便返回調用者。我們可以在 zend_API.h 中找到它的定義:

ZEND_API voidwrong_param_count(void);
#defineWRONG_PARAM_COUNT { wrong_param_count(); return; }

正如您所見,它調用了一個內部函數 wrong_param_count() ,這個函數會輸出一個警告信息。至於如何拋出一個自定義的錯誤信息,可以參見後面的“打印信息”一節。

取回參數

對傳入的參數進行解析是一件很常見同時也是頗爲乏味的事情,而且同時你還得做好標準化的錯誤檢查和發送錯誤消息等瑣事。不過從 PHP 4.1.0 開始,我們就可以用一個新的參數解析 API 來搞定這些事情。這個 API 可以大大簡化參數的接收處理工作,儘管它在處理可變參數時還有點弱。但既然絕大部分函數都沒有可變參數,那麼使用這個 API 也就理所應當地成爲了我們處理函數參數時的標準方法。

這個用於參數解析的函數的原型大致如下:

intzend_parse_parameters(intnum_args TSRMLS_DC, char *type_spec, ...);

第一個參數 num_args 表明了我們想要接收的參數個數,我們經常使用 ZEND_NUM_ARGS() 來表示對傳入的參數“有多少要多少”。第二參數應該總是宏 TSRMLS_CC 。第三個參數 type_spec 是一個字符串,用來指定我們所期待接收的各個參數的類型,有點類似於 printf 中指定輸出格式的那個格式化字符串。剩下的參數就是我們用來接收 PHP 參數值的變量的指針。

zend_parse_parameters() 在解析參數的同時會儘可能地轉換參數類型,這樣就可以確保我們總是能得到所期望的類型的變量。任何一種標量類型都可以轉換爲另外一種標量類型,但是不能在標量類型與複雜類型(比如數組、對象和資源等)之間進行轉換。

如果成功地解析和接收到了參數並且在轉換期間也沒出現錯誤,那麼這個函數就會返回 SUCCESS,否則返回 FAILURE。如果這個函數不能接收到所預期的參數個數或者不能成功轉換參數類型時就會拋出一些類似下面這樣的錯誤信息:

Warning - ini_get_all() requires at most 1 parameter, 2 given
Warning - wddx_deserialize() expects parameter 1 to be string, array given

當然,每個錯誤信息都會帶有錯誤發生時所在的文件名和行數的。

下面這份清單完整地列舉出了我們可以指定接收的參數類型:

    l - 長整數

    d - 雙精度浮點數

    s - 字符串 (也可能是空字節)和其長度

    b - 布爾值

    r - 資源, 保存在 zval*

    a - 數組, 保存在 zval*

    o - (任何類的)對象, 保存在 zval*

    O - (由class entry 指定的類的)對象, 保存在 zval*

    z - 實際的 zval*

下面的一些字符在類型說明字符串(就是那個 char *type_spec)中具有特別的含義:

    | - 表明剩下的參數都是可選參數。如果用戶沒有傳進來這些參數值,那麼這些值就會被初始化成默認值。

    / - 表明參數解析函數將會對剩下的參數以 SEPARATE_ZVAL_IF_NOT_REF() 的方式來提供這個參數的一份拷貝,除非這些參數是一個引用。

    ! - 表明剩下的參數允許被設定爲 NULL(僅用在 a、o、O、r和z身上)。如果用戶傳進來了一個 NULL 值,則存儲該參數的變量將會設置爲 NULL。

當然啦,熟悉這個函數的最好的方法就是舉個例子來說明。下面我們就來看一個例子:

注意,在最後的一個例子中,我們直接用了數值 3 而不是 ZEND_NUM_ARGS() 來作爲想要取得參數的個數。這樣如果我們的 PHP 函數具有可變參數的話我們就可以只接收最小數量的參數。當然,如果你想操作剩下的參數,你可以用 zend_get_parameters_array_ex() 來得到。

這個參數解析函數還有一個帶有附加標誌的擴展版本,這個標誌可以讓你控制解析函數的某些動作。

intzend_parse_parameters_ex(intflags, intnum_args TSRMLS_DC, char *type_spec, ...);

這個標誌(flags)目前僅接受 ZEND_PARSE_PARAMS_QUIET 這一個值,它表示這個函數不輸出任何錯誤信息。這對那些可以傳入完全不同類型參數的函數非常有用,但這樣你也就不得不自己輸出錯誤信息。

下面就是一個如何既可以接收 3 個長整形數又可以接收一個字符串的例子:

我想你通過上面的那些例子就可以基本掌握如何接收和處理參數了。如果你想看更多的例子,請翻閱 PHP 源碼包中那些自帶的擴展的源代碼,那裏麪包含了你可能遇到的各種情況。

以前的老式的獲取參數的的方法(不推薦)

獲取函數參數這件事情我們還可以通過 zend_get_parameters_ex() 來完成(不推薦使用這些舊式的 API,我們推薦您使用前面所述的新式的參數解析函數):

zval **parameter;
if(zend_get_parameters_ex(1, &parameter) != SUCCESS)
{
    WRONG_PARAM_COUNT;
}

所有的參數都存儲在一個二次指向的 zval 容器裏面(其實就是一個 zval* 數組,譯者注)。上面的這段代碼嘗試接收 1 個參數並且將其保存在 parameter 所指向的位置。

zend_get_parameters_ex() 至少需要兩個參數。第一個參數表示我們想要接收參數的個數(這個值通常是對應於 PHP 函數參數的個數,由此也可以看出事先對調用語法正確性的檢查是多麼重要)。第二個參數(包括剩下的所有參數)指向一個二次指向 zval 的指針。(即 ***zval,是不是有點糊塗了?^_^)這些指針是必須的,因爲 Zend 內部是使用 **zval 進行工作的。爲了能被在我們函數內部定義的**zval局部變量所訪問,我們就必須在用一個指針來指向它。

zend_get_parameters_ex() 的返回值可以是 SUCCESS 或> FAILURE,分別表示參數處理的成功或失敗。如果處理失敗,那最大的可能就是由於沒有指定一個正確的參數個數。如果處理失敗,則應該使用宏 WRONG_PARAM_COUNT 來退出函數。

如果想接收更多的的參數,可以用類似下面一段的代碼來處理:

zval **param1, **param2, **param3, **param4;
if(zend_get_parameters_ex(4, &param1, &param2, &param3, &param4) != SUCCESS)
{
    WRONG_PARAM_COUNT;
}

zend_get_parameters_ex() 僅檢查你是否在試圖訪問過多的參數。如果函數有 5 個參數,而你僅僅接收了其中的 3 個,那麼你將不會收到任何錯誤信息,zend_get_parameters_ex() 僅返回前三個參數的值。再次調用 zend_get_parameters_ex() 也不會獲得剩下兩個參數的值,而還是返回前三個參數的值。

接收可變(可選)參數

如果你想接收一些可變參數,那用前面我們剛剛討論的方法就不太合適了,主要是因爲我們將不得不爲每個可能的參數個數來逐行調用 zend_get_parameters_ex(),顯然這很不爽。

爲了解決這個問題,我們可以借用一下 zend_get_parameters_array_ex() 這個函數。它可以幫助我們接收不定量的參數並將其保存在我們指定的地方:

讓我們來看看這幾行代碼。首先代碼檢查了傳入參數的個數,確保在我們可接受的範圍內;然後就調用 zend_get_parameters_array_ex() 把所有有效參數值的指針填入 parameter_array。

我們可以在 fsockopen() 函數(位於ext/standard/fsock.c )中找到一個更爲漂亮的實現。代碼大致如下,你也不用擔心還沒有弄懂全部的函數,因爲我們很快就會談到它們。

例3.6 PHP中帶有可變參數的 fsockopen() 函數的實現

fsockopen() 可以接收 2-5 個參數。在必需的變量聲明之後便開始檢查參數的數量範圍。然後在一個 switch 語句中使用了貫穿(fall-through)法來處理這些的參數。這個 switch 語句首先處理最大的參數個數(即 5),隨後依次處理了參數個數爲 4 和 3 的情況,最後用 break 關鍵字跳出 switch 來忽略對其他情況下參數(也就是隻含有 2 個參數情況)的處理。這樣在經過 switch 處理之後,就開始處理參數個數爲最小時(即 2)的情況。

這種像樓梯一樣的多級處理方法可以幫助我們很方便地處理一些可變參數。

存取參數

爲了存取一些參數,讓每個參數都具有一個明確的(C)類型是很有必要的。但 PHP 是一種動態語言,PHP 從不做任何類型檢查方面的工作,因此不管你想不想,調用者都可能會把任何類型的數據傳到你的函數裏。比如說,如果你想接收一個整數,但調用者卻可能會給你傳遞個數組,反之亦然 - PHP 可不管這些的。

爲了避免這些問題,你就必須用一大套 API 函數來對傳入的每一個參數都做一下強制性的類型轉換。(見表3.4 參數類型轉換函數)

注意:
所有的參數轉換函數都以一個 **zval 來作爲參數。

表3.4 參數類型轉換函數

函數 說明
convert_to_boolean_ex() 強制轉換爲布爾類型。若原來是布爾值則保留,不做改動。長整型值0、雙精度型值0.0、空字符串或字符串‘0’還有空值 NULL 都將被轉換爲 FALSE(本質上是一個整數 0)。數組和對象若爲空則轉換爲 FALSE,否則轉爲 TRUE。除此之外的所有值均轉換爲 TRUE(本質上是一個整數 1)。
convert_to_long_ex() 強制轉換爲長整型,這也是默認的整數類型。如果原來是空值NULL、布爾型、資源當然還有長整型,則其值保持不變(因爲本質上都是整數 0)。雙精度型則被簡單取整。包含有一個整數的字符串將會被轉換爲對應的整數,否則轉換爲 0。空的數組和對象將被轉換爲 0,否則將被轉換爲 1。
convert_to_double_ex() 強制轉換爲一個雙精度型,這是默認的浮點數類型。如果原來是空值 NULL 、布爾值、資源和雙精度型則其值保持不變(只變一下變量類型)。包含有一個數字的字符串將被轉換成相應的數字,否則被轉換爲 0.0。空的數組和對象將被轉換爲 0.0,否則將被轉換爲 1.0。
convert_to_string_ex() 強制轉換爲數組。若原來就是一數組則不作改動。對象將被轉換爲一個以其屬性爲鍵名,以其屬性值爲鍵值的數組。(方法強制轉換爲字符串。空值 NULL 將被轉換爲空字符串。布爾值 TRUE 將被轉換爲 ‘1’,FALSE 則被轉爲一個空字符串。長整型和雙精度型會被分別轉換爲對應的字符串,數組將會被轉換爲字符串‘Array’,而對象則被轉換爲字符串‘Object’。
convert_to_array_ex(value) 強制轉換爲數組。若原來就是一數組則不作改動。對象將被轉換爲一個以其屬性爲鍵名,以其屬性值爲鍵值的數組。(方法將會被轉化爲一個‘scalar’鍵,鍵值爲方法名)空值 NULL 將被轉換爲一個空數組。除此之外的所有值都將被轉換爲僅有一個元素(下標爲 0)的數組,並且該元素即爲該值。
convert_to_object_ex(value) 強制轉換爲對象。若原來就是對象則不作改動。空值 NULL 將被轉換爲一個空對象。數組將被轉換爲一個以其鍵名爲屬性,鍵值爲其屬性值的對象。其他類型則被轉換爲一個具有‘scalar’屬性的對象,‘scalar’屬性的值即爲該值本身。
convert_to_null_ex(value) 強制轉換爲空值 NULL。

在你的參數上使用這些函數可以確保傳遞給你的數據都是類型安全的。如果提供的類型不是需要的類型,PHP 就會強制性地返回一個相應的僞值(比如空字符串、空的數組或對象、數值 0 或布爾值的 FALSE 等)來確保結果是一個已定義的狀態。

下面的代碼是從前面討論過的模塊中摘錄的,其中就用到了這些轉換函數:

在收到參數指針以後,參數值就被轉換成了一個長整型(或整形),轉換的結果就是這個函數的返回值。如果想要弄懂如何存取到這個返回值,我們就需要對 zval 有一點點認識。它的定義如下:

例3.7 PHP/Zend zval 類型的定義

實際上,pzval(定義在 php.h)就是 zval(定義在 zend.h)的一個別名,都是 _zval_struct 結構的一個別名。_zval_struct 是一個很有趣的結構,它保存了這個結構的真實值 value、類型 type 和引用信息 is_ref。字段 value 是一個 zvalue_value 聯合,根據變量類型的不同,你就可以訪問不同的聯合成員。對於這個結構的描述,可參見“表3.5 Zend zval 結構”、“表3.6 Zend zvalue_value 結構”和“表3.7 Zend 變量類型”。

表3.5 Zend zval 結構

字段 說明
value 變量內容的聯合,參見“表3.6 Zend zvalue_value 結構”。
type 變量的類型。“表3.7 Zend 變量類型”給出了一個完整的變量類型列表。
is_ref 0 表示這個變量還不是一個引用。1 表示這個變量還有被別的變量所引用。
refcount 表示這個變量是否仍然有效。每增加一個對這個變量的引用,這個數值就增加 1。反之,每失去一個對這個變量的引用,該值就會減1。當引用計數減爲0的時候,就說明已經不存在對這個變量的引用了,於是這個變量就會自動釋放。

表3.6 Zend zvalue_value 結構

字段 說明
lval 如果變量類型爲 IS_LONG、IS_BOOLEAN 或 IS_RESOURCE 就用這個屬性值。
dval 如果變量類型爲 IS_DOUBLE 就用這個屬性值。
str 如果變量類型爲 IS_STRING 就訪問這個屬性值。它的字段 len 表示這個字符串的長度,字段 val 則指向該字符串。由於 Zend 使用的是 C 風格的字符串,因此字符串的長度就必須把字符串末尾的結束符 0×00 也計算在內。
ht 如果變量類型爲數組,那這個 ht 就指向數組的哈希表入口。
obj 如果變量類型爲 IS_OBJECT 就用這個屬性值。

表3.7 Zend 變量類型

類型常量 說明
IS_NULL 表示是一個空值 NULL。
IS_LONG 是一個(長)整數。
IS_DOUBLE 是一個雙精度的浮點數。
IS_STRING 是一個字符串。
IS_ARRAY 是一個數組。
IS_OBJECT 是一個對象。
IS_BOOL 是一個布爾值。
IS_RESOURCE 是一個資源(關於資源的討論,我們以後會在適當的時候討論到它)。
IS_STRING 是一個常量。

想訪問一個長整型數,那你就訪問 zval.value.lval;想訪問一個雙精度數,那你就訪問 zval.value.dval,依此類推。不過注意,因爲所有的值都是保存在一個聯合裏面,所以如果你用了不恰當的字段去訪問,那就可能會得到一個毫無意義的結果。

訪問一個數組和對象可能會稍微複雜些,稍後再說。

處理通過引用傳遞過來的參數

如果函數裏面的參數是通過引用傳遞進來的,但是你又想去修改它,那就需要多加小心了。

根據我們前面所討論的知識,我們還沒有辦法去修改一個經 PHP 函數參數傳進來的 zval 。當然你可以修改那些在函數內部創建的局部變量的 zval ,但這並代表你可以修改任何一個指向 Zend 自身內部數據的 zval (也就是那些非局部的 zval)!

這是爲什麼呢?我想你可能注意到了,我們前面討論的 API 函數都是類似於 *_ex() 這樣子的。比如我們用 zend_get_parameters_ex() 而不用 zend_get_parameters(),用 convert_to_long_ex() 而不用 convert_to_long() 等等。這些 *_ex() 函數被稱爲新的“擴展”的 Zend API,它們的速度要快於對應的傳統 API,但副作用是它們只提供了只讀訪問機制。

因爲 Zend 內部是靠引用機制來運行的,因此不同的變量就有可能引自同一個 value (zval 結構的字段 value)。而修改一個 zval 就要求這個 zval 的 value 必須是獨立的,也就是說這個 value 不能被其他 zval 引用。如果有一個 zval 裏面的 value 還被其他 zval 引用了,你也同時把這個 value 給修改了,那你也同時就把其他 zval 的 value 給修改了,因爲它們的 value 只是簡單地指向了這個 value 而已。

但 zend_get_parameters_ex() 是根本不管這些的,它只是簡單地返回一個你所期望的那個 zval 的指針。至於這個 zval 是否還存在其他引用,who care?(所以我們說這些 *_ex() 只提供了只讀機制,並沒有提供可寫機制。你若利用 *_ex() 的結果強行賦值也是可以的,但這樣就沒法保證數據安全了。譯註)。而和這個 API 對應的傳統 API zend_get_parameters () 就會即時檢查 value 的引用情況。如果它發現了對 value 的引用,它就會馬上再重新創建一個獨立的 zval ,然後把引用的數據複製一份到新的剛剛申請的空間裏面,然後返回這個新的 zval 的指針。

這個動作我們稱之爲“zval 分離(或者 pval 分離)”。由於 *_ex()函數並不執行“zval 分離”操作,因此它們雖然快,但是卻不能用於進行寫操作。

但不管怎樣,要想修改參數,寫操作是不可避免的。於是 Zend 使用了這樣一個特別的方式來處理寫操作:無論何時,只要函數的某個參數使用過引用傳遞的,那它就自動進行 zval 分離。這也就意味着不管什麼時間,只要你像下面這樣來調用一個 PHP 函數,Zend 就會自動確保傳入的是一個獨立的 value 並處於“寫安全”狀態:

my_function(&$parameter);

但這不是一般參數(指不帶 & 前綴但也是引用的參數,譯者注)的情況。所有不是直接通過引用(指不帶 & 前綴)傳遞的參數都將只是處在一種“只讀”狀態(其實這裏的“只讀”狀態可以理解爲“寫不安全”狀態)。

這就要求你確認是否真的在同一個引用打交道,否則你可能會收到你不太想要的結果。我們可以使用宏 PZVAL_IS_REF 來檢查一個參數是否是通過引用傳遞的。這個宏接收一個 zval* 參數。“例3.8 檢查參數是否經引用傳遞”給出了這樣一個例子:

例3.8 檢查參數是否經引用傳遞

確保其他情況下某些參數的寫安全

有時候你可能會遇到過這種情況:你想對用 zend_get_parameters_ex() 接收的但是沒有通過引用傳遞的一個參數進行寫操作。這時你可以用宏 SEPARATE_ZVAL 來手工進行 zval 分離操作。這樣可以得到一個新創建的與原來內部數據獨立的 zval,但這個 zval 僅在局部有效,它可以被修改或銷燬而不影響外部的全局 zval 。

zval **parameter;

/* 接收參數 */
zend_get_parameters_ex(1, &parameter);

/* 此時仍然關聯在 Zend 的內部數據緩衝區 *//* 現在將“寫安全”化 */
SEPARATE_ZVAL(parameter);

/* 現在你可以放心大膽去修改了,無需擔心外部的 zval 會受到影響 */// ……

因爲宏 SEPARATE_ZVAL 通過 emalloc() 函數來申請一個新的 zval,所以這也就意味着如果你不主動去釋放這段內存的話,那它就會直到腳本中止時才被釋放。如果你大量調用這個宏卻沒有釋放,那它可能會瞬間塞滿你的內存。

注意:因爲現在已經很少遇到和需要傳統 API(諸如 zend_get_parameters() 等等)了(貌似它有點過時了),所以有關這些 API 本節不再贅述。

(十一)創建變量

當 PHP 腳本與擴展互相交換數據時,我們還需要做一件很重要的事情,那就是創建變量。這一小節將會展示如何處理那些 PHP 腳本所支持的變量類型。

概述

要創建一個能夠被 PHP 腳本所訪問的“外部變量”,我們只需先創建一個 zval 容器,然後對這個 zval 結構進行必要的填充,最後再把它引入到 Zend 的內部符號表中就可以了。而且幾乎所有變量的創建基本上都是這幾個步驟:

宏 MAKE_STD_ZVAL 通過 ALLOC_ZVAL 來申請一個新的 zval 容器的內存空間並調用 INIT_ZVAL(查看 PHP4/5 的源代碼可知此處可能爲原文筆誤,實際上應爲 INIT_PZVAL,下同。譯註)將其初始化。在當前的 Zend Engine 中,INIT_ZVAL 所負責的初始化工作除了將 zval 容器的引用計數(refcount)置爲 1 之外,還會把引用標識也清除(即把 is_ref 也置爲 0)。而且在以後的 Zend Engine 中還可能會繼續擴展這個 INIT_ZVAL 宏操作,因此我們推薦您使用 MAKE_STD_ZVAL 而非簡單使用一個 ALLOC_ZVAL 來完成一個變量的創建工作。當然,如果您是想優化一下速度(或者是不想明確地初始化這個 zval 容器),那還是可以只用 ALLOC_ZVAL 來搞定的。不過我們並不推薦這麼做,因爲這將不能保證數據的完整性。

ZEND_SET_SYMBOL 宏負責將我們新建的變量引入 Zend 內部的符號表。這個宏會首先檢查一下這個變量是否已經存在於符號表中,如果已經存在則將其轉換爲一個引用變量(同時會自動銷燬原有的 zval 容器)。事實上這個方法經常用在某些速度要求並不苛刻但希望能少用一些內存的情況下。

您可能注意到了 ZEND_SET_SYMBOL 是通過宏 EG 來訪問 Zend 執行器(executor)的全局結構的。特別的,如果你使用的是 EG(active_symbol_table),那你就可以訪問到當前的活動符號表,從而可以處理一些全局或局部變量。其中局部變量可能會依不同的函數而有所不同。

當然,要是你很在意程序的運行速度並且不在乎那一點點內存的話,那你可以跳過對相同名字變量存在性的檢查而直接使用 zend_hash_update() 函數強行將這個名字的變量插入符號表。

實際上這段代碼也是很多擴展使用的標準方法。

上面這段代碼所產生的變量是局部變量,作用範圍跟調用函數的上下文相關。如果你想創建一個全局變量那也很簡單,方法還是老方法,只需換個符號表就可以了。

注意:
現在宏 ZEND_SET_SYMBOL 使用的符號表是全局符號表 EG(symbol_table)。另外,active_symbol_table 是一個指針,而 symbol_table 卻不是。這就是我們爲什麼分別使用 EG(active_symbol_table) 和 &EG(symbol_table) 的原因 - ZEND_SET_SYMBOL 需要一個指針作爲其參數。

當然,你同樣也可以強行更新這個符號表:

例 3.9 “創建不同作用域的變量”向我們展示了創建一個局部變量local_variable和一個全局變量global_variable的過程。

注意:
你可能會發現在 PHP 函數裏似乎還不能直接訪問這個全局變量global_variable,因爲你在使用前還必須使用 global $global_variable; 聲明一下。

例3.9 創建不同作用域的變量

長整型(整數)

現在讓我們以長整型變量起點,瞭解一下如何爲一個變量賦值。PHP 中的整數全部是長整型,其值的存儲方法也是非常簡單的。看一下我們前面討論過的 zval.value 容器的結構你就會明白,所有的長整型數據都是直接保存在這個聯合中的 lval 字段,相應的數據類型(type 字段)爲 IS_LONG(見例3.10 “長整型變量的創建”)。

例3.10 長整型變量的創建

zval *new_long;
MAKE_STD_ZVAL(new_long);
new_long->type = IS_LONG;
new_long->value.lval = 10;

或者你也可以直接使用 ZVAL_LONG 宏:

zval *new_long;
MAKE_STD_ZVAL(new_long);
ZVAL_LONG(new_long, 10);

雙精度型(浮點數)

PHP 中的浮點數都是雙精度型,存儲方法和整型差不多,也很簡單。它的值是直接放在聯合中的 dval 字段,對應數據類型爲 IS_DOUBLE。

zval *new_double;
MAKE_STD_ZVAL(new_double);
new_double->type = IS_DOUBLE;
new_double->value.dval = 3.45;

同樣你也可以直接使用宏 ZVAL_DOUBLE:

zval *new_double;
MAKE_STD_ZVAL(new_double);
ZVAL_DOUBLE(new_double, 3.45);

字符串

字符串的存儲可能會稍費點事。字符串的值是保存在 zval.value 容器中的 str 結構裏面,相應的數據類型爲 IS_STRING。不過需要注意的是,前面我們已經提到過,所有與 Zend 內部數據結構相關的字符串都必須使用 Zend 自己的內存管理函數來申請空間。這樣一來,就不能使用那些靜態字符串(因爲這種字符串的內存空間是編譯器預先分配的)或通過標準函數(比如 malloc() 等函數)來申請空間的字符串。

請注意:
在這我們使用了 estrdup() 函數。當然我們仍可直接使用一個預定義宏 ZVAL_STRING 來完成這項工作:

ZVAL_STRING 宏的第三個參數指明瞭該字符串是否需要被複制(使用 estrdup() 函數)。值爲 1 將導致該字符串被複制,爲 0 時則僅僅是簡單地將其指向該變量的值容器(即字符串地址,譯註)。這項特性將會在你僅僅需要創建一個變量並將其指向一個已經由 Zend 內部數據內存時變得很有用。

如果你想在某一位置截取該字符串或已經知道了這個字符串的長度,那麼可以使用宏 ZVAL_STRINGL(zval, string, length, duplicate) 來完成這項工作。這個函數會額外需要一個表明該字符串長度地參數。這個宏不但速度上要比 ZVAL_STRING 快,而且還是二進制安全的。

如果想創建一個空字符串,那麼將其長度置 0 並且把 empty_string 作爲字符串的內容即可:

new_string->type = IS_STRING;
new_string->value.str.len = 0;
new_string->value.str.val = empty_string;

當然,我們也專門爲您準備了一個相應的宏 ZVAL_EMPTY_STRING 來搞定這個步驟:

MAKE_STD_ZVAL(new_string);
ZVAL_EMPTY_STRING(new_string);

布爾類型

布爾類型變量的創建跟長整型差不多,只是數據類型爲 IS_BOOL,並且字段 lval 所允許的值只能爲 0 和 1:

zval *new_bool;
MAKE_STD_ZVAL(new_bool);
new_bool->type = IS_BOOL;
new_bool->value.lval = 1;

也可以使用宏 ZVAL_BOOL (需要另外指定一個值)來完成這件事情,或者乾脆直接使用 ZVAL_TRUE 或 ZVAL_FALSE 直接將其值設定爲 TRUE 或 FALSE。

數組

數組在 Zend 內部是用哈希表(HashTable)來存儲的,這個哈希表可以使用一系列的 zend_hash_*() 函數來訪問。因此我們在創建一個數組時必須先創建一個哈希表,然後再將其保存在 zval.value 容器的 ht 字段中。

不過針對數組的創建我們現在另有一套非常方便 API 可供使用。爲了創建一個數組,我們可先調用一下 array_init() 函數:

zval *new_array;
MAKE_STD_ZVAL(new_array);
array_init(new_array); // array_init() 函數總是返回 SUCCESS

要給數組增加一個元素,根據實際需要,我們有 N 個函數可供調用。“表3.8 用於關聯數組的 API”、“表3.9 用於索引數組的 API 第一部分”和“表3.10 用於索引數組的 API 第二部分”有這些函數的說明。所有這些函數在調用成功時返回 SUCCESS,在調用失敗時返回 FAILURE。

表3.8 用於關聯數組的 API

函數 說明
add_assoc_long(zval *array, char *key, long n); 添加一個長整型元素。
add_assoc_unset(zval *array, char *key); 添加一個 unset 元素。
add_assoc_bool(zval *array, char *key, int b); 添加一個布爾值。
add_assoc_resource(zval *array, char *key, int r); 添加一個資源。
add_assoc_double(zval *array, char *key, double d); 添加一個浮點值。
add_assoc_string(zval *array, char *key, char *str, int duplicate); 添加一個字符串。duplicate 用於表明這個字符串是否要被複制到 Zend 的內部內存。
add_assoc_stringl(zval *array, char *key, char *str, uint length, int duplicate); 添加一個指定長度的字符串。其餘跟add_assoc_string () 相同。
add_assoc_zval(zval *array, char *key, zval *value); 添加一個 zval 結構。 這在添加另外一個數組、對象或流等數據時會很有用。

表3.9 用於索引數組的 API 第一部分

函數 說明
add_index_long(zval *array, uint idx, long n); 添加一個長整型元素。
add_index_unset(zval *array, uint idx); 添加一個 unset 元素。
add_index_bool(zval *array, uint idx, int b); 添加一個布爾值。
add_index_resource(zval *array, uint idx, int r); 添加一個資源。
add_index_double(zval *array, uint idx, double d); 添加一個浮點值。
add_index_string(zval *array, uint idx, char *str, int duplicate); 添加一個字符串。duplicate 用於表明這個字符串是否要被複制到 Zend 的內部內存。
add_index_stringl(zval *array, uint idx, char *str, uint length, int duplicate); 添加一個指定長度的字符串。其餘跟add_index_string () 相同。
add_index_zval(zval *array, uint idx, zval *value); 添加一個 zval 結構。 這在添加另外一個數組、對象或流等數據時會很有用。

表3.10 用於索引數組的 API 第二部分

函數 說明
add_next_index_long(zval *array, long n); 添加一個長整型元素。
add_next_index_unset(zval *array); 添加一個 unset 元素。
add_next_index_bool(zval *array, int b); 添加一個布爾值。
add_next_index_resource(zval *array, int r); 添加一個資源。
add_next_index_double(zval *array, double d); 添加一個浮點值。
add_next_index_string(zval *array, char *str, int duplicate); 添加一個字符串。duplicate 用於表明這個字符串是否要被複制到 Zend 的內部內存。
add_next_index_stringl(zval *array, char *str, uint length, int duplicate); 添加一個指定長度的字符串。其餘跟add_next_index_string () 相同。
add_next_index_zval(zval *array, zval value); 添加一個 zval 結構。 這在添加另外一個數組、對象或流等數據時會很有用。

所有這些函數都是對 Zend 內部 hash API 的一種友好抽象。因此,若你願意,你大可直接使用那些 hash API 進行操作。比方說,假如你已經有了一個 zval 容器並想把它插入到一個數組,那麼你就可以直接使用 zend_hash_update() 來把它添加到一個關聯數組(例3.11 給關聯數組添加一個元素)或索引數組(例3.12 給索引數組添加一個元素)。

例3.11 給關聯數組添加一個元素

例3.12 給索引數組添加一個元素

如果還想模擬下 add_next_index_*() ,那可以這麼做:

zend_hash_next_index_insert(ht, zval **new_element, sizeof(zval *), NULL)

注意:
如果要從函數裏面返回一個數組,那就必須首先對預定義變量 return_value (return_value 是我們導出函數中的一個預定義參數,用來存儲返回值)使用一下 array_init() 函數。不過倒不必對其使用 MAKE_STD_ZVAL 。

提示:
爲了避免一遍又一遍地書寫 new_array->value.ht,我們可以用 HASH_OF(new_array) 來代替。而且出於兼容性和風格上的考慮,我們也推薦您這麼做。

對象

既然對象可以被轉換成數組(反之亦然),那麼你可能已經猜到了兩者應該具有很多相似之處。實際上,對象就是使用類似的函數進行操作的,所不同的是創建它們時所用的 API。

我們可以調用 object_init() 函數來初始化一個對象:

zval *new_object;
MAKE_STD_ZVAL(new_object);
if(object_init(new_object) != SUCCESS)
{
    // do error handling here
}

可以使用“表3.11 用於創建對象的 API”來給對象添加一些成員。

表3.11 用於創建對象的 API

函數 說明
add_property_long(zval *object, char *key, long l); 添加一個長整型類型的屬性值。
add_property_unset(zval *object, char *key); 添加一個 unset 類型的屬性值。
add_property_bool(zval *object, char *key, int b); 加一個布爾類型的屬性值。
add_property_resource(zval *object, char *key, long r); 添加一個資源類型的屬性值。
add_property_double(zval *object, char *key, double d); 添加一個浮點類型的屬性值。
add_property_string(zval *object, char *key, char *str, int duplicate); 添加一個字符串類型的屬性值。
add_property_stringl(zval *object, char *key, char *str, uint length, int duplicate); 添加一個指定長度的字符串類型的屬性值,速度要比 add_property_string() 函數快,而且是二進制安全的。
add_property_zval(zval *obect, char *key, zval container); 添加一個 zval 結構的屬性值。 這在添加另外一個數組、對象等數據時會很有用。

** 資源

資源是 PHP 中一種比較特殊的數據類型。“資源”這個詞其實並不特指某些特殊類型的數據,事實上,它指的是一種可以維護任何類型數據信息方法的抽象。所有的資源均保存在一個 Zend 內部的資源列表當中。列表中的每份資源都有一個指向可以表明其種類的類型定義的指針。Zend 在內部統一管理所有對資源的引用。直接訪問一個資源是不大可能的,你只能通過提供的 API 來對其進行操作。某個資源一旦失去引用,那就會觸發調用相應的析構函數。

舉例來說,數據庫連接和文件描述符就是一種資源。MySQL 模塊中就有其“標準”實現。當然其他模塊(比如 Oracle 模塊)也都用到了資源。

注意:
實際上,一個資源可以指向函數中任何一種你所感興趣的數據(比如指向一個結構等等)。並且用戶也只能通過某個資源變量來將資源信息傳遞給相應的函數。

要想創建一個資源你必須先註冊一個這個資源的析構函數。這是因爲Zend 需要了解當你把某些數據存到一個資源裏後,如果不再需要這份資源時該如何將其釋放。這個析構函數會在釋放資源(無論是手工釋放還是自動釋放)時被 Zend 依次調用。析構函數註冊後,Zend 會返回一個此種資源類型句柄。這個句柄會在以後任何訪問此種類型的資源的時候被用到,而且這個句柄絕大部分時間都保存在擴展的全局變量裏面。這裏你不需要擔心線程安全方面的問題,因爲你只是需要在模塊初始化註冊一次就行了。

下面是這個用於註冊資源析構函數的 Zend 函數定義:

你或許已經注意到了,在該函數中我們需要提供兩種不同的資源析構函數:一種是普通資源的析構函數句柄,一種是持久化資源的析構函數句柄。持久化資源一般用於諸如數據庫連接等這類情況。在註冊資源時,這兩個析構函數至少得提供一個,另外一個析構函數可簡單地設爲 NULL。

zend_register_list_destructors_ex() 接受以下幾個參數:

ld 普通資源的析構函數。
pld 持久化資源的析構函數。
type_name 爲你的資源指定一個名稱。在 PHP 內部爲某個資源類型起個名字這是個好習慣(當然名字不能重複)。用戶調用 var_dump($resource) 時就可取得該資源的名稱。
module_number 這個參數在你模塊的 PHP_MINIT_FUNCTION 函數中會自動定義,因此你大可將其忽略。

返回值是表示該資源類型的具有唯一性的整數標識符,即資源類型句柄。

資源(不論是不是持久化資源)的析構函數都必須具有以下的函數原型:

voidresource_destruction_handler(zend_rsrc_list_entry *rsrc TSRMLS_DC);

參數 rsrc 指向一個 zend_rsrc_list_entry 結構:

typedefstruct_zend_rsrc_list_entry {
    void *ptr;
    inttype;
    intrefcount;
} zend_rsrc_list_entry;

成員 void *ptr 才真正指向你的資源。

現在我們就知道該怎麼開始了。我們先定義一個將要註冊到 Zend 內部的資源類型 my_resource,這個類型的結構很簡單,只有兩個整數成員:

typedefstruct {
    intresource_link;
    intresource_type;
} my_resource;

接着我們再定義一下這種資源的析構函數。這個析構函數大致上是以下這個樣子:

注意:
有一個很重要的事情必須要提一下:如果你的資源是一個比較複雜的結構,比如包含有你在運行時所申請內存的指針等,那你就必須在釋放資源本身前先釋放它們!

OK。現在我們定義了

我們的資源是什麼樣子;

我們資源的析構函數是什麼樣子。

那麼,我們還需要做哪些工作呢?我們還需要:

創建一個在整個擴展範圍內有效的全局變量用於保存資源類型句柄,這樣就可以在每個需要它的函數中都能訪問到它;

給我們的資源類型定義一個名稱;

完成前面定義的資源析構函數;

最後註冊這個析構函數。

註冊完這種資源的析構函數後,要真正註冊一個資源(實例),我們可以使用 zend_register_resource() 函數或使用 ZEND_REGISTER_RESOURE() 宏。這兩個的定義可以在 zend_list.h 中找到。儘管兩者的參數定義都是一一對應的,但使用宏通常可以得到更好的前向兼容性:

intZEND_REGISTER_RESOURCE(zval *rsrc_result, void *rsrc_pointer, intrsrc_type);
rsrc_result 這是一個初始化過 zval * 容器。
rsrc_pointer 指向所保存的資源。
rsrc_type 這個參數就是你在註冊函數析構函數時返回的資源類型句柄。對上面的代碼來說就是le_myresource le_myresource。

返回值就是表示這個資源(實例)的具有唯一性的整數。

那麼在我們註冊這個資源(實例)時究竟發生了什麼事呢?函數會從 Zend 內部某個列表取得一個空閒空間,然後將資源指針及類型保存到這個空間。最後這個空閒空間的索引被簡單地保存在給定的 zval * 容器裏面:

rsrc_id = zend_list_insert(rsrc_pointer, rsrc_type);
if (rsrc_result) {
    rsrc_result->value.lval = rsrc_id;
    rsrc_result->type = IS_RESOURCE;
}
return rsrc_id;

返回值 rsrc_id 就唯一性地標識了我們新註冊得到的那個資源。你可以使用宏 RETURN_RESOURE 來將其返回給用戶:

RETURN_RESOURCE(rsrc_id)

注意:
如果你想立刻把這個資源返回給用戶,那你就應該把 return_value 作爲那個 zval * 容器。這也是我們推薦的一種編程實踐。

Zend 引擎從現在就會開始跟蹤所有對這個資源的引用。一旦對這個資源的引用全都不存在了,那麼你在前面爲這個資源所註冊的析構函數就會被調用。這樣做的好處就是你不用擔心會在你的模塊裏面引入內存泄漏-你只需要把你調用腳本中所有需要分配的內存都註冊成資源即可。這樣一來,一旦腳本認爲不再需要它們的時候,Zend 就會找到它們然後再通知你(這就是 callback,譯註)。

現在用戶已經通過在某處傳入到你函數的參數拿到了他的資源。zval * 容器中的 value.lval 包含了你資源的標識符,然後他就可以宏 ZEND_FETCH_RESOURCE 來獲取資源了:

ZEND_FETCH_RESOURCE(rsrc, rsrc_type, rsrc_id, default_rsrc_id, resource_type_name, resource_type)
rsrc 這個指針將指向你前面已經聲明過的資源。
rsrc_type 這個參數用以表明你你想要把前面參數的那個指針轉換爲何種類型。比如 myresource * 等等。
rsrc_id 這個是用戶傳進你函數的那個 zval *container 的地址。 假如給出的是 zval *z_resource ,那麼此處就應該是 &z_resource。
default_rsrc_id 這個參數表明假如沒有取到資源時默認指定的資源標識符。通常爲 -1。
resource_type_name 所請求的資源類型資源類型名稱。當不能找到資源時,就用這個字符串去填充系統由於維護而拋出的錯誤信息。
resource_type 這個可以取回在註冊資源析構函數時返回的資源類型。在本例就是 le_myresource。

這個宏沒有返回值。這對開發人員可能會方便了點。不過還是要注意添加 TSRM 參數和確認一下是否取回了資源。如果在接收資源時出現了問題,那它就會拋出一個警告信息並且會立刻從當前函數返回,其返回值爲 NULL。

如果想從列表強行刪除一個資源,可以使用 zend_list_delete() 函數。當然也可以強行增加引用計數,如果你知道你正在創建一個指向已分配內存資源的引用(比如說你可能想重用一個默認的數據庫連接)。對於這種情況你可以使用函數 zend_list_addref() 。想要查找一個已分配內存的資源,請使用 zend_list_find() 函數。關於這些操作的完整 API 請參見 zend_list.h。

自動創建全局變量的宏

作爲我們早期所談論的一些宏的補充,還有一些宏可以讓我們很方便的創建全局變量。瞭解了它們,我們在引入一些全局標識時就會感覺很爽,不過這個習慣可能會不太好。在“表3.12 創建全局變量的宏”中描述了完成這些任務所用到的正確的宏。它們不需要申請任何 zval 容器,你只需簡單地提供一個變量名和其值即可。

表3.12 創建全局變量的宏

說明
SET_VAR_STRING(name, value) 新建一個字符串變量。
SET_VAR_STRINGL(name, value, length) 新建一個指定長度的字符串變量。這個宏要比 SET_VAR_STRING 快而且還是二進制安全的。
SET_VAR_LONG(name, value) 新建一個長整型變量。
SET_VAR_DOUBLE(name, value) 新建一個雙精度變量。

創建常量

Zend 支持創建真正的常量。訪問常量時不需要 $ 前綴,而且常量是全局有效的。比如 TRUE 和 FALSE 這兩個常量。

要想創建一個常量,你可以使用“表3.13 創建常量的宏”中所列舉的宏來完成這項工作。所有的宏在創建常量時都必須指定一個名稱和值。

你還可以爲常量指定一個特別的標識:

    CONST_CS – 這個常量的名稱是大小寫敏感的;

    CONST_PERSISTENT – 這個常量是持久化的。換句話說,當攜帶這個常量的進程關閉時這個常量在剩下的請求中還依然有效,並不會被“遺忘”。

可以使用二進制的“或(OR)”操作來使用其中的一個或兩個標識:

// 註冊一個長整型常量
REGISTER_LONG_CONSTANT("NEW_MEANINGFUL_CONSTANT", 324, CONST_CS | CONST_PERSISTENT);

我們提供有兩種不同類型的宏,分別是 REGISTER_*_CONSTANT 和 REGISTER_MAIN_*_CONSTANT。第一種類型在創建常量時只會綁定到當前模塊。一旦註冊這個模塊的常量從內存中卸載,那麼這個常量也就會隨即消逝。第二種類型創建的變量將會獨立於該模塊,始終保存在符號表中。

表3.13 創建常量的宏

說明
REGISTER_LONG_CONSTANT(name, value, flags)
REGISTER_MAIN_LONG_CONSTANT(name, value, flags)
新建一個長整型常量。
REGISTER_DOUBLE_CONSTANT(name, value, flags)
REGISTER_MAIN_DOUBLE_CONSTANT(name, value, flags)
新建一個雙精度型常量。
REGISTER_STRING_CONSTANT(name, value, flags)
REGISTER_MAIN_STRING_CONSTANT(name, value, flags)
新建一個字符串常量。給定的字符串的空間必須在Zend 內部內存。
REGISTER_STRINGL_CONSTANT(name, value, length, flags)
REGISTER_MAIN_STRINGL_CONSTANT(name, value, length, flags)
新建一個指定長度的字符串常量。同樣,這個給定的字符串的空間也必須在Zend 內部內存。

(十二)使用拷貝構造函數複製變量內容

遲早你會遇到把一個 zval 容器的內容賦給另外一個 zval 容器的情況。不過可別想當然,這事說起來容易做起來可有點難度。因爲 zval 容器不但包含了類型信息,而且還有對 Zend 內部數據的一些引用。比如,數組以及對象等依據其大小大都或多或少包含了一些哈希表結構。而我們在將一個 zval 賦給另外一個 zval 時,通常都沒有複製這些哈希表本身,複製的只是這些哈希表的引用而已。

爲了能夠正確複製這些複雜類型的數據,我們可以使用“拷貝構造函數(copy constructor)”來完成這項工作。拷貝構造函數在某些爲了可以複製複雜類型數據而支持操作符重載的語言中有着代表性的應用。如果你在這種語言中定義了一個對象,那你就可能想爲其重載(Overloading)一下“=”操作符,這個操作符通常用於將右值(操作符右邊表達式的值)賦給左值(操作符左邊表達式的值)。

“重載”就意味着將給予這個操作符另外一種不同的含義,它通常會把這個操作符跟某個函數調用關聯起來。當這個操作符作用在一個對象上時,與之關聯的函數就將會被調用,同時該操作符的左值和右值也會作爲該函數的參數一併傳入。這樣,這個函數就可以完成“=”操作符想要完成的事情(一般是某些額外數據的複製)。

這些“額外數據的複製”對 PHP 的 zval 容器來說也是很有必要的。對於數組來說,“額外數據的複製”就是指另外再重建和複製那些與該數組有關的哈希表(因爲當初我們複製 zval 時複製的僅僅是這些哈希表的指針)。而對字符串來說,“額外數據的複製”就意味着我們必須重新爲字符串值去申請空間。如此類推。

Zend Engine 會調用一個名爲 zval_copy_ctor()(在以前的 PHP 版本中這個函數叫做 pval_copy_constructor() )的函數來完成這項工作。

下面這個示例爲我們展示了這樣一個函數:它接收一個複雜類型的參數,在對其進行一定的修改後把它作爲結果返回給 PHP:

函數的頭一部分沒什麼可說的,只是一段很平常的接收參數的代碼而已。不過在對這個參數進行了某些修改後就變得有趣起來了:先是把 parameter 容器值賦給了(預先定義好的)return_value 容器,然後爲了能夠真正複製這個容器,我們便調用了拷貝構造函數。這個拷貝構造函數能夠直接處理它的參數,處理成功則返回 SUCCESS,否則返回 FAILURE。

在這個例子當中如果你忘了調用這個拷貝構造函數,那麼 parameter 和 return_value 就會分別指向同一個 Zend 內部數據,也就是說返回值 return_value 非法指向了一個數據結構。當你修改了參數 parameter 時這個函數的返回值就可能會受到影響。因此爲了創建一個獨立的拷貝,我們必須調用這個函數。

在 Zend API 中還有一個與拷貝構造函數相對應的拷貝析構函數:zval_dtor(),它做的工作正好與拷貝構造函數相反。

(十三)返回函數值

關於擴展內函數到 PHP 腳本的返回值我們前面談得比較少,這一節我們就來詳細說一下。任何函數的返回值都是通過一個名爲 return_value 的變量傳遞的。這個變量同時也是函數中的一個參數。這個參數總是包含有一個事先申請好空間的 zval 容器,因此你可以直接訪問其成員並對其進行修改而無需先對 return_value 執行一下 MAKE_STD_ZVAL 宏指令。

爲了能夠更方便從函數中返回結果,也爲了省卻直接訪問 zval 容器內部結構的麻煩,ZEND 提供了一大套宏命令來完成相關的這些操作。這些宏命令會自動設置好類型和數值。“表3.14 從函數直接返回值的宏”和“表3.15 設置函數返回值的宏”列出了這些宏和對應的說明。

注意:
使用“表3.14 從函數直接返回值的宏”會自動攜帶結果從當前函數返回。而使用“表3.15 設置函數返回值的宏”則只是設置了一下函數返回值,並不會馬上返回。

表3.14 從函數直接返回值的宏

說明
RETURN_RESOURCE(resource) 返回一個資源。
RETURN_BOOL(bool) 返回一個布爾值。
RETURN_NULL() 返回一個空值。
RETURN_LONG(long) 返回一個長整數。
RETURN_DOUBLE(double) 返回一個雙精度浮點數。
RETURN_STRING(string, duplicate) 返回一個字符串。duplicate 表示這個字符是否使用 estrdup() 進行復制。
RETURN_STRINGL(string, length, duplicate) 返回一個定長的字符串。其餘跟 RETURN_STRING 相同。這個宏速度更快而且是二進制安全的。
RETURN_EMPTY_STRING() 返回一個空字符串。
RETURN_FALSE 返回一個布爾值假。
RETURN_TRUE 返回一個布爾值真。

表3.15 設置函數返回值的宏

說明
RETVAL_RESOURCE(resource) 設定返回值爲指定的一個資源。
RETVAL_BOOL(bool) 設定返回值爲指定的一個布爾值。
RETVAL_NULL 設定返回值爲空值
RETVAL_LONG(long) 設定返回值爲指定的一個長整數。
RETVAL_DOUBLE(double) 設定返回值爲指定的一個雙精度浮點數。
RETVAL_STRING(string, duplicate) 設定返回值爲指定的一個字符串,duplicate 含義同 RETURN_STRING。
RETVAL_STRINGL(string, length, duplicate) 設定返回值爲指定的一個定長的字符串。其餘跟 RETVAL_STRING 相同。這個宏速度更快而且是二進制安全的。
RETVAL_EMPTY_STRING 設定返回值爲空字符串。
RETVAL_FALSE 設定返回值爲布爾值假。
RETVAL_TRUE 設定返回值爲布爾值真。

如果需要返回的是像數組和對象這樣的複雜類型的數據,那就需要先調用 array_init() 和 object_init(),也可以使用相應的 hash 函數直接操作 return_value。由於這些類型主要是由一些雜七雜八的東西構成,所以對它們就沒有了相應的宏。

(十四)信息輸出

就像我們在腳本中使用 print() 函數一樣,我們也經常需要從擴展向輸出流輸出一些信息。在這方面-比如輸出警告信息、phpinfo() 中對應的信息等一般性任務-PHP 也爲我們提供了一系列函數。這一節我們就來詳細地討論一下它們。

zend_printf()

zend_printf() 功能跟 printf() 差不多, 唯一不同的就是它是向 Zend 的輸出流提供信息。

zend_error()

zend_error() 用於創建一個錯誤信息。這個函數接收兩個參數:第一個是錯誤類型(見 zend_error.h),第二個是錯誤的提示消息。

zend_error(E_WARNING, "This function has been called with empty arguments");

“表3.16 Zend 預定義的錯誤信息類型” 列出了一些可能的值(在 PHP 5.0 及以上版本中又增加了一些錯誤類型,可參見 zend_error.h,譯註)。這些值也可以用在 php.ini 裏面,這樣你的錯誤信息將會依照 php.ini 裏面的設置,根據不同的錯誤類型而被選擇性地記錄。

表 3.16 Zend 預定義的錯誤信息類型

錯誤類型 說明
E_ERROR 拋出一個錯誤,然後立即中止腳本的執行。
E_WARNING 拋出一個一般性的警告。腳本會繼續執行。
E_NOTICE 拋出一個通知,腳本會繼續執行。注意: 默認情況下 php.ini 會關閉顯示這種錯誤。
E_CORE_ERROR 拋出一個 PHP 內核錯誤。通常情況下這種錯誤類型不應該被用戶自己編寫的模塊所引用。
E_COMPILE_ERROR 拋出一個編譯器內部錯誤。通常情況下這種錯誤類型不應該被用戶自己編寫的模塊所引用。
E_COMPILE_WARNING 拋出一個編譯器內部警告。通常情況下這種錯誤類型不應該被用戶自己編寫的模塊所引用。

示例:

在瀏覽器中顯示警告信息

向 phpinfo() 中輸出信息

在創建完一個模塊之後,你可能就會想往 phpinfo() 裏面添加一些關於你自己模塊的一些信息了(默認是隻顯示你的模塊名)。PHP 允許你用 ZEND_MINFO() 函數向 phpinfo() 裏面添加一段你自己模塊的信息。這個函數應該被放在模塊描述塊(見前文)部分,這樣在腳本調用 phpinfo() 時模塊的這個函數就會被自動調用。

如果你指定了 ZEND_MINFO 函數,phpinfo() 會自動打印一個小節,這個小節的頭部就是你的模塊名。其餘的信息就需要你自己去指定一下格式並輸出了。

一般情況下,你需要先調用一下 php_info_print_table_start(),然後再調用 php_info_print_table_header() 和 php_info_print_table_row() 這兩個標準函數來打印表格具體的行列信息。這兩個函數都以表格的列數(整數)和相應列的內容(字符串)作爲參數。最後使用 php_info_print_table_end() 來結束打印表格。“例3.13 源代碼及其在 phpinfo() 函數中的屏幕顯示”向我們展示了某個樣例和它的屏幕顯示效果。

例3.13 源代碼及其 在 phpinfo() 函數中的屏幕顯示

執行時信息

你還可以輸出一些執行時信息,像當前被執行的文件名、當前正在執行的函數名等等。當前正在執行的函數名可以通過 get_active_function_name() 函數來獲取。這個函數沒有參數(譯註:原文即是如此,事實上是跟後面提到的 zend_get_executed_filename() 函數一樣需要提交 TSRMLS_C 宏參數,譯註),返回值爲函數名的指針。當前被執行的文件名可以由 zend_get_executed_filename() 函數來獲得。這個函數需要傳入 TSRMLS_C 宏參數來訪問執行器全局變量。這個執行器全局變量對每個被 Zend 直接調用的函數都是有效的(因爲 TSRMLS_C 是我們前文討論過的參數宏 INTERNAL_FUNCTION_PARAMETERS 的一部分)。如果你想在其他函數中也訪問這個執行器全局變量,那就需要現在那個函數中調用一下宏 TSRMLS_FETCH()。

最後你還可以通過 zend_get_executed_lineno() 函數來取得當前正在執行的那一行代碼所在源文件中的行數。這個函數同樣需要訪問執行器全局變量作爲其參數。關於這些函數的應用,請參閱“例3.14 輸出執行時信息”。

例 3.14 輸出執行時信息

示例:

輸出執行時信息

(十五)啓動函數與關閉函數

啓動函數和關閉函數會在模塊的(載入時)初始化和(卸載時)反初始化時被調用,而且只調用這一次。正如我們在本章前面(見 Zend 模塊描述塊的說明)所提到的,它們是模塊和請求啓動和關閉時所發生的事件。

模塊啓動/關閉函數會在模塊加載和卸載時被調用。請求啓動/關閉函數會在每次處理一個請求時(也就是在執行一個腳本文件時)被調用。

對於動態加載的擴展而言,模塊和請求的啓動函數與模塊和請求的關閉函數都是同時發生的(嚴格來說模塊啓動函數是先於請求啓動函數被調用的,譯註)。

可以用某些宏來聲和明實現這些函數,詳情請參閱前面的關於“Zend 模塊聲明”的討論。

(十六)調用用戶函數

PHP 還允許你在你的模塊裏面調用一些一些用戶定義的函數,這樣在實現某些回調機制(比如在做一些數組的輪循(array walking)、搜索或設計一些簡單的事件驅動的程序時)時會很方便。

我們可以通過調用 call_user_function_ex() 來調用用戶函數。它需要你即將訪問函數表的指針、這個對象的指針(假如你訪問的是類的一個方法的話),函數名、返回值、參數個數、具體的參數數組和一個是否需要進行 zval 分離的標識(這個函數原型已經“過時”了,至少是從 PHP 4.2 開始這個函數就追加了一個 HashTable *symbol_table 參數。下面所列舉的函數原型更像是 call_user_function () 的聲明。譯註)。

需要注意的是你不必同時指定 function_table 和 object 這兩個參數,只需要指定其中一個就行了。不過如果你想調用一個方法的話,那你就必須提供一個包含此方法的對象。這時 call_user_function() 會自動將函數表設置爲當前這個對象的函數表。而對於其他情況,只需要設定一下 function_table 而把 object 設爲 NULL 就行了。

一般情況下,默認的函數表是包含有所有函數的“根”函數表。這個函數表是編譯器全局變量的一部分,你可以通過 CG() 宏來訪問它。如果想把編譯器全局變量引入你的函數,只需先執行一下 TSRMLS_FETCH 宏就可以了。

而調用的函數名是保存在一個 zval 容器內的。猛一下你可能會感到好奇,但其實這是很合乎邏輯的。想想看,既然我們在腳本中的大部分時間都是在接收一個函數名作爲參數,並且這個參數還是被轉換成(或被包含在)一個 zval 容器。那還不如現在就直接把這個 zval 容器傳送給函數,只是這個 zval 容器的類型必須爲 IS_STRING。

下一個參數是返回值 return_value 的指針。這個容器的空間函數會自動幫你申請,所以我們無需手動申請,但在事後這個容器空間的銷燬釋放工作得由我們自己(使用 zval_dtor())來做。

跟在 return_value 後面的是一個標識參數個數的整數和一個包含具體參數的數組。最後一個參數 no_separation 指明瞭函數是否禁止進行 zval 分離操作。這個參數應該總是設爲 0,因爲如果設爲 1 的話那這個函數會節省一些空間但要是其中任何一個參數需要做 zval 分離時都會導致操作失敗。

“例3.15 調用用戶函數”向我們展示如何去調用一個腳本中的用戶函數。這段代碼調用了一個我們模塊所提供的 call_userland() 函數。模塊中的 call_userland() 函數會調用腳本中一個名爲它的參數的用戶函數,並且將這個用戶函數的返回值直接作爲自己的返回值返回腳本。另外你可能注意到了我們在最後調用了析構函數。這個操作或許沒有太大必要(因爲這些值都應該是分離過的,對它們的賦值將會很安全),但這麼做總沒有什麼壞處,說不定在某個關鍵時刻它成爲我們的一道“免死金牌”。:D

例3.15 調用用戶函數

調用腳本:

上例將輸出:

We are in the test function! We have 3 as type Return value: 'hello'

(十七)支持初始化文件php.ini

PHP4 重寫了對初始化文件的支持。現在你可以直接在代碼中指定一些初始化選項,然後在運行時讀取和改變這些選項值,甚至還可以在選項值改變時接到相關通知。

如果想要爲你的模塊創建一個 .ini 文件的配置節,可以使用宏 PHP_INI_BEGIN() 來標識這個節的開始,並用 PHP_INI_END() 表示該配置節已經結束。然後在兩者之間我們用 PHP_INI_ENTRY() 來創建具體的配置項。

PHP_INI_ENTRY() 總共接收 4 個參數:配置項名稱、初始值、改變這些值所需的權限以及在值改變時用於接收通知的函數句柄。配置項名稱和初始值必須是一個字符串,即使它們是一個整數。

更改這些值所需的權限可以劃分爲三種:PHP_INI_SYSTEM 只允許在 php.ini 中改變這些值;PHP_INI_USER 允許用戶在運行時通過像 .htaccess 這樣的附加文件來重寫其值;而 PHP_INI_ALL 則允許隨意更改。其實還有第四種權限:PHP_INI_PERDIR,不過我們還暫時不能確定它有什麼影響。(本段關於這幾種權限的說明與手冊中《附錄G php.ini 配置選項》一節的描述略有出入。根據譯者自己查到的資料,相比之下還是《附錄G php.ini 配置選項》更爲準確些。譯註)

第四個參數是初始值被改變時接收通知的函數句柄。一旦某個初始值被改變,那麼相應的函數就會被調用。這個函數我們可以用宏 PHP_INI_MH 來定義:

改變後的新值將會以字符串的形式並通過一個名爲 new_value 變量傳遞給函數。要是再注意一下 PHP_INI_MH 的定義就會發現,我們實際上用到了不少參數:

這些定義都可以在 php_ini.h 文件裏找到。可以發現,我們的通知接收函數可以訪問整個配置項、改變後的新值以及它的長度和其他三個可選參數。這幾個可選參數可以通過 PHP_INI_ENTRY1(攜帶一個附加參數)、PHP_INI_ENTRY2(攜帶兩個附加參數)、PHP_INI_ENTRY3(攜帶三個附加參數)等宏來加以指定。

關於值改變的通知函數應該被用來本地緩存一些初始花選項以便可以更快地對其訪問或被用來從事一個值發生改變時所要求完成的任務。比如要是一個模塊對一個主機常量進行了連接,而這時有人改變了主機名,那麼就需要自動地關閉原來的連接,並嘗試進行新的連接。

可以使用“表3.17 PHP 中用以訪問初始化配置項的宏”來訪問初始化配置項:

表3.17 PHP 中用以訪問初始化配置項的宏

說明
INI_INT(name) 將配置項 name 的當前值以長整數返回。
INI_FLT(name) 將配置項 name 的當前值以雙精度浮點數返回。
INI_STR(name) 將配置項 name 的當前值以字符串返回。 注意:這個字符串不是複製過的字符串,而是直接指向了內部數據。如果你需要進行進一步的訪問的話,那就需要再進行復制一下。
INI_BOOL(name) 將配置項 name 的當前值以布爾值返回。(返回值被定義爲 zend_bool,也就是說是一個 unsigned char)。
INI_ORIG_INT(name) 將配置項 name 的初始值以長整型數返回。
INI_ORIG_FLT(name) 將配置項 name 的初始值以雙精度浮點數返回。
INI_ORIG_STR(name) 將配置項 name 的初始值以字符串返回。 注意:這個字符串不是複製過的字符串,而是直接指向了內部數據。如果你需要進行進一步的訪問的話,那就需要再進行復制一下。
INI_ORIG_BOOL(name) 將配置項 name 的初始值以布爾值返回。(返回值被定義爲 zend_bool,也就是說是一個 unsigned char)。

最後,你還得把整個初始化配置項引入 PHP。這項工作可以在模塊的起始/結束函數中使用宏 REGISTER_INI_ENTRIES() 和 UNREGISTER_INI_ENTRIES() 來搞定。

ZEND_MINIT_FUNCTION(mymodule)
{
    REGISTER_INI_ENTRIES();
}

ZEND_MSHUTDOWN_FUNCTION(mymodule)
{
    UNREGISTER_INI_ENTRIES();
}

(十八)何去何從

現在你已經掌握了很多關於 PHP 的知識了。你已經知道了如何創建一個動態加載的模塊或被靜態連接的擴展。你還知道了在 PHP 和 Zend 的內部變量是如何儲存的,以及如何創建和訪問這些變量。另外你也知道了很多諸如輸出信息文本、自動將變量引入符號表等一系列工具函數的應用。

儘管這一章常常有點“參考”的意味,但我們還是希望它能給你一些關於如何開始編寫自己的擴展這方面的知識。限於篇幅,我們不得不省略了很多東西。我們建議你花些時間仔細研究一下它的頭文件和一些模塊(尤其是 ext/standard 目錄下的一些文件以及 MySQL 模塊,看一下這些衆所周知的函數究竟是怎麼實現的),看一下別人是怎麼使用這些 API 函數的,尤其是那些本章沒有提到的那些函數。

(十九)關於配置文件的一些宏

由 buildconf 處理的配置文件 config.m4 包含了所有在配置過程中所執行的指令。這些指令諸如包含測試包含所需的外部文件,像頭文件、庫文件等等。PHP 定義了一系列處理這類情況的宏,其中最常用的我們已經在“表3.18 config.m4 中的 M4 宏”列了出來。

表3.18 config.m4 中的 M4 宏

說明
AC_MSG_CHECKING(message) 在執行 configure 命令時輸出“checking <message>”等信息。
AC_MSG_RESULT(value) 取得 AC_MSG_CHECKING 的執行結果,一般情況下 value 應爲 yes 或 no。
AC_MSG_ERROR(message) 在執行 configure 命令時輸出一條錯誤消息 message 並中止腳本的執行。
AC_DEFINE(name,value,description) 向 php_config.h 添加一行定義:#define name value // description(這對模塊的條件編譯很有用。)
AC_ADD_INCLUDE(path) 添加一條編譯器的包含路徑,比如用於模塊需要爲頭文件添加搜索路徑。
AC_ADD_LIBRARY_WITH_PATH(libraryname,librarypath) 指定一個庫的連接路徑。
AC_ARG_WITH(modulename,description,unconditionaltest,conditionaltest) 這是一款比較強大的宏,用於將模塊的描述 description 添加到“configure –help”命令的輸出裏面。PHP 會檢查當前執行的 configure 腳本里面有沒有–with-<modulename> 這個選項。 如果有則執行 unconditionaltest 語句(比如 –with-myext=yes 等), 此時,選項的值會被包含在 $withval 變量裏面。否則就執行 conditionaltest 語句。
PHP_EXTENSION(modulename, [shared]) 這個是配置你的擴展時 PHP 必定調用的一個宏。你可以在模塊名後面提供第二個參數,用來表明是否將其編譯爲動態共享模塊。這會導致在編譯時爲你的源碼提供一個 COMPILE_DL_<modulename> 的定義。

(二十) API 宏

下面(見表3.19 訪問 zval 容器的 API 宏)是一些引入到 Zend API 裏面用於訪問 zval 容器的 API 宏。

指向
Z_LVAL(zval) (zval).value.lval
Z_DVAL(zval) (zval).value.dval
Z_STRVAL(zval) (zval).value.str.val
Z_STRLEN(zval) (zval).value.str.len
Z_ARRVAL(zval) (zval).value.ht
Z_LVAL_P(zval) (*zval).value.lval
Z_DVAL_P(zval) (*zval).value.dval
Z_STRVAL_P(zval_p) (*zval).value.str.val
Z_STRLEN_P(zval_p) (*zval).value.str.len
Z_ARRVAL_P(zval_p) (zval).value.ht
Z_LVAL_PP(zval_pp) (*zval).value.lval
Z_DVAL_PP(zval_pp) (**zval).value.dval
Z_STRVAL_PP(zval_pp) (**zval).value.str.val
Z_STRLEN_PP(zval_pp) (**zval).value.str.len
Z_ARRVAL_PP(zval_pp) (**zval).value.ht
標籤:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章