译:我在江湖丢了
原文地址: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 |
---|---|---|
*
**
构建一个开发环境
现在我们已经学习了PHP和Zend引擎工作方式背后的一些理论,我猜你想运足功力开始开发了。然后在你开始前,必须得收集一些必备的工作来搭建一个满足你需要的开发环境。
首先,你需要PHP本身,因为这一系列开发工具都离不开PHP。如果你还对使用源码搭建PHP不熟悉,推荐你看下这篇文章先:http://www.php.net/install.unix.
(使用Windows开发PHP扩展的文章稍后给出)。虽然用你的linux发行版里的二进制包安全是非常诱人的,但是这些会遗漏两项在开发过程中十分方便的./configure选项。第一个就是
Hello World
任何没有完成Hello World应用的程序介绍都是不完整的。因此,下面将制作一个只有一个返回字符串"Hello World"的函数的扩展。在PHP代码里你可能这样写:
<?php function hello_word(){ return 'Hello World'; } ?>
现在我们使用PHP扩展来实现这段代码,首先我们在PHP源码的ext/目录下创建目录hello并且进到该目录下。事实上该目录在不在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 #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、声明一个方法
2、让这个方法返回一个字符串:"Hello World"
3、。。呃。。。1?这个1几个意思?
回忆一下,ZE包含了一套复杂的内存管理层,能确保分配的资源在脚本退出时被释放。然而在内存管理的掌控下,释放同样的块两次是非常大的禁忌。这种行为通常称为"double freeing",是一个常见的段错误的原因,涉及到一个正在调用的程序访问一个不再属于它的内存块。同样的,你不希望允许ZE去释放一个存活于程序空间并且其数据块被其他进程占用的静态字符串buffer(例如我们示例中的"Hello World")。
PHP_FUNCTION(hello_world) { char *str; str = estrup("hello World"); RETURN_STRING(str, 0); }
在此版本中,我们手动地分配内存给"Hello World"这个串,并且最终传回调用脚本,然后把内存传给
编译你的扩展
本练习的最后一步就是将你的扩展编译为一个动态可加载模块。如果你把上面的示例原封不动的抄下来,那么只需要在ext/hello/目录下运行下面三步命令就行:
phpize ./configure --enable-hello (译者注:如果编译PHP的时候使用了 --prefix 参数,此处要加上 --with-php-config 选项, 如笔者编译PHP时使用的是 ./configure --prefix=/use/local/phpdev/ 此处命令应使用 ./configure --enable-hello --with-php-c/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命令会原封不动地显示传递给他的参数(此处即为该函数的返回值)。
其他标量也可用类似的方式返回,使用
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里
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解析器,运行一段简单的脚本来试试你刚刚添加的方法。事实上,为何不现在不试呢?我等着呢。。。
试完了?很好。如果你使用
PHP_FUNCTION(hello_bool){ RETURN_TRUE; }
注意这里没有使用括号哦。
你也许注意到上面这些代码样品我们都没有传0和1什来表示这些值是否需要被拷贝。这是因为没有额外的内存(变量容器之外——我们将在第2部分深入)需要被分配或释放,因为这些标量都很简单很小。
还有另外三种返回类型:
INI Settings
Zend 引擎提供了两个方式处理
现在我们想在php.ini中定义一个变量,"hello.greeting", 用来处理你在"hello_function()"函数里用来打招呼的变量。你需要在hello.c和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中用下面这串代码覆盖当前版本的
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顶部剩下的
#ifdef HAVE_CONFIG_H #include "config.h" #endif #include "php.h" #include "php_ini.h "#include "php_hello.h"
最后,我们修改
PHP_FUNCTION(hello_world) { RETURN_STRING(INI_STR("hello.greeting"), 1); }
注意,你复制了来自
首次修改的部分包含了两个你需要熟悉的方法:
在你的方法hellow_world()中使用"INI_STR()"来取回当前"hello.greeting"项的值作为一个字符串。那些其他的已有方法取值作为长整型,浮点型和布尔型,如下表所示,"ORIG"补充了其他方法,能提供从
Current Value | Original Value | Type |
signed long | ||
signed double | ||
传入
第二个参数是初始值,并且通常是作为char*串不管是不是数字。这主要是因为实际上.ini文件里的值默认是文本——那是一个文本文件。你可以在你的脚本中使用
传入的第三个参数是一个访问模式修饰符。这是一个用于决定
我们就此跳过第四个参数,只点出该值用来传入一个回调方法便以ini配置无论在何时被修改时调用,比如当使用
全局变量
通常,扩展需要在特定的请求里跟踪变量的值,使之独立于并发请求。在无线程的SAPI中这可能比较简单:只需要在源文件中声明一个全局变量,在需要时调用。然而麻烦在于PHP被设计运行于线程级的web服务器(如Apache 2 和 IIS),这就需要保证一个线程中的全局变量与其他线程中的分离。PHP通过使用TSRM(Thread Safe Resource Management)抽象层,大大地简化了这一操作,有时被称为ZTS(Zend Thread Safety)。实际 上,到现在你已经使用了一部分TSRM了,虽然你对其不甚了解。(不要急着去搜索,通过这一系列的进展,你会发现它无处不在。)
创建线程安全全局变量的第一步,跟创建其他全局变量一样,声明它。为了实现这个例子,你将声明一个以
#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
这次你还将用到
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)
修改
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 };
修改你的
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; }
最后,修改你的
PHP_FUNCTION(hello_long) { HELLO_G(counter)++; RETURN_LONG(HELLO_G(counter)); }
在添加到php_hello.h的代码里,使用了一对宏
INI设置 vs 全局变量
如果你回观前文,一个在
ZEND_BEGIN_MODULE_GLOBAL(hello) login counter; zend_bool direction; ZEND_ENG_MODULE_GLOBALS(hello)
然后,通过修改
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; }
最后,在hello_long()中使用这个配置的值来决定是自增还是自减:
PHP_FUNCTION(hello_long) { if (HELLO_G(direction)) { HELLO_G(counter)++; } else { HELLO_G(counter)--; } RETURN_LONG(HELLO_G(counter)); }
就是这样,我们在
完整性检查
到现在,我们的三个文件里的内容应该如下所列的(为了可阅读性,一些条目被移到了一起)。
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扩展,导出方法,返回值,声明了
下一章我们研究PHP变量的内核结构,以及变量如何存储、跟踪和在脚本环境中修改。当一个函数被调用时我们使用