設定宿主環境
現在你已經了解了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替代:
這個Makefile和前面提供的命令有一些重要的區別. 首先, 它用-Wall開關打開了編譯期的警告, 並且用-g打開了調試信息. 此外它將編譯和鏈接兩個階段分為了兩個獨立的階段, 這樣在後期增加更多源文件的時候就相對容易. 請自己重新組著這個Makefile, 不過這裡用於對齊的是Tab(水平製表符)而不是空格.
現在,你對embed1.c源文件做修改後, 只需要執行一個make命令就可以構建出新的embed1可執行程序了.
現在通過嵌入包裝重新創建cli
現在通過嵌入包裝重新創建cli
現在通過嵌入包裝重新創建cli
現在通過嵌入包裝重新創建cli
你的應用中訪問了, 是時候讓它做一些事情了. 本章剩下的核心就是圍繞著在這個測試應用框架中重新創建cli sapi展開的.
很簡單, cli二進製程序最基礎的功能就是在命令列指定一個腳本的名字, 由php對其解釋執行. 用下面的程式碼替換你的embed1.c的內容就在你的應用中實現了cli.
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)
#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; }
$ ./embed1 test.php
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()
<?php var_dump($type); ?>
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()
int zend_alter_ini_entry(char *name, uint name_length, char *new_value, uint new_value_length, int modify_type, int stage);
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()
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()
第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扩展开发和嵌入式]第19章-设置宿主环境的内容,更多相关内容请关注PHP中文网(m.sbmmt.com)!