PHP


目前在 PHP 社区尤其是国内的 PHP 社区对 PHP 内核这方面讨论的比较少。我平时常看一些 PHP 源码,对 PHP 的运行机制算是有一些认识吧,因此我打算写一些这方面的文章,算是抛砖引玉。最终目标我希望可以做成一个关于 PHP 内核的中文百科全书。应该说这是一个不算太小的工程,依我个人之力几乎不可能完成。更何况老子曾经曰过:“知者不言,言者不知”,相信潜在水下面的大牛(水牛?^_^)还有 N 多。所以希望大家群策群力,共同完成这个项目。

整个项目我初步打算以 PHP 5.2.x 的源码为研究对象,侧重于对 Zend Engine 的表述,兼顾一些 SAPI 层。下面是我列出的一份清单,大家看看还有没有什么遗漏或者内容编排不合理之处。请诸位畅所欲言,有啥说啥,即使跑题也没关系~ 😀

    PHP 源代码分析 V0.0.1

    第一章 构建系统

  1. 准备工具、库及需要具备的基础知识;
  2. 如何编译不同平台的版本?编译时的各个选项是什么含义?源码的目录结构。
  3. 如何创建一个 PHP 扩展/模块?如何创建一个 Zend 扩展?
  4. 如何调试 PHP?如何调试 PHP/Zend 扩展?
    第二章 PHP 与 SAPI 的生命周期

  1. 脚本的运行周期,一切从 main() 开始;
  2. 模块/脚本的起始与终止函数;
  3. PHP SAPI 协议;
  4. 嵌入式 PHP 设计。
    第三章 内存管理

  1. Zend 的内存管理器框架;
  2. 内存申请与释放流程,垃圾回收;
  3. 持久化(persistence)
    第四章 线程安全

  1. 为何会有这个问题?Zend Engine 是如何解决的?
  2. 我是否该启用 ZTS?各有什么优缺点?
  3. 如何构建一个 ZTS 的程序/扩展?
    第五章 变量与常量

  1. PHP 中的数据类型。
  2. 变量、常量与静态变量。
  3. 引用计数机制;
  4. 资源的创建与回收;
  5. 未来字符串的 UNICODE 支持、JIT 支持。
    第六章 函数

  1. 函数的内部布局;
  2. 函数的定义;
  3. 如何获取函数的参数、可选参数、参数默认值;
  4. 函数的返回值;
    第七章 类与对象

  1. 类的内部布局(属性、方法);
  2. 构造函数与析构函数;
  3. 类的继承与转换(up casting 与 down casting);
  4. 接口(轻量级的类),微观上与类的差别;
  5. 类之间的 up casting 和 down casting。
  6. stdClass。
    第八章 错误与异常

  1. 什么是错误、什么是异常。两者的区别;
  2. 如何创建和抛出异常;
  3. try/catch 的设计与实现;
    第九章 流(Streams)支持

  1. 这方面我接触较少,内容待定;
    第十章 虚拟机

  1. 脚本编译机制(词法分析、语法分析);
  2. 脚本的执行机制(CALL|SWITCH|GOTO);
  3. 各个符号表的作用;
  4. 开发 OPCode 缓存器;
  5. 开发 PHP 调试器;
    附录

  1. 完整的 PHP API、Zend API 以及宏(Micro)参考(长期工程)
  2. Zend Engine 1 的主要特性,与 Zend Engine 2的主要差别;
  3. Zend Engine 3 的主要特性,与 Zend Engine 2的主要差别;
  4. 相关资源
  • 项目发起:Ben (ben.yan at msn dot com
  • 项目参与:Ben (ben.yan at msn dot com,http://www.yAnbiN.org
  • 项目启动:2007/06/09 (希望可以在明年奥运会开幕前完成 :D)
  • 项目进度:
    1. 2007/06/09 项目启动,讨论项目规划;
    2. 2007/xx/xx 待续……

以前每当一个 Zend Studio 的新版本发布时都会同时发布一个新版的 Zend Studio Server 组件,这个组件可以让我们很方便地进行远程调试。但是自从 Zend 发布了 Zend Platform 以后他们就不再更新 Zend Studio Server 组件了。这就导致我们只能远程调试 PHP 5.1.x 的环境,而不能调试 PHP 5.2.x。要想调试 PHP 5.2.x 只能装一个庞大的 Zend Platform。:(

因此我一直在找一个“轻量级”的解决方案。近日在逛 Zend.com 时发现了一个好东西:Zend Studio Web Debugger,直觉告诉我,这就是我想要的。果不其然,今天试验成功!

下面就说一说我的试验步骤:

  1. 到这里下载 Zend Studio Web Debugger,然后将其解压到某一目录,比如:C:\Program Files\Zend,这就会在该目录里面新建一个 ZendDebugger-5.2.14-Windows-i386 子目录,里面有 4_3_x_comp、4_4_x_comp、5_2_x_comp 等目录,将这些 x_y_z_comp 分别改为 php-x.y.z(比如将目录 5_2_x_comp 改为 php-5.2.x);
  2. 确保已经加载了 Zend Extension Manager,如果安装了 Zend Optimizer 则会自动安装 Zend Extension Manager,若没有安装请先安装 Zend Optimizer 。或者你可以把 Zend Optimizer 中 Zend Extension Manager.dll 给提取出来,然后手工在 php.ini 中添加一行:
    zend_extension_ts="C:\Program Files\Zend\ZendOptimizer\ZendExtensionManager.dll"
    其中 ZendExtensionManager.dll 的位置请根据你的实际情况填写;
  3. 在 Web Server 的 php.ini 添加下面几行:
    zend_extension_manager.debug_server_ts="C:\Program Files\Zend\ZendDebugger-5.2.14-Windows-i386"
    zend_debugger.expose_remotely=allowed_hosts
    zend_debugger.allow_hosts=127.0.0.1/32,192.168.1.0/16
    zend_debugger.allow_tunnel=127.0.0.1/32

    zend_extension_manager.debug_server_ts 的值请根据你的实际情况填写,就是 php-x.y.z 的父目录。
  4. 把 ZendDebugger-5.2.14-Windows-i386 目录下的 dummy.php 复制到你的 Web 站点根目录。
  5. 重启你的 Web Server,OK!

简单总结一下:Zend Studio 的远程调试功能是由 Zend Studio Server 组件(ZendDebuger.dll)提供的。本质上这是一个 Zend 扩展,因此你只要能把这个 Zend 扩展启用就可以了。只是 Zend 公司出品的 Zend 扩展只能由那个 Zend Extension Manager 负责加载,所以我们才需要做一些额外的步骤,否则只需简单地加一行 zend_extension_ts = xxxxxx 而已。

Zend Extension Manager 是一个 Zend 公司用于统一管理该公司出品的各种 Zend 扩展的 Zend 扩展。一般来说 Zend 扩展都是高度依赖 Zend Engine 版本的,但是 Zend Extension Manager 却可以不依赖任何具体版本的 PHP 运行库,并且会根据不同的运行环境自动加载不同产品相应版本的 Zend 扩展。相信通过学习 Zend Extension Manager 的实现会对我们统一开发部署 Zend 扩展提供一些帮助。

附件就是 Zend Extension Manager v1.2.0 版本的源代码和 VC++ 工程文件。源代码是根据 ZendOptimizer-3.2.8-Windows-i386 中的 ZendExtensionManager.dll 逆向出来的。编译出来的 ZendExtensionManager.dll 可完全替代原始文件(事实上也没有任何区别)。

点击下载 Zend Extension Manager v1.2.0 源代码

php|tek 是由《php|architect》杂志主办的重量级 PHP 专业盛会。虽然不能到场亲自聆听大牛们的演讲(事实上像我这种鸟语菜菜到了也不一定能听得懂。:(),但看看他们演讲时提纲挈领的幻灯片也是可以管窥到很多有价值的信息的。下面是我收集到的大会上这些大牛们的幻灯片,供感兴趣的朋友参考:

演讲人 演讲主题
Aaron Wormus Moving to PHP5 with Style
Brian Shire APC @ Facebook
Caroline Maynard Services made simple with PHP
Chris Hartjes What Can PHP Learn From Ruby on Rails?
Derick Rethans Help, I Found a Bug in my Code!
Derick Rethans Exposing Hidden PHP Secrets
Ilia Alshanetsky High Performance PHP
Ilia Alshanetsky Securing PHP Applications
Ilia Alshanetsky PHP Security Pitfalls
Jason Sweat Test Driven Development
Jason Sweat Design Patterns
Jay Pipes Top 15 Ways to Kill MySQL Performance
Jeff Griffiths PHP, Remote XUL and jQuery
Jeff Moore Writing Maintainable PHP Code
Jeff Moore Dependency Injection in PHP
Jeff Moore Exceptional PHP
Marcus Boerger Introducing Phar
Marcus Boerger The Standard PHP Library
Paul Reinheimer Zend PHP 5 Certification Crash Course
Rasmus Lerdorf PHP on Hormones
Sara Golemon PHP Extension Writing
Sebastian Bergmann Testing PHP/Web Applications with PHPUnit 3 and Selenium

《Zend API,深入 PHP 内核》一章的初译暂时算是告一段落了。整个翻译进度前快后慢,这主要是和我本人的水平有很大关系的,同时也略微夹杂着一丝“翻译疲劳”。不过翻译的过程也是学习的过程。有很多地方都是平时囫囵吞枣,得过且过的,但这次为了翻译出去不误人子弟,被迫将所有的知识点都串联理顺了一下,自己感觉也有了很大的提高。

以后半个月或一个月我会再对初译稿进行一些审定,有些术语也尽量统一起来。整个翻译的行文现在看起来还是有点晦涩,还没有摆脱技术译稿的感觉,所以在这一点也须做些润色。在复审之后我会尝试将这些翻译提交到 PHP 手册的中文文档组,以便让更多的 PHPer 在做这方面的工作时有一点点中文参考。

同时在翻译的过程中也有了一些自己的看法,有机会再另行一些文章和大家交流。

下面(见表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

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 应为 yesno
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> 的定义。

现在你已经掌握了很多关于 PHP 的知识了。你已经知道了如何创建一个动态加载的模块或被静态连接的扩展。你还知道了在 PHP 和 Zend 的内部变量是如何储存的,以及如何创建和访问这些变量。另外你也知道了很多诸如输出信息文本、自动将变量引入符号表等一系列工具函数的应用。

尽管这一章常常有点“参考”的意味,但我们还是希望它能给你一些关于如何开始编写自己的扩展这方面的知识。限于篇幅,我们不得不省略了很多东西。我们建议你花些时间仔细研究一下它的头文件和一些模块(尤其是 ext/standard 目录下的一些文件以及 MySQL 模块,看一下这些众所周知的函数究竟是怎么实现的),看一下别人是怎么使用这些 API 函数的,尤其是那些本章没有提到的那些函数。

PHP4 重写了对初始化文件的支持。现在你可以直接在代码中指定一些初始化选项,然后在运行时读取和改变这些选项值,甚至还可以在选项值改变时接到相关通知。

如果想要为你的模块创建一个 .ini 文件的配置节,可以使用宏 PHP_INI_BEGIN() 来标识这个节的开始,并用 PHP_INI_END() 表示该配置节已经结束。然后在两者之间我们用 PHP_INI_ENTRY() 来创建具体的配置项。

PHP_INI_BEGIN()
PHP_INI_ENTRY("first_ini_entry",  "has_string_value", PHP_INI_ALL, NULL)
PHP_INI_ENTRY("second_ini_entry", "2",                PHP_INI_SYSTEM, OnChangeSecond)
PHP_INI_ENTRY("third_ini_entry",  "xyz",              PHP_INI_USER, NULL)
PHP_INI_END() 

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 来定义:

PHP_INI_MH(OnChangeSecond);             // handler for ini-entry "second_ini_entry"

// specify ini-entries here
PHP_INI_MH(OnChangeSecond)
{
    zend_printf("Message caught, our ini entry has been changed to %s<br>", new_value);
    return(SUCCESS);
}

改变后的新值将会以字符串的形式并通过一个名为 new_value 变量传递给函数。要是再注意一下 PHP_INI_MH 的定义就会发现,我们实际上用到了不少参数:

#define PHP_INI_MH(name) int name(
    php_ini_entry *entry,
    char *new_value,
    uint new_value_length,
    void *mh_arg1,
    void *mh_arg2,
    void *mh_arg3
)

这些定义都可以在 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 还允许你在你的模块里面调用一些一些用户定义的函数,这样在实现某些回调机制(比如在做一些数组的轮循(array walking)、搜索或设计一些简单的事件驱动的程序时)时会很方便。

我们可以通过调用 call_user_function_ex() 来调用用户函数。它需要你即将访问函数表的指针、这个对象的指针(假如你访问的是类的一个方法的话),函数名、返回值、参数个数、具体的参数数组和一个是否需要进行 zval 分离的标识(这个函数原型已经“过时”了,至少是从 PHP 4.2 开始这个函数就追加了一个 HashTable *symbol_table 参数。下面所列举的函数原型更像是 call_user_function () 的声明。译注)。

ZEND_API int call_user_function_ex(
    HashTable *function_table,
    zval *object,
    zval *function_name,
    zval **retval_ptr_ptr,
    int param_count,
    zval **params[],
    int no_separation
);

需要注意的是你不必同时指定 function_tableobject 这两个参数,只需要指定其中一个就行了。不过如果你想调用一个方法的话,那你就必须提供一个包含此方法的对象。这时 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 调用用户函数

zval **function_name;
zval *retval;

if((ZEND_NUM_ARGS() != 1) || (zend_get_parameters_ex(1, &function_name) != SUCCESS))
{
   WRONG_PARAM_COUNT;
}
if((*function_name)->type != IS_STRING)
{
   zend_error(E_ERROR, "Function requires string argument");
}

TSRMSLS_FETCH();
if(call_user_function_ex(CG(function_table), NULL, *function_name, &retval, 0, NULL, 0) != SUCCESS)
{
   zend_error(E_ERROR, "Function call failed");
}
zend_printf("We have %i as type\n", retval->type);

*return_value = *retval;
zval_copy_ctor(return_value);
zval_ptr_dtor(&retval);

调用脚本:

dl("call_userland.so");
function test_function()
{
    echo "We are in the test function!\n";
    return 'hello';
}
$return_value = call_userland("test_function");
echo "Return value: '$return_value'";

上例将输出:

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

« Previous PageNext Page »

'