天天看點

[翻譯][php擴充開發和嵌入式]第19章-設定宿主環境

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

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

本書在github上的位址: https://github.com/goosman-lei/php-eae

未來本書将可能部分合并到phpbook項目中, 同時保留一份獨立版本.

原書名: <Extending and Embedding PHP>

原作者: Sara Golemon

譯者: goosman.lei(雷果國)

譯者Email: [email protected]

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

權利聲明

此譯本在不獲利的情況下, 可以無限制自由傳播.

設定宿主環境

現在你已經了解了PHPAPI的世界, 并可以使用zval以及語言内部擴充機制執行很多工作了, 是時候轉移目标用它做它最擅長的事情了: 解釋腳本代碼.

嵌入式SAPI

回顧介紹中, php建構了一個層級系統. 最高層是提供使用者空間函數和類庫的所有擴充. 同時, 其下是服務API(SAPI)層, 它扮演了webserver(比如apache, iis以及指令行接口cli)的接口.

在這許多sapi實作中有一個特殊的sapi就是嵌入式sapi. 當這個sapi實作被建構時, 将會建立一個包含所有你已知的php和zend api函數以及變量的庫對象, 這個庫對象還包含一些額外的幫助函數和宏, 用以簡化外部程式的調用.

生成嵌入式api的庫和頭檔案和其他sapi的編譯所執行的動作相同. 隻需要傳遞--enable-embed到./configure指令中即可. 和以前一樣, 使用--enable-debug對于錯誤報告和跟蹤很有幫助.

你可能還需要打開--enable-maintainer-zts, 當然, 理由你已經耳熟能詳了, 它将幫助你注意到代碼的錯誤, 不過, 這裡還有其他原因. 假設某個時刻, 你有多個應用使用php嵌入庫執行腳本任務; 其中一個應用是簡單的短生命周期的, 它并沒有使用線程, 是以為了效率你可能想要關閉ZTS.

現在假設第二個應用使用了線程, 比如webserver, 每個線程需要跟蹤自己的請求上下文. 如果ZTS被關閉, 則隻有第一個應用可以使用這個庫; 然而, 如果打開ZTS, 則兩個應用都可以在自己的程序空間使用同一個共享對象.

當然, 你也可以同時建構兩個版本, 并給它們不同的名字, 但是這相比于在不需要ZTS時包括ZTS帶來的很小的效率影響更多的問題.

預設情況下, 嵌入式庫将建構為libphp5.so共享對象, 或者在windows下的動态連結庫, 不過, 它也可能使用可選的static關鍵字(--enable-embed=static)被建構為靜态庫.

建構為靜态庫的版本避免了ZTS/非ZTS的問題, 以及潛在的可能在一個系統中有多個php版本的情況. 風險在于這就意味着你的結果應用二進制将顯著變大, 它将承載整個ZendEngine和PHP架構, 是以, 選擇的時候就需要慎重的考慮你是否需要的是一個相對更小的庫.

無論你選擇那種建構方式, 一旦你執行make install, libphp5都将被拷貝到你的./configure指定的PREFIX目錄下的lib/目錄中. 此外還會在PREFIX/include/php/sapi/embed目錄下放入名為php_embed.h的頭檔案, 以及你在使用php嵌入式庫編譯程式時需要的其他幾個重要的頭檔案.

建構并編譯一個宿主應用

究其本質而言, 庫隻是一個沒有目的的代碼集合. 為了讓它工作, 你需要用以嵌入php的應用. 首先, 我們來封裝一個非常簡單的應用, 它啟動Zend引擎并初始化PHP處理一個請求, 接着就回頭進行資源的清理.

#include <sapi/embed/php_embed.h>

int main(int argc, char *argv[])
{
    PHP_EMBED_START_BLOCK(argc,argv)
    PHP_EMBED_END_BLOCK()

    return 0;
}
           

由于這涉及到了很多頭檔案, 建構實際上需要的時間要長于這麼小的代碼片段通常需要的時間. 如果你使用了不同于預設路徑(/usr/local)的PREFIX, 請确認以下面的方式指定路徑:

gcc -I /usr/local/php-dev/include/php/ \
	-I /usr/local/php-dev/include/php/main/ \
	-I /usr/local/php-dev/include/php/Zend/ \
	-I /usr/local/php-dev/include/php/TSRM/ \
	-lphp5 \
	-o embed1 
	embed1.c
           

由于這個指令每次輸入都很麻煩, 你可能更原意用一個簡單的Makefile替代:

CC = gcc 
CFLAGS = -c \
    -I /usr/local/php-dev/include/php/ \
    -I /usr/local/php-dev/include/php/main/ \
    -I /usr/local/php-dev/include/php/Zend/ \
    -I /usr/local/php-dev/include/php/TSRM/ \
    -Wall -g
LDFLAGS = -lphp5

all: embed1.c
    $(CC) -o embed1.o embed1.c $(CFLAGS)
    $(CC) -o embed1 embed1.o $(LDFLAGS)
           

這個Makefile和前面提供的指令有一些重要的差別. 首先, 它用-Wall開關打開了編譯期的警告, 并且用-g打開了調試資訊. 此外它将編譯和連結兩個階段分為了兩個獨立的階段, 這樣在後期增加更多源檔案的時候就相對容易. 請自己重新組着這個Makefile, 不過這裡用于對齊的是Tab(水準制表符)而不是空格.

現在, 你對embed1.c源檔案做修改後, 隻需要執行一個make指令就可以建構出新的embed1可執行程式了.

通過嵌入包裝重新建立cli

現在php已經可以在你的應用中通路了, 是時候讓它做一些事情了. 本章剩下的核心就是圍繞着在這個測試應用架構中重新建立cli sapi展開的.

很簡單, cli二進制程式最基礎的功能就是在指令行指定一個腳本的名字, 由php對其解釋執行. 用下面的代碼替換你的embed1.c的内容就在你的應用中實作了cli.

#include <stdio.h>
#include <sapi/embed/php_embed.h>

int main(int argc, char *argv[]) {
    zend_file_handle    script;

    /* 基本的參數檢查 */
    if ( argc <= 1 ) {
        fprintf(stderr, "Usage: %s <filename.php> <arguments>\n", argv[0]);
        return -1;
    }
    
    /* 設定一個檔案處理結構 */
    script.type             = ZEND_HANDLE_FP;
    script.filename         = argv[1];
    script.opened_path      = NULL;
    script.free_filename    = 0;
    if ( !(script.handle.fp = fopen(script.filename, "rb")) ) {
        fprintf(stderr, "Unable to open: %s\n", argv[1]);
        return -1;
    }
    
    /* 在将指令行參數注冊給php時(php中的$argv/$argc), 忽略第一個指令行參數, 因為它對php腳本無意義 */
    argc --;
    argv ++;
    
    PHP_EMBED_START_BLOCK(argc, argv)
        php_execute_script(&script TSRMLS_CC);
    PHP_EMBED_END_BLOCK()
    
    return 0;
}
           

譯注: 原著中的代碼在譯者的環境不能直接運作, 上面的代碼是經過修改的.

當然, 你需要一個檔案測試它, 建立一個小的php腳本, 命名為test.php, 在指令行使用你的embed程式執行它:

$ ./embed1 test.php
           

如果你給指令行傳遞了其他參數, 你可以在你的php腳本中使用$_SERVER['argc']/$_SERVER['argv']看到它們.

你可能注意到了, 在PHP_EMBED_START_BLOCK()和PHP_EMBED_END_BLOCK()之間的代碼是縮進的. 這個細節是因為這兩個宏實際上構成了一個C語言的代碼塊作用域. 也就是說PHP_EMBED_START_BLOCK()包含一個打開的花括号"{", 在PHP_EMBED_END_BLOCK()中則有與之對應的關閉花括号"}". 這樣做非常重要的一個問題是它們不能被放入到獨立的啟動/終止函數中. 下一章你将看到這個問題的解決方案.

老技術新用

在PHP_EMBED_START_BLOCK()被調用後, 你的應用處于一個php請求周期的開始位置, 相當于RINIT回調函數完成以後. 此刻你就可以和前面一樣執行php_execute_script()指令, 或者其他任意合法的, 可以在PHP_FUNCTION()或RINIT()塊中出現的php/Zend API指令.

設定初始變量

第2章"變量的裡裡外外"中介紹了操縱符号表的概念, 第5至18章則介紹了怎樣通過使用者空間腳本調用内部函數使用這些技術. 到這裡這些處理也并沒有發生變化, 雖然這裡并沒有激活的使用者空間腳本, 但是你的包裝應用仍然可以操縱符号表. 将你的PHP_EMBED_START_BLOCK()/PHP_EMBED_END_BLOCK()代碼塊替換為下面的代碼:

PHP_EMBED_START_BLOCK(argc, argv)
        zval    *type;

        ALLOC_INIT_ZVAL(type);
        ZVAL_STRING(type, "Embedded", 1);
        ZEND_SET_SYMBOL(&EG(symbol_table), "type", type);
        
        php_execute_script(&script TSRMLS_CC);
    PHP_EMBED_END_BLOCK()
           

現在使用make重新建構embed1, 并用下面的測試腳本進行測試:

<?php
	var_dump($type);
?>
           

當然, 這個簡單的概念可以很容易的擴充為填充這個類型資訊到$_SERVER超級全局變量數組中.

PHP_EMBED_START_BLOCK(argc, argv)
        zval    **SERVER_PP, *type;

        /* 注冊$_SERVER超級全局變量 */
        zend_is_auto_global_quick("_SERVER", sizeof("_SERVER") - 1, 0 TSRMLS_CC);
        /* 查找$_SERVER超級全局變量 */
        zend_hash_find(&EG(symbol_table), "_SERVER", sizeof("_SERVER"), (void **)&SERVER_PP) ;

        /* $_SERVER['SAPI_TYPE'] = "Embedded"; */
        ALLOC_INIT_ZVAL(type);
        ZVAL_STRING(type, "Embedded", 1);
        ZEND_SET_SYMBOL(Z_ARRVAL_PP(SERVER_PP), "SAPI_TYPE", type);

        php_execute_script(&script TSRMLS_CC);
    PHP_EMBED_END_BLOCK()
           

譯注: 譯者的環境中代碼運作到zend_hash_find()處$_SERVER尚未注冊, 經過跟蹤, 發現它是直到編譯使用者空間代碼的時候, 發現使用者空間使用了$_SERVER變量才進行的注冊. 是以, 上面的代碼中增加了zend_is_auto_global_quick()的調用, 通過這個調用将完成對$_SERVER的注冊.

覆寫INI選項

在第13章"INI設定"中, 有一部分是講INI修改處理器的, 在那裡看到的是INI階段的處理. PHP_EMBED_START_BLOCK()宏則将這些代碼放到了運作時階段. 也就是說這個時候修改某些設定(比如register_globals/magic_quotes_gpc)已經有點遲了.

不過在内部通路也沒有什麼不好. 所謂的"管理設定"比如safe_mode在這個略遲的階段可以使用下面的zend_alter_ini_entry()指令打開或關閉:

int zend_alter_ini_entry(char *name, uint name_length,
                         char *new_value, uint new_value_length,
                         int modify_type, int stage);
           

name, new_value以及它們對應的長度參數的含義正如你所預期的: 修改名為name的INI設定的值為new_value. 要注意name_length包含了末尾的NULL位元組, 然而new_value_length則不包含; 然而, 無論如何, 兩個字元串都必須是NULL終止的.

modify_type則提供簡化的通路控制檢查. 回顧每個INI設定都有一個modifiable屬性, 它是PHP_INI_SYSTEM, PHP_INI_PERDIR, PHP_INI_USER等常量的組合值. 當使用zend_alter_ini_entry()修改INI設定時, modify_type參數必須包含至少一個INI設定的modifiable屬性值.

使用者空間的ini_set()函數通過傳遞PHP_INI_USER利用了這個特性, 也就是說隻有modifiable屬性包含PHP_INI_USER标記的INI設定才能使用這個函數修改. 當在你的嵌入式應用中使用這個API調用時, 你可以通過傳遞PHP_INI_ALL标記短路這個通路控制系統, 它将包含所有的INI通路級别.

stage必須對應于Zend Engine的目前狀态; 對于這些簡單的嵌入式示例, 總是PHP_INI_STAGE_RUNTIME. 如果這是一個擴充或更高端的嵌入式應用, 你可能就需要将這個值設定為PHP_INI_STAGE_STARTUP或PHP_INI_STAGE_ACTIVE.

下面是擴充embed1.c源檔案, 讓它在執行腳本檔案之前強制開啟safe_mode.

PHP_EMBED_START_BLOCK(argc, argv)
        zval    **SERVER_PP, *type;

        /* 不論php.ini中如何設定都強制開啟safe_mode */
        zend_alter_ini_entry("safe_mode", sizeof("safe_mode"), "1", sizeof("1") - 1, PHP_INI_ALL, PHP_INI_STAGE_RUNTIME);
        
        /* 注冊$_SERVER超級全局變量 */
        zend_is_auto_global_quick("_SERVER", sizeof("_SERVER") - 1, 0 TSRMLS_CC);
        /* 查找$_SERVER超級全局變量 */
        zend_hash_find(&EG(symbol_table), "_SERVER", sizeof("_SERVER"), (void **)&SERVER_PP) ;
        
        /* $_SERVER['SAPI_TYPE'] = "Embedded"; */
        ALLOC_INIT_ZVAL(type);
        ZVAL_STRING(type, "Embedded", 1);
        ZEND_SET_SYMBOL(Z_ARRVAL_PP(SERVER_PP), "SAPI_TYPE", type);
        
        php_execute_script(&script TSRMLS_CC);
    PHP_EMBED_END_BLOCK()
           

定義附加的超級全局變量

在第12章"啟動, 終止, 以及其中的一些點"中, 你知道了使用者空間全局變量以及超級全局變量可以在啟動(MINIT)階段定義. 同樣, 本章介紹的嵌入式直接跳過了啟動階段, 處于運作時狀态. 和覆寫INI一樣, 這并不會顯得太遲.

超級全局變量的定義實際上隻需要在腳本編譯之前定義即可, 并且在php的程序生命周期中它隻應該出現一次. 在擴充中的正常情況下, MINIT是唯一可以保證這些條件的地方.

由于你的包裝應用現在是在控制中的, 是以可以保證定義使用者空間自動全局變量的這些點位于真正編譯腳本源檔案的php_execute_script()指令之前. 我們定義一個$_EMBED超級全局變量并給它設定一個初始值來進行測試:

PHP_EMBED_START_BLOCK(argc, argv)
        zval    **SERVER_PP, *type, *EMBED, *foo;

        /* 在全局作用域建立$_EMBED數組 */
        ALLOC_INIT_ZVAL(EMBED);
        array_init(EMBED);
        ZEND_SET_SYMBOL(&EG(symbol_table), "_EMBED", EMBED);

        /* $_EMBED['foo'] = 'Bar'; */
        ALLOC_INIT_ZVAL(foo);
        ZVAL_STRING(foo, "Bar", 1); 
        add_assoc_zval_ex(EMBED, "foo", sizeof("foo"), foo);

        /* 注冊超級全局變量$_EMBED */
        zend_register_auto_global("_EMBED", sizeof("_EMBED")
#ifdef ZEND_ENGINE_2
            , 1, NULL TSRMLS_CC);
#else
            , 1 TSRMLS_CC);
#endif

        /* 不論php.ini中如何設定都強制開啟safe_mode */
        zend_alter_ini_entry("safe_mode", sizeof("safe_mode"), "1", sizeof("1") - 1, PHP_INI_ALL, PHP_INI_STAGE_RUNTIME);

        /* 注冊$_SERVER超級全局變量 */
        zend_is_auto_global_quick("_SERVER", sizeof("_SERVER") - 1, 0 TSRMLS_CC);
        /* 查找$_SERVER超級全局變量 */
        zend_hash_find(&EG(symbol_table), "_SERVER", sizeof("_SERVER"), (void **)&SERVER_PP) ;

        /* $_SERVER['SAPI_TYPE'] = "Embedded"; */
        ALLOC_INIT_ZVAL(type);
        ZVAL_STRING(type, "Embedded", 1); 
        ZEND_SET_SYMBOL(Z_ARRVAL_PP(SERVER_PP), "SAPI_TYPE", type);

        php_execute_script(&script TSRMLS_CC);
    PHP_EMBED_END_BLOCK()
           

要記住, Zend Engine 2(php 5.0或更高)使用了不同的zend_register_auto_global()元嬰, 是以你需要用前面講php 4相容時候講過的#ifdef. 如果你不關心舊版本php的相容性, 則可以丢棄這些指令讓代碼變得更加整潔.

小結

如你所見, 将完整的Zend Engine和PHP語言嵌入到你的應用中相比如擴充新功能來說工作量要少. 由于它們共享相同的基礎API, 我們可以學習嘗試讓其他執行個體可通路.

通過本章的學習, 你了解了最簡單的嵌入式腳本代碼格式, 同時還有all-in-one的宏PHP_EBED_START_BLOCK()和PHP_EMBED_END_BLOCK(). 下一章你将回到這些宏的層的使用, 利用它們将php和你的宿主系統結合起來.

目錄 上一章: php擴充的自動生成 下一章: php中的進階嵌入式