Extension Writing Part I: Introduction to PHP and Zend
譯:我在江湖丢了
原文位址:http://devzone.zend.com/303/extension-writing-part-i-introduction-to-php-and-zend/
部落格位址:http://lg.uuhonghe.com/index/view?id=3
簡介
何為擴充
生命周期
Hello World
建立自己的擴充
ini 配置
全局變量
設定 ini 為全局變量
完整性檢查
然後哩?
簡介
如果你正閱讀本教程,那你可能對PHP語言的擴充編寫頗感興趣。如果不是。。。也許你讀完之後會發現你對這個之前不知道的東西産生了興趣。
本文假設讀者基本了解PHP語言和用C寫的PHP解釋器。
讓我們先确認一下你為何想寫一個PHP擴充:
1、由于語言核心的抽象深度使你有一些庫和系統調用無法使用PHP直接完成。
2、你希望PHP以一些不尋常的方法實作自身行為。
3、你已經寫了一堆PHP代碼,但你知道它可以跑得更快。
4、你有個實作了特别機智點子的代碼想賣,但更重要的是你要賣的代碼要能跑但不能在源碼裡看到。
這些都是非常正當的理由了,但要建立一個擴充,首先你得了解什麼是擴充。
何為擴充?
如果你寫過PHP,那你一定用過擴充了。隻需要小小的幾個擴充,一切PHP裡的使用者空間功能都在這個或那個擴充的函數組裡了。大量這些函數都在标準擴充的一部分——标準擴充總共的400多個。PHP源碼裡捆綁了86個擴充,平均每個有大概30個函數。掐指一算,總約2500個函數。如果這還不夠,PECL倉庫裡還提供了100個以上的附加擴充,更多的可以再網上别的地方找到。
“這些函數都在擴充裡,那還有什麼?”你會問,“它們擴充了什麼?PHP的核心是什麼?”。
PHP的核心由兩部分組成。在最底層你能找到Zend引擎(簡稱ZE)。ZE把人能識别的腳本解析為機器識别的符号,并且在程序空間裡運作這些符号。ZE同時處理記憶體管理,變量域和函數調用。這種區分方式的另一部門是PHP核心。PHP核心處理通信、連接配接和SAPI層(Server Application Programming Interface, 通常也用于指主機環境,如Apache, IIS, CLI, CGI 等),還提供了控制層對 safe_mode 和 open_basedir 的統一檢測,還有和用于檔案和網絡I/O的使用者空間函數fopen(), fread()和fwrite()相關的串流層。
生命周期
當一個SAPI啟動,例如在 /usr/local/apache/bin/apachectl start 的響應中,PHP以初始化它的核心子系統開始。在這個啟動全程将結束之際,會加載每個擴充的核心并調用他們的子產品初始化例程(MINIT)。
這給每個擴充一個機會去初始化内部變量,配置設定資源,注冊資源句柄,并且使用ZE注冊它的函數,以便當腳本調這些函數時,ZE知道運作哪段代碼。
接下來,PHP等待SAPI層來請求一個頁面去處理。在CGI或者CLI SAPI的情況下,這會直接發生并且隻發生一次。在Apache, IIS 或者其他一些成熟的 web 伺服器SAPI中,這是在遠端使用者請求時發生,并且或發生多次,可能伴随着并發。無論請求如何到達,PHP以通知ZE建立一個供腳本運作的環境為開始,然後調用每個擴充的請求初始化(RINI)函數。RINI給擴充一個機會去建立自己特定的環境變量,配置設定請求特定的資源,或執行其他任務如審計。RINI函數行為的一個主要例子是在sessions 擴充裡,如果session.auto_start項是開啟的, RINI将會自動觸發使用者空間session_start()函數并且預設定$_SESSION變量。
一旦請求被初始化了,ZE通過翻譯PHP腳本為tokens,最後轉為opcodes來接管,opcodes能夠單步調試和執行。
當其中一個opcodes包含的一個擴充方法被調用,ZE會捆綁上該方法的參數,并且臨時性地交出控制直接方法完成。
在腳本運作完成之後,PHP調用每個擴充的請求關閉(RSHUTDOWN)函數來做最後的清理工作(例如保證會話變量到磁盤)。接着,ZE運作一個清理程序(被稱為垃圾回收),該程序能有效在請求前期中使用的每個變量上執行unset()操作.
操作完成後,PHP等待SAPI請求另一個文檔或者信号關閉。在CGI和CLI SAPI情況下,是沒有“下一個請求”的,是以SAPI直接啟動關閉程序。在關閉程序中,PHP再次周遊每個擴充,調用子產品關閉(MSHUTDOWN)函數,最後關閉自己的核心子系統。
以上或許聽來使人生畏,然後當你開始鑽石一個工作中的擴充時,一些将逐漸清晰。
記憶體管理
為了防止寫得很爛的擴充記憶體丢失,ZE用一個表明持續性的附加辨別來執行它内部的記憶體管理器。持續性配置設定是很重要的,能保證記憶體配置設定持續到比一個頁面請求還長。相對而言,一個不持續性配置設定在它配置設定的請求結束時被釋放,無論釋放函數是否被調用。例如,使用者空間變量會在請求結束之後不再使用時候時配置設定為不持續性的。
然而也許一個擴充理論上會依賴ZE在頁面請求結束時自動釋放不持續性記憶體,這是不推薦的。記憶體配置設定會給未回收的一個更長的周期,與記憶體有關的資源則不太可能會被在恰當的時候關閉,沒有清理工作會讓這一次工作亂作一團。稍後你會看到,確定配置設定的資料适時清理其時是一件很簡單的事情,下面我們簡單的對比一下傳統記憶體配置設定(必須在使用外部類庫時使用)和PHP/ZE中的持續性與未持續性記憶體配置設定。
Traditional | Non-Persistent | Persistent |
---|---|---|
| | * |
| | |
| | |
| | |
** | | |
*
pemalloc()
家族包含一個 'persistent' 标志來讓他們與他們的未持續性部分相對應。例如:
safe_emalloc(1234)
和
safe_pemalloc(1234, 0)
一樣。
**
safe_emalloc()
和
safe_pemalloc()
(在 PHP5中) 加了個檢查避免整型溢出。
建構一個開發環境
現在我們已經學習了PHP和Zend引擎工作方式背後的一些理論,我猜你想運足功力開始開發了。然後在你開始前,必須得收集一些必備的工作來搭建一個滿足你需要的開發環境。
首先,你需要PHP本身,因為這一系列開發工具都離不開PHP。如果你還對使用源碼搭建PHP不熟悉,推薦你看下這篇文章先:http://www.php.net/install.unix. (使用Windows開發PHP擴充的文章稍後給出)。雖然用你的linux發行版裡的二進制包安全是非常誘人的,但是這些會遺漏兩項在開發過程中十分友善的./configure選項。第一個就是
--enable-debug
。 這個選項令PHP在編譯時加入可執行的符号資訊,以便當發生一個段錯誤時,你能從核心存儲中得到它并且使用gdb跟蹤段錯誤發生的地方和原因。另外一個選項取決于你開發使用的PHP的版本。在PHP4.3中這選項名為
--enable-experimental-zts
,在PHP5或更高的版本中為
--enable-maintainer-zts
。該先項讓PHP在多線程環境中思考自己的操作,讓你捕獲一些常見在非線程環境下是無害而導緻您的擴充在多線程環境下用不了的異常。隻要你在編譯PHP時使用了這些額外的選項安裝在你的開發伺服器(或者工作站)上,你可以開始編寫你的第一個擴充了。
Hello World
任何沒有完成Hello World應用的程式介紹都是不完整的。是以,下面将制作一個隻有一個傳回字元串"Hello World"的函數的擴充。在PHP代碼裡你可能這樣寫:
<?php
function hello_word(){
return 'Hello World';
}
?>
現在我們使用PHP擴充來實作這段代碼,首先我們在PHP源碼的ext/目錄下建立目錄hello并且進到該目錄下。事實上該目錄在不在PHP目錄樹下都可以,但是我讓你把它放在這裡以便後面将展示一個不相關的概念。該目錄下需要建立三個檔案:一個包含
hello_world
方法的源檔案,一個包含用于讓PHP加載你的擴充的引用的頭檔案,和一個用于讓phpize為編譯擴充做準備的配置檔案。
config.m4
PHP_ARG_ENABLE(hello, whether to enable Hello World support, [--enable-hello Enable Hello World support])
if test "$PHP_HELLO" = "yes"; then
AC_DEFINE(HAVE_HELLO, 1, [Whether you have Hello World])
PHP_NEW_EXTENSION(hello, hello.c, $ext_shared)
fi
php_hello.h
#ifndef PHP_HELLO_H
#define PHP_HELLO_H 1
#define PHP_HELLO_WORLD_VERSION "1.0"
#define PHP_HELLO_WORLD_EXTNAME "hello"
PHP_FUNCTION(hello_world);
extern zend_module_entry hello_module_entry;
#define phpext_hello_ptr &hello_module_entry
#endif
hello.c
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "php_hello.h"
static function_entry hello_functions[] = {
PHP_FE(hello_world, NULL)
{NULL, NULL, NULL}
};
zend_module_entry hello_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
PHP_HELLO_WORLD_EXTNAME,
hello_functions,
NULL,
NULL,
NULL,
NULL,
NULL,
#if ZEND_MODULE_API_NO >= 20010901
PHP_HELLO_WORLD_VERSION,
#endif
STANDARD_MODULE_PROPERTIES
};
#ifdef COMPILE_DL_HELLO
ZEND_GET_MODULE(hello)
#endif
PHP_FUNCTION(hello_world)
{
RETURN_STRING("Hello World", 1);
}
看得出來,上述示例擴充裡的大部分代碼是膠-協定語言,用于介紹擴充給PHP并且建立一個對話讓它們通信的。隻有最後4行代碼可以稱之後"real code",用于執行使用者空間層腳本能互動的任務。确實這一層的代碼看上去很像我們之前看的PHP代碼也容易看懂:
1、聲明一個方法
hello_world
2、讓這個方法傳回一個字元串:"Hello World"
3、。。呃。。。1?這個1幾個意思?
回憶一下,ZE包含了一套複雜的記憶體管理層,能確定配置設定的資源在腳本退出時被釋放。然而在記憶體管理的掌控下,釋放同樣的塊兩次是非常大的禁忌。這種行為通常稱為"double freeing",是一個常見的段錯誤的原因,涉及到一個正在調用的程式通路一個不再屬于它的記憶體塊。同樣的,你不希望允許ZE去釋放一個存活于程式空間并且其資料塊被其他程序占用的靜态字元串buffer(例如我們示例中的"Hello World")。
RETURN_STRING()
能夠假設任何一個傳遞給它的字元串都被複制過以便可以安全的釋放;但是因為在核心函數裡配置設定記憶體給一個字元串不太常見,是以動态的填充它,然後傳回,
RETURN_STRING()
允許使用者指定是不是必須複制字元串。為了進一步說明這個概念,下面這個代碼片段作用和上面相應部分一樣:
PHP_FUNCTION(hello_world)
{
char *str;
str = estrup("hello World");
RETURN_STRING(str, 0);
}
在此版本中,我們手動地配置設定記憶體給"Hello World"這個串,并且最終傳回調用腳本,然後把記憶體傳給
RETURN_STRING()
, 第二個參數值為0表示不需要複制一份,可以直接使用傳遞過來的。
編譯你的擴充
本練習的最後一步就是将你的擴充編譯為一個動态可加載子產品。如果你把上面的示例原封不動的抄下來,那麼隻需要在ext/hello/目錄下運作下面三步指令就行:
phpize
./configure --enable-hello
(譯者注:如果編譯PHP的時候使用了 --prefix 參數,此處要加上 --with-php-config 選項,
如筆者編譯PHP時使用的是 ./configure --prefix=/use/local/phpdev/ 此處指令應使用
./configure --enable-hello --with-php-config=/usr/local/phpdev/bin/php-config)
make
運作完上述三個指令之後,你應該在ext/hello/modules/下找到一個 hello.so 檔案。(譯者注:如果在make時報錯: error: unknown type name 'function_entry' ,可以把 'function_entry' 改為 'zend_function_entry',參見:https://bugs.php.net/bug.php?id=61479 )。現在,就像其他PHP擴充一樣,你可以把你的擴充拷貝到擴充目錄(預設為,/usr/local/lib/php/extensions/,可以通過php.ini确認)然後在php.ini裡加上extension=hello.so一行可以在以觸發它在程式啟動時被加載到了。對于CGI/CLI SAPIs 來說,啟動就指下一次運作PHP;而對我web server SAPIs如Apache來說,啟動指下次web server重新開機。讓我們試下運作下面指令:
$ php -r 'echo hello_world();'
如果一切順利,你現在應該能看到這段代碼輸出"Hello World"了,因為你的擴充裡的"hello_world()"傳回了一個字元串"Hello World",而echo指令會原封不動地顯示傳遞給他的參數(此處即為該函數的傳回值)。
其他标量也可用類似的方式傳回,使用
RETURN_LONG()
傳回整型,
RETURN_DOUBLE()
傳回浮點數,
RETURN_BOOL()
傳回true/false值,
RETURN_NULL()
你猜到了,傳回
NULL
值。讓我們來逐行分析一下在hello.c中
function_entry
結構體下通過
PHP_FE()
添加的行和檔案末尾的那些
PHP_FUNCTION()
都做了些什麼。
static function_entry hello_functions[] =
{
PHP_FE(hello_world, NULL)
PHP_FE(hello_long, NULL)
PHP_FE(hello_double, NULL)
PHP_FE(hello_bool, NULL)
PHP_FE(hello_null, NULL)
{NULL, NULL, NULL}
};
PHP_FUNCTION(hello_long)
{
RETURN_LONG(42);
}
PHP_FUNCTION(hello_double)
{
RETURN_DOUBLE(3.1415926535);
}
PHP_FUNCTION(hello_bool)
{
RETURN_BOOL(1);
}
PHP_FUNCTION(hello_null)
{
RETURN_NULL();
}
你同樣需要在頭檔案php_hello.h裡
hello_world()
的旁邊添加這些方法的原型,拿得建立過程能正确執行:
PHP_FUNCTION(hello_world);
PHP_FUNCTION(hello_long);
PHP_FUNCTION(hello_double);
PHP_FUNCTION(hello_bool);
PHP_FUNCTION(hello_null);
因為你沒有修改config.m4檔案,是以技術上這次跳過phpize和./configure這兩個步驟直接make是安全的。然後,在本遊戲的這個階段,我還是要求你從頭把三個步驟都執行一遍以确認活幹得漂亮。另外,最後一步的時候,你應該執行make clean all而不是簡單的執行make,來確定所有源檔案重建。再次聲明,現在的改動上述這些步驟是非必須的,但是會更安全更清晰。子產品建好後,再拷貝到你的擴充目錄下,替換舊版本。
這個時候你可以再次調用你的PHP解析器,運作一段簡單的腳本來試試你剛剛添加的方法。事實上,為何不現在不試呢?我等着呢。。。
試完了?很好。如果你使用
var_dump()
而不是
echo
來檢視每個函數的傳回值你也許會發現"hello_bool()"傳回true。這就是值1在"RETURN_BOOL()"所表示的。就像在PHP腳本裡,整型值
等于
FALSE
, 但是其他所有的整形值都等于
TRUE
。擴充作者經常使用"1"作這一系列的約定,我們希望你也這樣,但是也不必拘泥于此。為了更具可讀性,
RETURN_TRUE
和
RETURN_FALSE
兩個宏也可使用;現在再次修改"hello_bool()",這次使用"RETURN_TRUE":
PHP_FUNCTION(hello_bool){
RETURN_TRUE;
}
注意這裡沒有使用括号哦。
RETURN_TRUE
和
RETURN_FALSE
與其他宏
RETURN_*()
格式的變體,是以這裡注意别被捕獲。
你也許注意到上面這些代碼樣品我們都沒有傳0和1什來表示這些值是否需要被拷貝。這是因為沒有額外的記憶體(變量容器之外——我們将在第2部分深入)需要被配置設定或釋放,因為這些标量都很簡單很小。
還有另外三種傳回類型:
RESOURCE
(例如"mysql_connect()","fsockopen()"和"ftp_connect()"的傳回值),
ARRAY
(也稱為
HASH
),還有
OBJECT
(通過關鍵詞new傳回)。我們将會在第二章更深入變量學習的時候了解這一系列。
INI Settings
Zend 引擎提供了兩個方式處理
INI
變量。
我們先看一下較簡單的一種,而更全面、更複雜的方式,等以後你有機會接觸全局變量再說。
現在我們想在php.ini中定義一個變量,"hello.greeting", 用來處理你在"hello_function()"函數裡用來打招呼的變量。你需要在hello.c和php_hello.h中添另一些東西,并且
hello_module_entry
結構也得有所變化。以加入下面這些原型到php_hello.h的使用者空間方法原型旁邊:
PHP_MINIT_FUNCTION(hello);
PHP_MSHUTDOWN_FUNCTION(hello);
PHP_FUNCTION(hello_world);
PHP_FUNCTION(hello_long);
PHP_FUNCTION(hello_double);
PHP_FUNCTION(hello_bool);
PHP_FUNCTION(hello_null);
現在到hello.c中用下面這串代碼覆寫目前版本的
hello_module_entry
:
zend_module_entry hello_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
PHP_HELLO_WORLD_EXTNAME,
hello_functions,
PHP_MINIT(hello),
PHP_MSHUTDOWN(hello),
NULL,
NULL,
NULL,
#if ZEND_MODULE_API_NO >= 20010901
PHP_HELLO_WORLD_VERSION,
#endif
STANDARD_MODULE_PROPERTIES
};
PHP_INI_BEGIN()
PHP_INI_ENTRY("hello.greeting", "Hello World", PHP_INI_ALL, NULL)
PHP_INI_END()
PHP_MINIT_FUNCTION(hello)
{
REGISTER_INI_ENTRIES();
return SUCCESS;
}
PHP_MSHUTDOWN_FUNCTION(hello)
{
UNREGISTER_INI_ENTRIES();
return SUCCESS;
}
現在,你隻需要在hello.c頂部剩下的
#include
上加一個
#include
來包含支援
INI
的正确的頭:
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "php_ini.h
"#include "php_hello.h"
最後,我們修改
hello_world
方法來使用
INI
值:
PHP_FUNCTION(hello_world)
{
RETURN_STRING(INI_STR("hello.greeting"), 1);
}
注意,你複制了來自
INI_STR()
. 的傳回值。因為這涉及到PHP變量棧都關心的問題,這是一個靜态變量。實際上,如果你試圖去修改這個方法傳回的值,PHP運作環境會變得不穩定甚至可能崩潰。
首次修改的部分包含了兩個你需要熟悉的方法::
MINIT
, 和
MSHUTDOWN
。正如上面提到的,這兩個方法分别是在SAPI層初始啟動和最後關閉時調用的。他們不在請求之間也不在請求之中調用。在這個示例中,你用他們注冊在你擴充中定義的php.ini選項。在本文的後面,你将學習如何使用"MINIT"和"MSHUTDOWN"方法來注冊資源、對象和流句柄。
在你的方法hellow_world()中使用"INI_STR()"來取回目前"hello.greeting"項的值作為一個字元串。那些其他的已有方法取值作為長整型,浮點型和布爾型,如下表所示,"ORIG"補充了其他方法,能提供從
INI
設定中引用的值(在被.htaccess或者
ini_set()
修改之前的值)。
Current Value | Original Value | Type |
| | (NULL terminated) |
| | signed long |
| | signed double |
| | |
傳入
PHP_INI_ENTRY()
的第一個參數是一個包含了php.ini的選項的名稱的字元串。為避免命名空間碰撞,你必須使用和你方法相同的約定;那就是在所有值前加上你擴充的名字為字首,比如剛才的"hello.greeting",作一個慣例的約定,一度曾将擴充名和ini設定名中更具描述性的部分和分開。
第二個參數是初始值,并且通常是作為char*串不管是不是數字。這主要是因為實際上.ini檔案裡的值預設是文本——那是一個文本檔案。你可以在你的腳本中使用
INI_INT()
,
INI_FLT()
,或者
INI_BOOL()
來作類型轉換。
傳入的第三個參數是一個通路模式修飾符。這是一個用于決定
INI
在什麼時候什麼地方可以修改的位掩碼。對于某些,比如
register_globals
, 是很簡單的不允許在腳本中使用
ini_set()
來修改的,因為這個配置隻能在請求啟動腳本前有機會運作。另外如
allow_url_fopen
,是一些管理項,你不希望在共享主機環境中允許使用者修改,不管是
ini_set()
還是.htaccess指令。這個參數的另一個典型的值應該是
PHP_INI_ALL
,表示這個值在哪裡都能修改。另有
PHP_INI_SYSTEM|PHP_INI_PERDIR
,,表示這個配置能在php.ini檔案中設定,也能通過Apache指令在.htaccess檔案中修改,但是不允許通過
ini_set()
修改。再者有
PHP_INI_SYSTEM
,意思是該值隻能在php.ini檔案中修改,其他地方都不行。
我們就此跳過第四個參數,隻點出該值用來傳入一個回調方法便以ini配置無論在何時被修改時調用,比如當使用
ini_set()
修改時。這能令擴充更精确的控制設定被修改,或者通過修改一個新的配置來觸發相應的行為。
全局變量
通常,擴充需要在特定的請求裡跟蹤變量的值,使之獨立于并發請求。在無線程的SAPI中這可能比較簡單:隻需要在源檔案中聲明一個全局變量,在需要時調用。然而麻煩在于PHP被設計運作于線程級的web伺服器(如Apache 2 和 IIS),這就需要保證一個線程中的全局變量與其他線程中的分離。PHP通過使用TSRM(Thread Safe Resource Management)抽象層,大大地簡化了這一操作,有時被稱為ZTS(Zend Thread Safety)。實際 上,到現在你已經使用了一部分TSRM了,雖然你對其不甚了解。(不要急着去搜尋,通過這一系列的進展,你會發現它無處不在。)
建立線程安全全局變量的第一步,跟建立其他全局變量一樣,聲明它。為了實作這個例子,你将聲明一個以
long
型開始,值為
的全局變量。每次
hello_long()
方法被調用時,我們增值該變量變傳回。下面這段php_hello.h中的代碼放在
#define PHP_HELLO_H
子產品後面:
#ifdef ZTS
#include "TSRM.h"
#endif
ZEND_BEGIN_MODULE_GLOBALS(hello)
login counter;
ZEND_END_MODULE_GLOBALS(hello)
#ifdef ZTS
#define HELLO_G(v) TSRM(hello_globals_id, zend_hello_globals *, v)
#else
#define HELLO_G(v) (hello_globals.v)
#endif
這次你還将用到
RINIT
方法,是以得在頭檔案中聲明它的原型:
PHP_MINIT_FUNCTION(hello);
PHP_MSHUTDOWN_FUNCTION(hello);
PHP_RINIT_FUNCTION(hello);
現在 到hello.c中添加下面這段代碼到include 子產品後:
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "php_ini.h"
#include "php_hello.h"
ZEND_DECLARE_MODULE_GLOBALS(hello)
修改
hello_module_entry
添加
PHP_RINIT(hello)
:
zend_module_entry hello_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
PHP_HELLO_WORLD_EXTNAME,
hello_functions,
PHP_MINIT(hello),
PHP_MSHUTDOWN(hello),
PHP_RINIT(hello),
NULL,
NULL,
#if ZEND_MODULE_API_NO >= 20010901
PHP_HELLO_WORLD_VERSION,
#endif
STANDARD_MODULE_PROPERTIES
};
修改你的
MINIT
方法,添加另一對方法,來處理初始化啟動請求:
static void
php_hello_init_globals(zend_hello_globals *hello_globals)
{
}
PHP_RINIT_FUNCTION(hello)
{
HELLO_G(counter) = 0;
return SUCCESS;
}
PHP_MINIT_FUNCTION(hello)
{
ZEND_INIT_MODULE_GLOBALS(hello, php_hello_init_globals, NULL);
REGISTER_INI_ENTRIES();
return SUCCESS;
}
最後,修改你的
hello_long()
方法來使用這個值:
PHP_FUNCTION(hello_long)
{
HELLO_G(counter)++;
RETURN_LONG(HELLO_G(counter));
}
在添加到php_hello.h的代碼裡,使用了一對宏
ZEND_BEGIN_MODULE_GLOBALS()
和
ZEND_END_MODULE_GLOBALS()
建立了一個包含一個
long
變量的結構體
zend_hello_globals
。然後根據條件是否處于一個無線程環境定義了是從線程池取值還是僅從全局範圍取值的宏
HELLO_G()
。在hello.c中,我們使用
ZEND_DECLARE_MODULE_GLOBALS()
來建立了一個zend_hello_globals結構的執行個體,作為一個全局變量(如果在非線程安全中建立)或者一個線程資源池的一員。作為擴充作者,這個差別我們是不用但心的,Zend 引擎會幫我們處理好的。最後,在
MINI
中,使用
ZEND_INIT_MODULE_GLOBALS()
配置設定了一個線程安全資源id——現在還不用擔心這個是啥。你也許注意到
php_hello_init_globals()
并沒有作什麼,但是我們聲明
RINIT
時初始化counter為
,為何?關鍵在于這兩個函數何時被調用。
php_hello_init_globals()
僅在一個新程序或者線程啟動時調用;然而一個程序可以處理多個請求,是以使用這個方法初始化我們的counter為
隻會在第一個頁面請求時調用。接下來的請求到同一個程序的頁面會一直使用這個已存儲的值,因為不會從
開始計數。為了在每個頁面請求中初始化counter為
,我們實作了
RINIT
方法,如前所述的在頁面請求之前實作。我們在這裡包含
php_hello_init_globals()
方法是因為一會就要使用到它,傳一個
NULL
給
ZEND_INIT_MODULE_GLOBALS()
來初始化函數會在非線程平台導緻一個段錯誤 。
INI設定 vs 全局變量
如果你回觀前文,一個在
PHP_INI_ENTRY()
裡聲明的php.ini是作為字元串轉換成其他需要的類型,通過
INI_INT()
,
INI_FLT()
和
INI_BOOL()
。對某些配置,這代表在腳本執行的過程中會做大量重複的工作一遍又一遍的讀這個值。幸運的是,可以指定ZE使用特殊的資料類型存儲
INI
值,隻在其值改變時做類型轉換。讓我們通過聲明另一個
INI
值來試一下這個功能,這次使用布爾型标明計數器應該增還是減。首先修改php_hello.h下的
MODULE_GLOBALS
子產品如下:
ZEND_BEGIN_MODULE_GLOBAL(hello)
login counter;
zend_bool direction;
ZEND_ENG_MODULE_GLOBALS(hello)
然後,通過修改
PHP_INI_BEGIN()
子產品來聲明
INI
自己的值:
PHP_INI_BEGIN()
PHP_INI_ENTRY("hello.greeting", "Hello World", PHP_INI_ALL, NULL)
STD_PHP_INI_ENTRY("hello.direction", "1", PHP_INI_ALL, OnUpdateBool, direction, zend_hello_globals, hello_globals)
PHP_INI_END()
現在在
init_globals
方法裡初始化配置:
static void php_hello_init_globals(zend_hello_globals *hello_globals)
{
hello_globals->direction = 1;
}
最後,在hello_long()中使用這個配置的值來決定是自增還是自減:
PHP_FUNCTION(hello_long)
{
if (HELLO_G(direction)) {
HELLO_G(counter)++;
} else {
HELLO_G(counter)--;
}
RETURN_LONG(HELLO_G(counter));
}
就是這樣,我們在
INI_ENTRY
裡指定的方法
OnUpdateBool
方法會自動轉換php.ini, .htaccess或者腳本中通過
ini_set()
指定的值到相關的
TRUE
/
FALSE
值當你直接通過腳本通路的時候。
STD_PHP_INI_ENTRY
的最後三個參數告訴PHP去修改哪個全局變量,我們擴充的全局變量的結構體是什麼樣,以及他們包含的全局範圍的名稱。
完整性檢查
到現在,我們的三個檔案裡的内容應該如下所列的(為了可閱讀性,一些條目被移到了一起)。
config.m4
PHP_ARG_ENABLE(hello, whether to enable Hello World support, [ --enable-hello Enable Hello World support])
if test "$PHP_HELLO" = "yes"; then
AC_DEFINE(HAVE_HELLO, 1, [Whether you have Hello World])
PHP_NEW_EXTENSION(hello, hello.c, $ext_shared)
fi
php_hello.h
#ifndef PHP_HELLO_H
#define PHP_HELLO_H 1
#ifdef ZTS
#include "TSRM.h"
#endif
ZEND_BEGIN_MODULE_GLOBALS(hello)
long counter;
zend_bool direction;
ZEND_END_MODULE_GLOBALS(hello)
#ifdef ZTS
#define HELLO_G(v) TSRMG(hello_globals_id, zend_hello_globals *, v)
#else
#define HELLO_G(v) (hello_globals.v)
#endif
#define PHP_HELLO_WORLD_VERSION "1.0"
#define PHP_HELLO_WORLD_EXTNAME "hello"
PHP_MINIT_FUNCTION(hello);
PHP_MSHUTDOWN_FUNCTION(hello);
PHP_RINIT_FUNCTION(hello);
PHP_FUNCTION(hello_world);
PHP_FUNCTION(hello_long);
PHP_FUNCTION(hello_double);
PHP_FUNCTION(hello_bool);
PHP_FUNCTION(hello_null);
extern zend_module_entry hello_module_entry;
#define phpext_hello_ptr &hello_module_entry
#endif
hello.c
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "php_ini.h"
#include "php_hello.h"
ZEND_DECLARE_MODULE_GLOBALS(hello)
static function_entry hello_functions[] = {
PHP_FE(hello_world, NULL)
PHP_FE(hello_long, NULL)
PHP_FE(hello_double, NULL)
PHP_FE(hello_bool, NULL)
PHP_FE(hello_null, NULL)
{NULL, NULL, NULL}
};
zend_module_entry hello_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
PHP_HELLO_WORLD_EXTNAME,
hello_functions,
PHP_MINIT(hello),
PHP_MSHUTDOWN(hello),
PHP_RINIT(hello),
NULL,
NULL,
#if ZEND_MODULE_API_NO >= 20010901
PHP_HELLO_WORLD_VERSION,
#endif
STANDARD_MODULE_PROPERTIES
};
#ifdef COMPILE_DL_HELLO
ZEND_GET_MODULE(hello)
#endif
PHP_INI_BEGIN()
PHP_INI_ENTRY("hello.greeting", "Hello World",
PHP_INI_ALL, NULL)
STD_PHP_INI_ENTRY("hello.direction", "1", PHP_INI_ALL,
OnUpdateBool, direction, zend_hello_globals, hello_globals)
PHP_INI_END()
static void php_hello_init_globals(zend_hello_globals *hello_globals)
{
hello_globals->direction = 1;
}
PHP_RINIT_FUNCTION(hello)
{
HELLO_G(counter) = 0;
return SUCCESS;
}
PHP_MINIT_FUNCTION(hello)
{
ZEND_INIT_MODULE_GLOBALS(hello, php_hello_init_globals,
NULL);
REGISTER_INI_ENTRIES();
return SUCCESS;
}
PHP_MSHUTDOWN_FUNCTION(hello)
{
UNREGISTER_INI_ENTRIES();
return SUCCESS;
}
PHP_FUNCTION(hello_world)
{
RETURN_STRING("Hello World", 1);
}
PHP_FUNCTION(hello_long)
{
if (HELLO_G(direction)) {
HELLO_G(counter)++;
} else {
HELLO_G(counter)--;
}
RETURN_LONG(HELLO_G(counter));
}
PHP_FUNCTION(hello_double)
{
RETURN_DOUBLE(3.1415926535);
}
PHP_FUNCTION(hello_bool)
{
RETURN_BOOL(1);
}
PHP_FUNCTION(hello_null)
{
RETURN_NULL();
}
接下來做什麼?
在這教程中我們開發了一個簡單的PHP擴充,導出方法,傳回值,聲明了
INI
配置并且在一個請求中跟蹤他的核心狀态。
下一章我們研究PHP變量的核心結構,以及變量如何存儲、跟蹤和在腳本環境中修改。當一個函數被調用時我們使用
zend_parse_parameters
從程式擷取參數,接着學習傳回更複雜的結果的方法,包括本章提到的
array
,
object
, 和
resource
類型。
Copyright © Sara Golemon, 2005. All rights reserved.
Copyright © 肖, 2005. All rights reserved.