1.为什么要开发 PHP 扩展
1.注重效率
2.有些系统调用不能用 PHP 直接访问的
3.不想暴露源码
2.windows 下
php ext_skel_win32.php --extname=myext
3.Linux 下
1.安装 php-dev 包
使用 php-dev 包中的 phpize 工具可以减少很多繁琐的步骤。如果使用 php 源码编译的话,就不用按照 php-dev 包,因为源码中已经
有 phpize 工具。
apt-get install php5-dev
phpize -version
2.使用 ext_skel 工具
./ext_skel --extname=myext
To use your new extension, you will have to execute the following steps:
1. $ cd ..
2. $ vi ext/myext/config.m4
3. $ ./buildconf
4. $ ./configure --[with|enable]-myext
5. $ make
6. $ ./sapi/cli/php -f ext/myext/myext.php
7. $ vi ext/myext/myext.c
8. $ make
Repeat steps 3-6 until you are satisfied with ext/myext/config.m4 and
step 6 confirms that your module is compiled into PHP. Then, start writing
code and repeat the last two steps as often as necessary.
生成扩展后,需要修改扩展的 m4 文件。打开 config.m4 文件,去掉以下配置前的 dnl :
16 PHP_ARG_ENABLE(myext, whether to enable myext support,
18 [ --enable-myext Enable myext support])
3.创建好扩展之后,进入到扩展的目录下面,使用 phpize 命令生成扩展的配置工具,然后编译安装。
phpize
./configure --with-php-config=/usr/local/php5/bin/php-config
make
make test
make install
4. 安装编译好之后,在 /usr/local/php5/lib/php/extensions/no-debug-zts-20060613 目录下看到生成的扩展文件 myext.so。
接着在 php.ini 中添加扩展信息。
extension=myext.so
使用 php -m 查看是否安装成功
4.PHP 的生命周期
1.call each extension's MINIT
这个过程在扩展被载入时调用。一般写在扩展的以下函数中 :
PHP_MINIT_FUNCTION(myext)
{
//注册常量或者类等初始化操作
return SUCCESS;
}
2.Request test.php
请求 test.php 文件。当请求到达后,PHP 会初始化执行脚本的基本环境,例如创建一个执行环境,包括保存PHP运行过程中变量名称和变量值内容的符号
表,以及当前所有的函数以及类等信息的符号表。然后PHP会调用所有的模块的 RINIT 函数,这个阶段各个模块也可以执行一些相关的操作,模块的RINIT函数
和 MINIT 函数类似:
PHP_RINIT_FUNCTION(myext)
{
//例如记录请求开始时间
return SUCCESS;
}
3.Execute test.php
执行 test.php 阶段,主要是把 PHP 文件编译成 Opcodes, 然后在 PHP 虚拟机下执行。
4.Call each extension's RSHUTDOWN
情趣处理完后进入结束阶段,一般脚本执行到末尾或者通过调用 exit() 或者 die() 函数,PHP 都将进入结束阶段。和开始阶段对应,结束阶段也分为2个
环节,一个在请求结束后(RSHUTDOWN),一个在 SAPI 生命周期结束时(MSHUTDOWN)。
PHP_RSHUTDOWN_FUNCTION(myext)
{
//例如记录请求结束时间
return SUCCESS;
}
PHP_MSHUTDOWN_FUNCTION(myext)
{
//注销一些持久化资源
return SUCCESS;
}
5.PHP 内核中的变量
zval 结构体就是通常用到的 PHP 变量在内核中的表示方式。在 zval 结构体中,可以看到4个成员变量,分别是:
1.zvalue_value value; //变量的值,PHP变量的值就保存在这里
2.zend_uint refcount__gc; //表示引用计数
3.zend_uchar type; //变量具体的类型
4.zend_uchar is_ref__gc;//表示是否为引用
PHP 内核提供了一个飞吻和设置变量类型的方法:
Z_TYPE(zval) 对应 zval 结构体的实体
Z_TYPE_P(&zval) 对应 zval 结构体的指针
Z_TYPE_PP(&&zval) 对应 zval 结构体的二级指针
//可以用下面方法设置变量的类型
Z_TYPE(zval) = IS_LONG;
//用以下方法飞吻变量的类型
if (Z_TYPE(zval) == IS_LONG) {
printf("is long \n");
}
创建一个值为 10 的整数变量 lvar:
zval lvar;
Z_TYPE(lvar) = IS_LONG;
Z_LVAL(lvar) = 10;
如果用 PHP 脚本的话,相当于以下代码:
$lvar = 10;
2.引用计数器与写时复制
PHP 是不支持指针的,为了解决这个问题,引入了引用计数器。
$a = 'huang su hong';
xdebug_debug_zval('a');
$b = &$a;
xdebug_debug_zval('a');
6.内核中的 HashTable
HashTable 是 php 的灵魂,因为在 zend 引擎中大量使用了 HashTable,如变量表,常量表,函数表等,这些都是使用 HashTable 保存,另外
PHP 数组也是使用 HashTable 实现的。
typedef struct bucket {
ulong h; // 对char *key进行hash后的值,或者是用户指定的数字索引值 /* Used for numeric indexing */
uint nKeyLength;// hash关键字的长度,如果数组索引为数字,此值为0
void *pData;// 指向value,一般是用户数据的副本,如果是指针数据,则指向pDataPtr
void *pDataPtr;//如果是指针数据,此值会指向真正的value,同时上面pData会指向此值
struct bucket *pListNext;// 整个hash表的下一元素
struct bucket *pListLast;// 整个哈希表该元素的上一个元素
struct bucket *pNext; // 存放在同一个hash Bucket内的下一个元素
struct bucket *pLast; // 同一个哈希bucket的上一个元素
const char *arKey;// 保存当前值所对于的key字符串,这个字段只能定义在最后,实现变长结构体
} Bucket;
一个 Bucket 结构只能保存一个数据,而 HashTable 的目的就是通过索引(key)把每个元素分散到唯一的位置(没有冲突的时候)。这是怎么做到的呢?答案就是,
通过 hash 算法把索引处理成一个 int 的数,然后定位到一个 Bucket 数组的其中一个元素中。
PHP 内核通过 HashTable 结构管理 Bucket 数组。
typedef struct _hashtable {
uint nTableSize; // hash Bucket的大小,最小为8,以2x增长。
uint nTableMask; // nTableSize-1 , 索引取值的优化
uint nNumOfElements;//hash Bucket中当前存在的元素个数,count()函数会直接返回此值
ulong nNextFreeElement;//下一个数字索引的位置
Bucket *pInternalPointer; /* Used for element traversal */// 当前遍历的指针(foreach比for快的原因之一)
Bucket *pListHead;// 存储数组头元素指针
Bucket *pListTail;// 存储数组尾元素指针
Bucket **arBuckets; // 存储hash数组
dtor_func_t pDestructor;// 在删除元素时执行的回调函数,用于资源的释放
zend_bool persistent;//指出了Bucket内存分配的方式。如果persisient为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数。
unsigned char nApplyCount;// 标记当前hash Bucket被递归访问的次数(防止多次递归)
zend_bool bApplyProtection;// 标记当前hash桶允许不允许多次访问,不允许时,最多只能递归3次
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;
7.Zend API 详解与扩展编写
1.什么是 zend 引擎
zend 引擎是脚本语言引擎(解析器+虚拟器),主要的工作就是解析,翻译和执行php 脚本。
zend 引擎的流程:
PHP脚本 => zend Engine Compiler => 编译 => Opcodes => Zend Engine Excutor => 解析
zend 引擎2个主要工作:
1.编译 php 脚本
2.解析执行 Opcodes,输出结果
在解析执行的过程中 zend 引擎可以调用到所有已经载入到 php 环境的扩展库。
2.zend 引擎内存管理
3.PHP 扩展架构
1.包含头文件
2.声明导出函数
3.声明 zend 函数块
4.声明 zend 模块
5.实现 get_module() 函数
6.实现导出函数
上面提到的这些结构在 php 扩展中都是必不可少的,php内核就是通过这些约定的规矩与扩展通信的。
1.声明导出函数:
编写扩展的目的是能够让 php 脚本调用扩展中的函数和类,那么怎么才能够php脚本调用编写的扩展呢?答案就是声明和实现 '导出函数'.
那什么是 ‘导出函数’呢 : 导出函数就是按照 php 内核规定的标准编写的函数。形式如下:
void zif_ext_function (
int ht,
zval *return_value,
zval *this_ptr,
int return_value_used,
zend_executor_globals *executor_globals
)
php 脚本中就可以使用下面代码调用上面函数:
<?php
ext_function(...);
因为导出函数的固定的,所以 zend 引擎提供一个方面的宏声明,具体如下:
ZEND_FUNCTION(functioin_name);
function_name 就是在 php 脚本中调用的函数名。ZEND_FUNCTION 宏会把导出函数补充成以下形式:
void zif_function_name (
int ht,
zval *return_value,
zval *this_ptr,
int return_value_used,
zend_executor_globals *executor_globals
)
导出函数是没有返回值的。
导出函数的参数和作用:
1.ht 保存扩展函数参数的个数。但是不应该直接访问这个值,而是通过 ZEND_NUM_ARGS()宏获取。
2.return_value 用来保存扩展函数向 php 脚本返回的值
3.this_ptr 根据这个参数可以访问该函数所在的对象
4.return_value_used 用来标识函数的返回值是否为脚本所用。0表示脚本不适用其他返回值,1相反。
5.executor_globals 指向 zend 引擎的全局设置,在创建新变量的时候有用。
2.声明 zend 函数块:
已经知道怎么声明导出函数,但 zend 引擎是不会自动引入声明的导出函数的。那怎么才能把编写的函数引入到zend 引擎呢.答案就是
zend_function_entry 结构体。
typedef struct _zend_function_entry {
const char *fname;
void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
const struct _zend_arg_info *arg_info;
zend_uint num_args;
zend_uint flags;
} zend_function_entry;
zend 引擎就是通过 zend_function_entry 结构数组把声明的导出函数导入内部。zend 引擎在载入扩展时,主动把这个数组中的所有函数
引入到函数表中,这样 php 脚本就可以调用这些函数了。
3.声明 zend 模块
php 扩展信息被保存在 zend_module_entry 结构体中,这个结构体包含所有需要向 zend 引起提供的模块信息。
struct _zend_module_entry {
unsigned short size;
unsigned int zend_api;
unsigned char zend_debug;
unsigned char zts;
const struct _zend_ini_entry *ini_entry;
const struct _zend_module_dep *deps;
const char *name;
const struct _zend_function_entry *functions;
int (*module_startup_func)(INIT_FUNC_ARGS);
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
int (*request_startup_func)(INIT_FUNC_ARGS);
int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
const char *version;
size_t globals_size;
#ifdef ZTS
ts_rsrc_id* globals_id_ptr;
#else
void* globals_ptr;
#endif
void (*globals_ctor)(void *global TSRMLS_DC);
void (*globals_dtor)(void *global TSRMLS_DC);
int (*post_deactivate_func)(void);
int module_started;
unsigned char type;
void *handle;
int module_number;
const char *build_id;
};
4.实现 get_module() 函数
ZEND_GET_MODULE(myext) 展开后就是 get_module 函数了, 这个函数返回一个 zend_module_entry 指针。这个指针就是 zend 模块。
这样 php 内核就可以通过调研 get_module() 函数取得编写的扩展信息了。php 内核和扩展通信的渠道就是 get_module() 函数。
5.实现导出函数
实现导出函数是构建扩展的最后一步。导出函数可以在 php 脚本中调用的函数。
PHP_FUNCTION(confirm_myext_compiled)
{
char *arg = NULL;
int arg_len, len;
char *strg;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
return;
}
len = spprintf(&strg, 0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "myext", arg);
RETURN_STRINGL(strg, len, 0);
}
实现导出函数跟声明导出函数一样都是使用 PHP_FUNCTION 宏定义一个导出函数,然后在函数体中实现想要的功能。
在声明的 zend 函数块中的所有函数都必须实现,不然会出现错误。例如,在 zend 函数块中声明一个 ZEND_FE(show_message, NULL) 函数,就必须实现
ZEND_FUNCTION(show_message) 函数。
6.接收用户传递的参数
1.取得参数
ZEND_NUM_ARGS
PHP 内核在传递参数的时候也使用引用计数的方式,所以不管是否使用了引用传递参数,它们所指向的值都是相同的。
但是当参数不是使用引用传递过来的时候,但内核却是使用引用传递。为了解决这个问题 php 内核中实现了 'zval 分离'。
zval 分离就是写时复制。使用 PZVAL_IS_REF(zval *) 宏可以判断传递过来的参数是否使用引用传递。而使用 SEPARATE_ZVAL(zval **)
可以分离一个 zval。SEPARATE_ZVAL() 宏通过 emalloc() 申请一个新的 zval。
6.在 PHP 扩展中创建变量
1.局部变量
要创建一个能够被 php 脚本访问的局部变量,先要创建一个 zval 容器,然后对这个 zval 容器进行填充,最后把它引入 zend 引擎的内部
符号表。
zval *new_var;
//申请并初始化一个新的zval容器
MAKE_STD_ZVAL(new_val);//申请并初始化一个新的zval容器
//将 'new_var' 变量引入当前的活动符号表中,现在就可以在脚本中使用 $new_var 了
ZEND_SET_SYMBOL(EG(active_symbol_table),"new_var",new_var)
符号表就像一个字典。在 php 内核中,所有的变量都是保存在这个符号表中的,php 内核就是在这个符号表中查找对应的变量的。
如果在意程序的运行效率,而不在乎内存的话,可以使用 zend_hash_update() 函数跳过检查变量名是否存在符号表中,强行插入。
2.全局变量
全局变量和局部变量在 php 内核看来是一样的,不一样的只是它们保存在不同的符号表中,局部变量保存在 active_symbol_table 中,
而全局变量保存在 symbol_table 中。只要把 active_symbol_table 替换成 symbol_table 就可以了。
zval *new_var;
MAKE_STD_ZVAL(new_var);
ZEND_SET_SYMBOL(&EG(symbol_table), "new_var", new_var);
除了符号表把 active_symbol_table 替换成 symbol_table 之外,还应该注意 active_symbol_table 是一个指针,而 symbol_table 不是,
所以还需要增加取地址符号,&EG,因为 ZEND_SET_SYMBOL 宏需要一个指针作为参数。
3.在 php 扩展中为变量赋值
在 php 中变量不但保存着值,还保存着类型。所以不但要为变量赋值,同时还要为变量设置类型。
1.长整型类型变量
zval *new_var;
MAKE_STD_ZVAL(new_var);
new_var->value.lval = 12;
new_var->type = IS_LONG;
//为了兼容,使用 ZVAL_LONG 宏赋值
zval *new_var;
MAKE_STD_ZVAL(new_var);
ZVAL_LONG(new_var,12);
//等同于
<?php
$new_var = 12;
2.双精度类型变量
zval *new_var;
MAKE_STD_ZVAL(new_var);
ZVAL_DOUBLE(new_var, 12.56);
3.字符串
zval *new_var;
MAKE_STD_ZVAL(new_var);
new_var->value.str.len = strlen(str);
new_var->value.str.val = estrdup(str);
new_var->type = IS_STRING;
estrdup() 函数是使用 zend 引起的内存管理函数.
4.布尔类型
zval *new_var;
MAKE_STD_ZVAL(new_var);
ZVAL_BOOL(new_var, 1);
5.数组类型
在为变量赋值数组类型的时候,先要创建一个 HashTable,然后将其保存在 zval.val 容器的 ht 字段中。对于这项工作,zend 引擎提供了一个简单的接口
--- array_init()。
zval *new_var;
MAKE_STD_ZVAL(new_var);
arrary_init(new_array);
//等同于
<?php
$new_var = array();
zval *array,*element;
char *key = "key_for_search";
char *value = "value_for_element";
MAKE_STD_ZVAL(array);
MAKE_STD_ZVAL(element);
array_init(array);
ZVAL_STRING(element,value,1);
add_assoc_zval(array,key,element);
//等同于
<?php
$array = array();
$element = 'value_for_element';
$array["key_for_search"] = $element;
6.对象
zval *new_object;
MAKE_STD_ZVAL(new_object);
if (object_init(new_object) != SUCCESS) {
RETURN_NULL();
}
add_prooerty_string(new_object, "name", "Jamens", 1);
7.资源类型
创建资源类型的变量比创建其他类型的变量繁琐一点。严格来说,资源不是数据类型,它是一个可以维护的任何数据类型的抽象。所有的资源类型
都是保存在一个 zend 内部的资源列表中,列表中的每份资源都有一个指向实际数据的指针。
如果某一个资源失去了所有的引用,就会触发相应的析构函数,而这个析构函数是由资源自己提供的。为什么要提供析构函数呢?因为zend引擎不能管理
资源的实际数据,所以为了防止内存泄露,就必须提供析构函数释放这些内存。
使用 zend_register_list_destructors_ex()函数可以用来注册一个资源变量的析构函数,该函数返回一个句柄。这个句柄的作用是把资源与析构函数
相关联。
一般需要一个模块全局变量保存 zend_register_list_destructors_ex()函数返回的资源。如果用 ext_skel 工具生成扩展框架,这个全局变量已经
自动生成,以 'le_' 作为前缀。如 le_myext 用来保存 zend_register_list_descructors_ex() 函数返回的资源句柄。
一般在 MINIT 阶段注册析构函数。
注册完析构函数之后,还需要把真正的资源与这资源的句柄相关联起来(这样当资源没有用时,就会自动调用析构函数)。可以使用 zend_register_resource()
或者 ZEND_REGISTER_RESOURCE() 宏实现。
zend_list_insert() 函数把资源注册到资源表中,并且返回资源在所在列表的位置(资源ID)。之后就可以通过这个id访问资源了。可以通过 RETURN_RESOURCE()
宏返回这个资源给用户。如: RETURN_RESOURCE(rsrc_id);
当注册一个资源后,zend 引擎就会一直监视这个资源的引用,当这个资源的引用为0 时,zend 引擎会自动调用资源的析构函数。
大多数创建资源的函数都有相应的用于释放资源的函数,如 mysql_connect()有 mysql_close(). 或者直接使用 unset() 一个资源变量。
<?php
$fp = fopen('fiel.txt','r');
unset($fp);
没有使用 fclose() 函数关闭文件的句柄,但是结果这个文件句柄却被关闭了。当 unset 一个资源的时候,会把这个资源的引用减1,当变量的引用等于0时
会自动调用注册的资源析构函数。这就利用了php内核的垃圾回收机制。
但是如果强制释放资源怎么办?这个时候可以使用 zend_list_delete 函数完成。该函数的作用就是把资源变量的引用减1,然后判断引用是否等于0,如果是的
话,就把资源删除。因为该函数只是把资源从列表删除,资源占用的内存是不会被释放的,所以删除资源后还要释放内存。
4.错误和输出api
在 php 内核中,不能简单的使用printf()函数打印数据。因为php有自己的一套管理输出流的函数,如果调用 printf()函数,可能会出现意外情况。
1.php_printf()
php_printf() 函数跟 c 语言的标准库的 printf() 函数很相似,唯一不同的就是 php_printf()函数指向的是 zend 输出流。
2.zend_error()
用于输出一个错误的信息。
3.向 phpinfo() 中输出信息
怎么向 phpinfo() 中添加信息呢? 答案就是实现 ZEND_MINFO() 函数。当在 php 脚本中调用 phpinfo() 函数时,ZEND_MINFO() 函数会自动被调用。
如果指定 ZEND_MINFO()函数,phpinfo() 会自动打印一个小节,而小节的头部就是模块名。
php 内核提供了一些以表格格式显示信息的接口,可以使用这些接口格式化要显示的信息。一般情况下,需要完成3个步骤:
1.调用 php_info_print_table_start 函数制定表格
2.调用 php_info_print_table_header 和 php_info_print_table_row 这2个函数打印表格具体的行列信息。
3.调用 php_info_print_table_end 函数结束表格
php_info_print_table_start, php_info_print_table_end 这2个函数没有参数,而 php_info_print_table_header, php_info_print_table_row
这2个函数需要的参数会工具需要而变化。
5. 运行时信息函数
如果想要了解正在执行的文件名或者正在进行的函数名,可以通过 php 内核提供的一系列查看这些信息的函数。
1.查看当前正在执行的函数名, get_active_function_name()。
2.查看当前执行的文件名, zend_get_executed_filename()。
3.查看当前执行到哪一行代码,zend_get_executed_lineno()。
6.调用用户自定义函数
能不能在扩展里面调用用户在 php 脚本中定义的函数呢?可以的。php 允许在扩展里面调用 php 脚本中定义的函数,实现一些回调机制之类的功能。
要在扩展中调用一个 php 脚本中定义的函数,可以使用 call_user_function_ex() 函数实现。
7.php 配置项
如果你想为你的扩展创建一个 .ini 文件的配置节,就可以使用 PHP_INI_BEGIN() 宏标识这个节的开始,并使用 PHP_INI_END() 宏标识配置节的结束。
可以在两者之间使用 PHP_INI_ENTRY()宏创建具体的配置项,如下:
PHP_INI_BEGIN();
PHP_INI_ENTRY('myext.setting', 'hello world', PHP_INI_ALL,NULL);
PHP_INI_END();
PHP_INI_ENTRY()宏接收4个参数:配置项名称,初始值,改变这个值所需要的权限以及改变这个值时的回调函数句柄。配置项名称和初始值必须是一个字符串,
即使它们是一个整数也要转换成字符串。
改变配置项所需的权限可以分为三种:
1.PHP_INI_SYSTEM, 只允许在 php.ini 中修改这些值
2.PHP_INI_USER, 允许用户在运行像 .htaccess 这样的文件时重写其值
3.PHP_INI_ALL, 允许随意修改这些值
第四个参数指定当初始值被修改后的回调函数句柄。可以在 PHP_INI_MH() 宏定义函数:
PHP_INI_MH(OnChangeSetting);
要让扩展能够读到配置的值,就必须把整个初始化配置项引入到 php 内核中,这项工作可以在模块的其实和结束函数中使用 REGISTER_INI_ENTRIES() 宏和
UNREGISTER_INI_ENTRIES() 宏完成.
8.创建常量的宏
PHP 的常量是不需要使用 '$' 作为前缀的,而且是全局有效。
//创建长整型常量 MY_NEW_CONSTANT,并且是大小写敏感和持久化的
REGISTER_LOONG_CONSTANT('MY_NEW_CONSTANT', 10, CONST_CS | CONST_PERSISTENT);
1.windows
2.Linux 下
3.PHP 的生命周期
4.PHP 内核中的变量
5.内核中的 HashTable
6.Zend API 详解与扩展编写