PHP 扩展开发学习笔记(1)- 生命周期与PHP变量

源码结构

PHP的源码在结构上非常清晰。下面是PHP源码的目录结构。

根目录: 
/       这个目录包含的东西比较多,主要包含一些说明文件以及设计方案。 
build/  顾名思义,这里主要放置一些和源码编译相关的一些文件,比如开始构建之前的buildconf脚本等文件,还有一些检查环境的脚本等。
ext/    官方扩展目录,包括了绝大多数PHP的函数的定义和实现,如array系列,pdo系列,spl系列等函数的实现,都在这个目录中。个人写的扩展在测试时也可以放到这个目录,方便测试和调试。
main/   这里存放的就是PHP最为核心的文件了,主要实现PHP的基本设施,这里和Zend引擎不一样,Zend引擎主要实现语言最核心的语言运行环境。
Zend/   Zend引擎的实现目录,比如脚本的词法语法解析,opcode的执行以及扩展机制的实现等等。
pear/   “PHP 扩展与应用仓库”,包含PEAR的核心文件。
sapi/   包含了各种服务器抽象层的代码,例如apache的mod_php,cgi,fastcgi以及fpm等等接口。
TSRM/   PHP的线程安全是构建在TSRM库之上的,PHP实现中常见的*G宏通常是对TSRM的封装,TSRM(Thread Safe Resource Manager)线程安全资源管理器。
tests/  PHP的测试脚本集合,包含PHP各项功能的测试文件
win32/  这个目录主要包括Windows平台相关的一些实现,比如sokcet的实现在Windows下和*Nix平台就不太一样,同时也包括了Windows下编译PHP相关的脚本。

PHP 生命周期

PHP开始执行以后会经过两个主要的阶段:处理请求之前的开始阶段和请求之后的结束阶段。

开始阶段有两个过程:第一个过程是模块初始化阶段(MINIT), 在整个SAPI生命周期内(例如Apache启动以后的整个生命周期内或者命令行程序整个执行过程中), 该过程只进行一次。

第二个过程是模块激活阶段(RINIT),该过程发生在请求阶段, 例如通过url请求某个页面,则在每次请求之前都会进行模块激活(RINIT请求开始)。

例如PHP注册了一些扩展模块,则在MINIT阶段会回调所有模块的MINIT函数。 模块在这个阶段可以进行一些初始化工作,例如注册常量,定义模块使用的类等等。 模块在实现时可以通过如下宏来实现这些回调函数:

PHP_MINIT_FUNCTION(myphpextension)
{
    // 注册常量或者类等初始化操作
    return SUCCESS; 
}

请求到达之后PHP初始化执行脚本的基本环境,例如创建一个执行环境,包括保存PHP运行过程中变量名称和值内容的符号表, 以及当前所有的函数以及类等信息的符号表。

然后PHP会调用所有模块的RINIT函数, 在这个阶段各个模块也可以执行一些相关的操作:

PHP_RINIT_FUNCTION(myphpextension)
{
    // 例如记录请求开始时间
    // 随后在请求结束的时候记录结束时间。这样我们就能够记录下处理请求所花费的时间了
    return SUCCESS; 
}

请求处理完后就进入了结束阶段,一般脚本执行到末尾或者通过调用exit()或die()函数, PHP都将进入结束阶段。和开始阶段对应,结束阶段也分为两个环节:

一个在请求结束后停用模块(RSHUTDOWN,对应RINIT), 一个在SAPI生命周期结束(Web服务器退出或者命令行脚本执行完毕退出)时关闭模块(MSHUTDOWN,对应MINIT)。

变量类型

PHP在内核中是通过zval这个结构体来存储变量的,它的定义在Zend/zend.h文件里,简短精炼,只有四个成员组成

struct _zval_struct {
    /* Variable information */
    zvalue_value value;        /* value */
    zend_uint refcount__gc;
    zend_uchar type;    /* active type */
    zend_uchar is_ref__gc;
};
typedef struct _zval_struct zval;

其中 zend_uint 与 zend_uchar 是在Zend/zend_types.h中定义的:

typedef unsigned char zend_uchar;
typedef unsigned int zend_uint;

保存变量值的value则是zvalue_value类型(PHP5),它是一个union,同样定义在了Zend/zend.h文件里:

typedef union _zvalue_value {
    long lval;                    /* long value */
    double dval;                /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;                /* hash table value */
    zend_object_value obj;
} zvalue_value;

在以上实现的基础上,PHP语言得以实现了8种数据类型,这些数据类型在内核中的分别对应于特定的常量,它们分别是:

IS_NULL
IS_BOOL
IS_LONG singed long 类型 所以PHP语言中的整形都是带符号的
IS_DOUBLE
IS_STRING zval中保存了指向这块内存的指针,同时记录了字符串的实际长度
IS_ARRAY C语言的数组只能承载一种类型的数据,PHP数组借助HashTable可以承载任意类型的数据 每个元素都有两个部分组成,索引与值
IS_OBJECT 
IS_RESOURCE

zval结构体中的type成员值便是上面IS_*常量之一

PHP源码中通过三个宏来验证类型

#define Z_TYPE(zval)        (zval).type
#define Z_TYPE_P(zval_p)    Z_TYPE(*zval_p)
#define Z_TYPE_PP(zval_pp)  Z_TYPE(**zval_pp)

比如:

zval *foo
Z_TYPE_P(foo) == IS_NULL

除此之外每个类型都有特定的宏来提供操作,比如获取字符串的值、长度,获取数组的值等

创建PHP变量

PHP提供一个创建PHP变量的宏:MAKE_STD_ZVAL(pzv)。

这个宏会用内核的方式来申请一块内存并将其地址付给pzv, 并初始化它的refcount和is_ref两个属性,更棒的是,它不但会自动的处理内存不足问题, 还会在内存中选个最优的位置来申请。

先看一下PHP的符号表,Zend/zend_globals.h,其中有两个成员:

struct _zend_executor_globals {
    ...
    HashTable *active_symbol_table;
    HashTable symbol_table;     /* main symbol table */
    ...
};

symbol_table代表着PHP的全局变量,如$GLOBALS,
active_symbol_table它代表的是处于当前作用域的变量符号表
他们都可以使用EG宏来访问

下面是个创建PHP变量的例子

<?php
    $foo = 'bar';
?>

而在源码中,我们需要

{
    zval *fooval;

    MAKE_STD_ZVAL(fooval);
    ZVAL_STRING(fooval, "bar", 1);
    ZEND_SET_SYMBOL( EG(active_symbol_table) ,  "foo" , fooval);
}

首先,我们声明一个zval指针,并申请一块内存。然后通过ZVAL_STRING宏将值设置为‘bar’。
最后一行的作用就是将这个zval加入到当前的符号表里去,并将其label定义成foo,这样用户就可以在代码里通过$foo来使用它了

除此之外,内核中提供一些宏来简化我们的操作,可以只用一步便设置好zval的类型和值,如:ZVAL_LONG、ZVAL_DOUBLE 等

编写第一个扩展

使用ext_skel 生成器

使用PHP源码中的内置工具ext_skel 可以生成一个扩展模板

cd php-5.4.41/ext
./ext_skel --extname=hello

ncqdeMacBook-Pro:ext ncq$ cd hello/
ncqdeMacBook-Pro:hello ncq$ ll
total 56
-rw-r--r--  1 ncq  staff     6 10 24 22:15 CREDITS
-rw-r--r--  1 ncq  staff     0 10 24 22:15 EXPERIMENTAL
-rw-r--r--  1 ncq  staff  2002 10 24 22:15 config.m4
-rw-r--r--  1 ncq  staff   289 10 24 22:15 config.w32
-rw-r--r--  1 ncq  staff  5086 10 24 22:15 hello.c
-rw-r--r--  1 ncq  staff   499 10 24 22:15 hello.php
-rw-r--r--  1 ncq  staff  2837 10 24 22:15 php_hello.h
drwxr-xr-x  3 ncq  staff   102 10 24 22:15 tests

我们看一下config.m4文件的重要部分,为了顺利编译生成扩展,我们需要去掉模板文件前面相关的dnl注释:

PHP_ARG_ENABLE(hello, whether to enable hello support,
[  --enable-hello           Enable hello support])

if test "$PHP_HELLO" != "no"; then
    PHP_SUBST(HELLO_SHARED_LIBADD)
    PHP_NEW_EXTENSION(hello, hello.c, $ext_shared)
fi

PHP中约定--enable-extname一般用于直接可编译的扩展,--with-extname大多需要依赖第三方的lib

如果我们在编译PHP的时候显式运行./configure --enable-hello,那么终端环境便会自动将$PHP_HELLO变量设置为yes,而PHP_SUBST函数只不过是php官方对autoconf里的AC_SUBST函数的一层封装。 最后重要的一点是,PHP_NEW_EXTENSION函数声明了这个扩展的名称、需要的源文件名、此扩展的编译形式。如果我们的扩展使用了多个文件,便可以将这多个文件名罗列在函数的参数里。

$ext_shared参数用来声明这个扩展不是一个静态模块,而是在php运行时动态加载的。

PHP_NEW_EXTENSION(sample, sample.c sample2.c sample3.c, $ext_shared)

编写第一个函数

我们看一下自动生成的 hello.c文件

PHP_FUNCTION()可以写成ZEND_FUNCTION(),ZEND_FUNCTION()更前卫、标准一些,两者是完全相同的。

/* 生成PHPINFO内容 */
PHP_MINFO_FUNCTION(hello)
{

}

/* 定义可供PHP调用的函数 */
PHP_FUNCTION(confirm_hello_compiled)
{

}

我们可以看一下PHP_FUNCTION和ZEND_FUNCTION会被展开成什么:

#define PHP_FUNCTION                ZEND_FUNCTION
#define ZEND_FUNCTION(name)         ZEND_NAMED_FUNCTION(ZEND_FN(name))
#define ZEND_NAMED_FUNCTION(name)   void name(INTERNAL_FUNCTION_PARAMETERS)
#define ZEND_FN(name)               zif_##name

通过以上宏定义,我们可以看出,最终被展开成:
void zif_confirm_hello_compiled(INTERNAL_FUNCTION_PARAMETERS) {}
其中zif是zend internal function,也就是Zend内部函数,是PHP语言调用的函数在C语言中函数名称的前缀。
INTERNAL_FUNCTION_PARAMETERS 是一个很长的参数表。

zend_module_entry hello_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
    STANDARD_MODULE_HEADER,
#endif
    "hello",
    hello_functions,    
    PHP_MINIT(hello),
    PHP_MSHUTDOWN(hello),
    PHP_RINIT(hello),       /* Replace with NULL if there's nothing to do at request start */
    PHP_RSHUTDOWN(hello),   /* Replace with NULL if there's nothing to do at request end */
    PHP_MINFO(hello),
#if ZEND_MODULE_API_NO >= 20010901
    PHP_HELLO_VERSION,
#endif
    STANDARD_MODULE_PROPERTIES
};

zend_module_entry hello_module_entry 是联系C扩展与PHP语言的重要纽带,其中第二个参数是zend_function_entry[]类型,所以我们可以看到该定义:

const zend_function_entry hello_functions[] = {
    PHP_FE(confirm_hello_compiled,  NULL)       /* For testing, remove later. */
    PHP_FE_END  /* Must be the last line in hello_functions[] */
};

PHP_FE/ZEND_FE()宏函数是对我们walu_hello函数的一个声明,如果我们有多个函数可以直接写成多行,PHP_FE_END/ZEND_FE_END 是个固定的 { NULL, NULL, NULL, 0, 0 } 值,注意每个之间不需要加逗号。