这篇文章是我个人的学习笔记,我把这篇文章送给所有喜欢PHP语言,喜欢PHP扩展开发的同行、同学们。
闲话不和大家撤了,如果您想学习php的扩展开发,相信您对php基础知识已经有了一定的了解。PHP扩展时用C语言编写的,如果您还不知道C语言
里面指针式什么东西,建议您先移步他出,好好学习一下C语言,大家都在骂谭浩强的《C语言程序设计》,但是我仍然建议您好好看看这本书。
做PHP开发得先准备一下环境吧。说起来容易,对于一些新手来说恐怕也是一场噩梦。建议您安装UBUNTU LINUX,网上有很多的linux安装教程,相信我不用多说了。
我们假设您已经安装了linux,我用的debain,不过ubuntu的同学不用担心,因为ubuntu是debain变话而来的,所谓万变不离其宗,相信您使用ubuntu可以达到同样的效果。
首先,我们安装一下php的开发环境:PHP+APACHE2+MYSQL
UBUNTU下的安装很简单,简单到您只需要执行下面的命令就可以了:
sudo apt-get install apache2
sudo apt-get install php5
sudo apt-get install libapache2-mod-php5
sudo apt-get install mysql-server
sudo apt-get install libapache2-mod-auth-mysql
sudo apt-get install php5-mysql
sudo /etc/init.d/apache2 restart
进行php扩展开发我们还需要下载一份php的源代码,我们以PHP5.3为例进行php扩张开发讲解,下面是一些下载地址,为了防止下载链接失效,我会不断更新链接,当然PHP版本不会变化^_^
http://cn2.php.net/get/php-5.3.19.tar.gz/from/this/mirror
你也可以到www.php.net上面,点击download下载源码。
假设您已经或得到了源码,下面我们将源代码解压缩 tar xzvf php-5.3.19 如果这个命令不能正确解压缩的话请使用tar xvf php-5.3.19试试,再不行的话用rar解压缩吧,我相信你总有办法将这个文件解压缩~ 多大的事 - _ -
我们的php扩展是在linux上面开发的,如果您想在win上面开发,这片文章恐怕不太适合您了。但是技术这种东西都是触类旁通,希望您在win上面部署好了环境再来看看我的文章。
还记得第一次写代码,是用C语言打印出来了HelloWorld,那天我兴奋激动了一天。希望你你看完本页内容后自己也写一个helloworld的php扩展。
第一个扩展程序,改怎么写呢?
其实很简单~~~ 开始吧~~
上次我们说,我们第一步需要卸载php的源代码,你下载好了吗?
建议:建议大家在开发时,只安装一个php,其实在一个系统中你可以安装多个php,但是为了避免前期很多不必要的解释,读者最好安装一个。等你对php娴熟后,多个php就不在话下了。
ext文件夹存放了是php扩展的源代码,同样的,我们编写扩展也需要这里面进行
大家可以看看ext文件夹下各个文件夹的名字:mysql、xml、zip、sqlite...这些都是php的扩张
执行上面的命令建立扩张开发的框架,这是你会发现ext文件夹下面多了一个叫helloworld的文件夹,同时命令行也输出了一些文本,
我们先把这些东西放在这里,不做解释,稍后大家在看看输出的命令是什么意思。
记者这幅图奥,我们下面会分析
3.3 进入php源码的根目录, 编辑文件 vi ext/helloworld/config.m4
去掉这个文件中的几行注释
所谓的取消注释,就是讲16~18行前面的dnl去掉而已(不同版本的php源码行数可能不一样,请大家注意)
注释取消后,保存文件并且退出
3.4 在php根目录执行命令./buildconf --force 这一步以后的文章中进行解释
3.5 在php源码的根目录编译php程序,注意命令为 ./configure--with-helloworld
3.6 进入我们的扩张目录helloworld,执行命令 phpize
3.7 在helloworld目录编译我们的扩展./configure --with-php-config=/usr/local/bin/php-config(使用你自己环境的php-config)--enable-helloworld
3.8 执行make命令 make
我在执行make的时候,系统报错,错误信息为
/home/work/src/php-5.3.19/ext/helloworld/helloworld.c:43: error:'PHP_FE_END' undeclared here (not in a function)
make: *** [helloworld.lo] Error 1
此时,请大家讲helloworld.c中的第43行中PHP_FE_END换成{NULL,NULL, NULL}
替换完成后,请大家执行make命令重新编译
有些同学可能知道,php的程序可以使用makeinstall进行安装,但是为了更好的理解php的工作原理,
我们采用手动安装php扩展的方式进行安装,其实php的安装非常简单
3.9.1 找到php的扩展安装目录
work@baidu:~$ php -r "phpinfo();" | grep extension_dir
extension_dir => /usr/lib/php5/20090626+lfs =>/usr/lib/php5/20090626+lfs
我们需要将我们的扩张安装到/usr/lib/php5/20090626+lfs目录下面,
在helloworld目录下面执行命令
sudo cpmodules/helloworld.so /usr/lib/php5/20090626+lfs 目录/usr/lib/php5/20090626+lfs需要根据自己的实际情况作出改动
3.9.2 在php.ini中打开我们的扩展
同样,请执行下面的命令
work@baidu:~$ php -r "phpinfo();" | grep"php.ini"
Configuration File (php.ini) Path => /etc/php5/cli
Loaded Configuration File => /etc/php5/cli/php.ini
可以看出php.ini的路径为/etc/php5/cli/php.ini
sudo vi /etc/php5/cli/php.ini
在文件的最后一行添加代码 extension=helloworld.so
你可以通过执行下面的命令进行验证 php -r "phpinfo();" | grep helloworld
如果你安装成功的话,应该看到下面的信息
root@baidu:/usr/lib/php5/20090626+lfs# php -r "phpinfo();" |grep helloworld
helloworld
helloworld support => enabled
至此,php扩展安装完成
也许你会问,我该怎么打印helloworld呢,别急,下面接着就将helloworld程序了。
3.11 进入扩展helloworld目录,编辑文件php_helloworld.h,在最后一行添加函数 PHP_FUNCTION(fun_helloworld);
3.12 在helloworld.c中实现我们的函数
将fun_helloworld函数加入到helloworld_functions[]中
3.14 按照step 9-2 重新安装我们的扩展
php -r "echo fun_helloworld();"
如果一切顺利的话,你就可以看到我们的问候语句了Hello World !
也许你按照我的步骤走到最后,你看到了我们的问候语hello world,也许你没有,但是我已经尽力的把开发的过程描述的尽量详细,请大家勿要骂我。其实做程序开发的过程就是一个不断解决问题的过程,如果你在我上面的步骤中遇到了问题,那么我要恭喜你,因为你得到一个提高自己能力的机会。在解决问题的过程中,百度、谷歌是我们最好的帮手,虽然你也可以通过其他教程或者官方文档解决问题,但是百度、谷歌对于新手来说却是最快解决问题的方式。
说了这么多,无非是想告诉大家:不要害怕在学习的过程中遇到问题,因为这些问题才是我们进步的基石。
4 SAPI简介
要想写好php扩展,了解一下SAPI肯定是少不了的。也许你看完本节的内容你还不能很好的掌握SAPI,不过没有关系,这并不会影响到你后续的php扩展开发,但是我建议读者还是细细地看完这一节,如果有问题的话可以给我联系。
SAPI(ServerApplication Programming Interface)指是PHP具体应用编程接口。PHP脚本的执行方式有很多,常见的就是把PHP放在web服务器上面执行、在命令行直接通过php test.php的方式执行,其实php脚本也可以嵌入在其他程序中执行。
目前常见的sapi实现方式有apache模块,fpm,cgi,cli等,而这些实现可以分为三类:单进程SAPI、多进程SAPI、多线程SAPI。
CLI/CGI模式的PHP属于单进程SAPI实现方式(或者叫做实现模型)。这类的请求在处理一次后就关闭。它的生命周期如下:
请记住:每次请求都会执行上面所有的过程。
给大家解释一下:假如你使用命令行方式运行php文件或者通过web服务器以cgi方式访问一个php文件,每一次请求你都会经历下面的过程:
1. 执行每一个扩展的MINIT方法
2. 执行每个扩展的RINIT方法
3. 执行你要访问的php文件
4. 执行每个扩展的RSHUTDOWN方法
5. 执行清理工作(针对你要执行的php文件而言)
6. 执行每个扩展的MSHUTDOWN方法
7. php执行结束
看到MINIT, RINIT,RSHUTDWON, MSSHUTDOWN了吗,是不是有点面熟啊?
如果PHP编译成为apache的一个模块处理PHP请求,那么apache一般会采用多进程的模式,apache启动后会创建多个子进程,每个子进程在整个生命周期中,可能会处理多次请求。只有在apache关闭,这些子进程才会关闭。这种模式的SAPI生命周期如下:
解释:上图顶部红色部分在apache fork出来这个子进程后就会自行,而中间蓝颜色表示部分每个请求时执行一次,一次请求只能在被一个子进程处理,上图底部红色部分是子进程结束的时候执行的。对于每个进程而言,MINIT函数只会执行一次,但是每次请求都会执行RINIT RSHUTDOWN函数,进程结束时执行MSHUTDOWN函数。
多线程模式和多进程中的某个进程类似,不同的是整个进程的声明周期内会并行重复请求开始->请求结束,它的生命周期如下图:
我们依次讲解一下apache的对请求的处理流程、php文件的处理流程、php扩展执行的流程。如果您想深入了解扩展,并且向在扩展开发方面有所成就的话,那么我建议您详细的看一下这一节,也许你看不懂,但是没有关系,以后我们还会详细讲解。
5 php扩展调用php.ini配置
本节中我们读取一下php.ini文件中的配置。其实,读取php.ini配置的方式挺多,本次我们只讲一种,如果有兴趣的话,大家可以各自研究一下。
废话少说,我们更改一下之前的讲解方式,本次我们直接上代码。(其实是因为php扩展读取配置文件太简单了)
(假设我们建立了一个ini_read的扩展,代码的改动部分我们用黄颜色标出
在php.ini中增加配置ini_read.helloworld=hellohellohello
php_ini_read.h的改动如下:(改动的地方我用黄颜色)
#ifndef PHP_INI_READ_H
#define PHP_INI_READ_H
externzend_module_entry ini_read_module_entry;
#define phpext_ini_read_ptr &ini_read_module_entry
#ifdef PHP_WIN32
# define PHP_INI_READ_API __declspec(dllexport)
#elif defined(__GNUC__) &&__GNUC__ >= 4
# define PHP_INI_READ_API __attribute__((visibility("default")))
#else
# define PHP_INI_READ_API
#endif
#ifdef ZTS
#include "TSRM.h"
#endif
PHP_MINIT_FUNCTION(ini_read);
PHP_MSHUTDOWN_FUNCTION(ini_read);
PHP_RINIT_FUNCTION(ini_read);
PHP_RSHUTDOWN_FUNCTION(ini_read);
PHP_MINFO_FUNCTION(ini_read);
/*
Declare any global variables you may need between the BEGIN
and END macros here:
ZEND_BEGIN_MODULE_GLOBALS(ini_read)
long global_value;
char *global_string;
ZEND_END_MODULE_GLOBALS(ini_read)
*/
/* In every utility function you addthat needs to use variables
in php_ini_read_globals, call TSRMLS_FETCH(); after declaring other
variables used by that function, or better yet, pass in TSRMLS_CC
after the last function argument and declare your utility function
with TSRMLS_DC after the last declared argument. Always refer to
the globals in your function as INI_READ_G(variable). You are
encouraged to rename these macros something shorter, see
examples in any other php module directory.
*/
#ifdef ZTS
#define INI_READ_G(v)TSRMG(ini_read_globals_id, zend_ini_read_globals *, v)
#else
#define INI_READ_G(v)(ini_read_globals.v)
#endif
#endif /* PHP_INI_READ_H */
/*
* Local variables:
* tab-width: 4
* c-basic-offset: 4
* End:
* vim600: noet sw=4 ts=4 fdm=marker
* vim<600: noet sw=4 ts=4
*/
PHP_FUNCTION(helloworld);
ini_read.c的改动如下
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "php_ini.h"
#include"ext/standard/info.h"
#include "php_ini_read.h"
/* If you declare any globals inphp_ini_read.h uncomment this:
ZEND_DECLARE_MODULE_GLOBALS(ini_read)
*/
/* True global resources - no need forthread safety here */
static int le_ini_read;
/* {{{ ini_read_functions[]
*
* Every user visible function must have anentry in ini_read_functions[].
*/
constzend_function_entry ini_read_functions[]={
PHP_FE(helloworld, NULL) /* Fortesting, remove later. */
{NULL, NULL, NULL}
};
/* }}} */
/* {{{ ini_read_module_entry
*/
zend_module_entry ini_read_module_entry={
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
"ini_read",
ini_read_functions,
PHP_MINIT(ini_read),
PHP_MSHUTDOWN(ini_read),
NULL,
NULL,
NULL,
#if ZEND_MODULE_API_NO >= 20010901
"0.1",/* Replace with version number for yourextension */
#endif
STANDARD_MODULE_PROPERTIES
};
/* }}} */
#ifdef COMPILE_DL_INI_READ
ZEND_GET_MODULE(ini_read)
#endif
/* {{{ PHP_INI
*/
PHP_INI_BEGIN()
PHP_INI_ENTRY("ini_read.helloworld", "foobar",PHP_INI_ALL, NULL)
PHP_INI_END()
/* }}} */
/* {{{ php_ini_read_init_globals
*/
/* Uncomment this function if you haveINI entries
static voidphp_ini_read_init_globals(zend_ini_read_globals *ini_read_globals)
{
ini_read_globals->global_value = 0;
ini_read_globals->global_string = NULL;
}
*/
/* }}} */
/* {{{ PHP_MINIT_FUNCTION
*/
PHP_MINIT_FUNCTION(ini_read)
{
REGISTER_INI_ENTRIES();
return SUCCESS;
}
/* }}} */
/* {{{ PHP_MSHUTDOWN_FUNCTION
*/
PHP_MSHUTDOWN_FUNCTION(ini_read)
{
UNREGISTER_INI_ENTRIES();
return SUCCESS;
}
/* }}} */
/* Remove if there's nothing to do atrequest start */
/* {{{ PHP_RINIT_FUNCTION
*/
PHP_RINIT_FUNCTION(ini_read)
{
return SUCCESS;
}
/* }}} */
/* Remove if there's nothing to do atrequest end */
/* {{{ PHP_RSHUTDOWN_FUNCTION
*/
PHP_RSHUTDOWN_FUNCTION(ini_read)
{
return SUCCESS;
}
/* }}} */
/* {{{ PHP_MINFO_FUNCTION
*/
PHP_MINFO_FUNCTION(ini_read)
{
php_info_print_table_start();
php_info_print_table_header(2,"ini_read support","enabled");
php_info_print_table_end();
/* Remove comments if you have entries in php.ini
DISPLAY_INI_ENTRIES();
*/
}
/* }}} */
/* }}} */
/* The previous line is meant for vimand emacs, so it can correctly fold and
unfold functions in source code. See the corresponding marks just before
function definition, where the functions purpose is also documented.Please
follow this convention for the convenience of others editing your code.
*/
/*
* Local variables:
* tab-width: 4
* c-basic-offset: 4
* End:
* vim600: noet sw=4 ts=4 fdm=marker
* vim<600: noet sw=4 ts=4
*/
PHP_FUNCTION(helloworld)
{
RETURN_STRING(INI_STR("ini_read.helloworld"),1);
}
上面步骤执行完了后,根据之前安装扩张的方式安装一下我们的扩展吧。
如果你安装完了扩展,那么运行下面的命令您就可以看到输出了
php -r "echohelloworld();" 此时的输出应该为hellohellohello
将这一节的目的是想交给大家,如和声明一个变量。使得这个变量针对每次请求独立,也就是说,同一次请求我们访问的变量是同一个,不同的请求我们使用的变量不是同一个。
说道这里我先抛出一个问题:既然要实现上面的要求,那么我们该怎么办呢?我应该在哪里声明我的全局变量呢?
还记得SAPI简介那一张吗?SAPI的实现有三种方式,单进程,多进程,多线程,但是对于每一次而言,都必须执行的几个过程为RINIT RSHUTDOWN….说道这里你意识到了吗。我们是不是在RINIT过程的时候初始化我们的全局常量,这样每次请求的时候这个变量的值都会变成默认值。还没有看明白?看看下面的流程你也许就懂了。
1. 首先在.h文件中声明全局变量
2. 在RINIT过程时初始化这个变量
3. 调用变量
我们还是直接上源代码:我把一些无用的注释去掉了,希望大家别介意
头文件php-iamnew.h
#ifndef PHP_IAMNEW_H
#define PHP_IAMNEW_H
extern zend_module_entry iamnew_module_entry;
#define phpext_iamnew_ptr &iamnew_module_entry
#ifdef PHP_WIN32
# definePHP_IAMNEW_API __declspec(dllexport)
#elif defined(__GNUC__) && __GNUC__ >= 4
# definePHP_IAMNEW_API __attribute__ ((visibility("default")))
#else
# definePHP_IAMNEW_API
#endif
#ifdef ZTS
#include "TSRM.h"
#endif
PHP_MINIT_FUNCTION(iamnew);
PHP_MSHUTDOWN_FUNCTION(iamnew);
PHP_RINIT_FUNCTION(iamnew);
PHP_RSHUTDOWN_FUNCTION(iamnew);
PHP_MINFO_FUNCTION(iamnew);
PHP_FUNCTION(confirm_iamnew_compiled); /* For testing, remove later. */
ZEND_BEGIN_MODULE_GLOBALS(iamnew)
long counter;
ZEND_END_MODULE_GLOBALS(iamnew)
#ifdef ZTS
#define IAMNEW_G(v) TSRMG(iamnew_globals_id,zend_iamnew_globals *, v)
#else
#define IAMNEW_G(v) (iamnew_globals.v)
#endif
#endif
PHP_FUNCTION(test_global_value);
源文件iamnew.c
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_iamnew.h"
ZEND_DECLARE_MODULE_GLOBALS(iamnew) //声明全局变量
static int le_iamnew;
const zend_function_entry iamnew_functions[]={
PHP_FE(test_global_value,NULL)
{NULL,NULL,NULL} //此处修改以后不再解释
};
zend_module_entry iamnew_module_entry ={
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
"iamnew",
iamnew_functions,
PHP_MINIT(iamnew),
PHP_MSHUTDOWN(iamnew),
PHP_RINIT(iamnew),
PHP_RSHUTDOWN(iamnew),
PHP_MINFO(iamnew),
#if ZEND_MODULE_API_NO >= 20010901
"0.1",
#endif
STANDARD_MODULE_PROPERTIES
};
#ifdef COMPILE_DL_IAMNEW
ZEND_GET_MODULE(iamnew)
#endif
// 这个函数之前是被注释的,去掉注释,并且函数内容为空即可
static void php_iamnew_init_globals(zend_iamnew_globals*iamnew_globals)
{
}
PHP_MINIT_FUNCTION(iamnew)
{
ZEND_INIT_MODULE_GLOBALS(iamnew, php_iamnew_init_globals,NULL);
return SUCCESS;
}
PHP_MSHUTDOWN_FUNCTION(iamnew)
{
return SUCCESS;
}
PHP_RINIT_FUNCTION(iamnew)
{
IAMNEW_G(counter)=0; //初始化
return SUCCESS;
}
PHP_RSHUTDOWN_FUNCTION(iamnew)
{
return SUCCESS;
}
PHP_MINFO_FUNCTION(iamnew)
{
php_info_print_table_start();
php_info_print_table_header(2,"iamnew support","enabled");
php_info_print_table_end();
}
// 增加测试函数
PHP_FUNCTION(test_global_value)
{
IAMNEW_G(counter)++;
RETURN_LONG(IAMNEW_G(counter));
}
修改完成后,编译安装我们的扩展,执行下面的命令进行测试
php -r "echotest_global_value(); test_global_value();"
以后我们可能不在对这些简单的结果进行验证了,但是为了学习效果,建议大家自己验证一下。
我们先看一下头文件中声明全局变量的两个宏
ZEND_BEGIN_MODULE_GLOBALS
ZEND_END_MODULE_GLOBALS
我们看一下这两个宏的展开内容:
#define ZEND_BEGIN_MODULE_GLOBALS(module_name) \
typedef struct _zend_##module_name##_globals {
#define ZEND_END_MODULE_GLOBALS(module_name) \
}zend_##module_name##_globals;
从展开信息中我们可以看到,这两个宏仅仅是定了一个叫做zend_##module##_globals的结构体,而ZEND_BEGIN_MODULE_GLOBALS和ZEND_END_MODULE_GLOBALS就是结构体zend_##module##_globals的成员变量。
我们再来看一下ZEND_DECLARE_MODULE_GLOBALS这句话是
#define ZEND_DECLARE_MODULE_GLOBALS(module_name) \
ts_rsrc_idmodule_name##_globals_id;
其实ts_rsrc_id就是int类型,查看ts_rsrc_id的定义就可以知道:typedef int ts_rsrc_id;
在一个宏声明ZEND_INIT_MODULE_GLOBALS,查看一下这个宏的定义
#define ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor,globals_dtor) \
ts_allocate_id(&module_name##_globals_id,sizeof(zend_##module_name##_globals), (ts_allocate_ctor) globals_ctor,(ts_allocate_dtor) globals_dtor);
从定义来看,ZEND_INIT_MODULE_GLOBALS给变量module_name##_globals_id分配了一个id,这个id是一个线程安全的资源id。而module_name##_globals_id不就是ZEND_DECLARE_MODULE_GLOBALS分配的变量吗!
globals_ctor是一个回调函数指针,这里不再多说。
我们再来看一下IAMNEW_G这个宏的定义
#ifdef ZTS //是否线程安全
#defineIAMNEW_G(v) TSRMG(iamnew_globals_id,zend_iamnew_globals *, v)
#else
#defineIAMNEW_G(v) (iamnew_globals.v)
#endif
----------------------------------------------------
TSRMG的定义为:
#define TSRMG(id, type, element) (((type) (*((void ***)tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(id)])->element)
tsrm_ls的定义为:
void ***tsrm_ls;
从宏整体的定义可以看出IAMNEW_G是取得变量的值。
其实IAMNEW_G就是取的全局变量的值。
如果你对TSRMG非常感兴趣的话,也可以看看这个宏具体的实现方式,不多对于新手来说的确有点困难。
7 使用php.ini里面的配置初始化全局变量
前面我们讲到了如何读取php.ini里面的配置,也讲到了如何初始化全局变量,这一节的任务也很明了,就是如何使用php.ini里面的配置初始化全局变量。这一节我们作为课堂任务来进行处理,不在单独解释。
前面我们讲到了php扩展开发的大体架构,本节我们介绍扩展如何接收php脚本中传入的参数。
任务:写一个扩展,输出php脚本传入的参数。例如php –r “echo hello(‘param test.’);” 将要输出param test.
首先我们建立一个paramtest的扩展,建议、编译、安装、测试过程不再赘述。
首先,我们需要建立一个函数叫做hello,建立的过程和之前是一样的,也不多说。
我们具体看一下,hello函数的实现,paramtest.c中hello函数的实现如下:
PHP_FUNCTION(hello)
{
char* str_hello;
int int_hello_str_length;
if(zend_parse_parameters(ZEND_NUM_ARGS()TSRMLS_CC,"s",
&str_hello,&int_hello_str_length)==FAILURE)
{
RETURN_NULL();
}
php_printf("%s",str_hello);
RETURN_TRUE;
}
我们先看一下zend_parse_parameters的函数定义:ZEND_NUM_ARGS() TSRMLS_CC其实是zend_parse_parameters的两个参数,具体可以查找这两个宏的定义,这两个宏主要是传入参数信息和保证线程安全。参数”s”其实是格式化字符串,也就是说这个参数告诉ZEND编译器,我可以接收的参数类型是什么样子的。Zend_parse_parameters的其他参数负责具体接收php脚本中函数变量的值。
请注意:我们在php脚本的函数中传入了一个参数,但是在zend_parse_parameters中却需要两个参数进行接收,给大家解释一下原因:我们在php脚本中传入的参数是字符串,对于C语言来说,PHP脚本传入字符串的长度是无法直接用函数strlen来进行获取的。原因是因为,我们在php脚本中可以传入\0,但是\0在C语言中是字符串结尾的意思。
在zend_parse_parameters中,格式化字符创(本例中是”s”)有很多,如果你想要接受多个参数的话,只要在格式化参数中加入相应的类型标示符,在参数中添加接受变量就可以了。例如,如果你的php函数需要传入两个变量,第一个变量为问候字符串,第二个参数为bool,表示是否输出,那么zend_parse_parameters函数可以这样写:zend_parse_parameters(ZEND_NUM_ARGS()TSRMLS_CC,"sb",&str_hello,&len,&is_output)。
其中,格式化字符串可以列表如下:
PHP变量类型 | 代码 | C扩展变量类型 |
boolean | b | zend_bool |
long | l | long |
double | d | double |
string | s | char*, int |
resource | r | zval* |
array | a | zval* |
object | o | zval* |
zval | z | zval* |
上表中有很多zval类型,这个类型我们下一节进行单独介绍。
另外一个需要注意的地方是,我们使用php_printf在C函数中进行输出,你能想到原因吗?前面我们已经讲到php既可以作为脚本在命令行运行,也可以通过web服务器,以单进程、多进程、多线程方式运行,如果我们在web服务器中将我们的信息输出到stdout中,会导致信息无法输出或者输出错误。给大家举个例子,我们可以再apache+php环境中写一个脚本,通过浏览器访问这个脚本,如果你使用printf输出字符串,那么浏览器将无法看到你的输出信息。但是如果你使用php_printf输出的话,浏览器就能显示你输出的信息。
我们已经学会了如何接收普通参数,大家都知道php中还有可选参数,那么我们改怎样接收可选参数呢?(如果你不知道php可以使用可选参数的话,建议你先温习一下php的基础知识。)
这个很简单,我们直接上代码:
PHP_FUNCTION(hello)
{
char*str_hello;
int int_hello_str_length;
zend_bool is_output =0;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"s|b",&str_hello,
&int_hello_str_length,&is_output)== FAILURE)
{
RETURN_NULL();
}
if(is_output)
{
php_printf("%s", str_hello);
}
RETURN_TRUE;
}
很简单吧,如果你有可选参数,那么在格式化字符串中加入|就可以了,|后面的就是可选参数,其他的和普通变量的接收是一样的。
9 zval结构分析
我们先来看一下zval的定义:
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;
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;
9.2 zval的创建和使用
我们先来看一段代码:
PHP_FUNCTION(hello)
{
zval* t;
ALLOC_INIT_ZVAL(t);
Z_TYPE_P(t)= IS_LONG;
Z_LVAL_P(t)=1234567890;
zval_ptr_dtor(&t);
}
ALLOC_INIT_ZVAL宏用来给t分配内存,并且将t初始化为一个空变量,Z_TYPE_P用来给zval变量指定变量的类型,Z_LVAL_P用来给变量赋值,zval_ptr_dtor用来清理变量空间。
我们可以使用上面的代码使用变量外,我们还可以使用宏ZVAL_LONG来快速的定义变量和给变量赋值。也就是说上面的代码我们可以使用下面的代码来代替。
PHP_FUNCTION(hello)
{
zval* t;
ZVAL_LONG(t, 1234567890);
}
我们可以使用下面的宏来快速的定义和使用zval变量:
ZVAL_RESOURCE、 ZVAL_BOOL、ZVAL_NULL、ZVAL_LONG、ZVAL_DOUBLE
ZVAL_STRING、ZVAL_STRINGL、ZVAL_EMPTY_STRING、ZVAL_ZVAL
上面的宏很简单,如果大家有什么不明白的地方,大家可以去看看源代码。
终于讲完zval了,前面我们讲到了函数的定义和使用,但是我们没有讲函数的返回值。因为C扩展中,函数的返回值类型为zval的,所以我们把这一节放在了这里进行讲解。
本节任务,我们写一个简单的计算器,完成加减乘除运算,要求:编写函数calculate(num1, num2, opt),我们希望完成num1 opt num2= ?的运算。我们看一下完成上面任务的代码:
PHP_FUNCTION(calculate)
{
int num1;
int num2;
char* opt;
int opt_len;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"lls",
&num1,&num2,&opt,&opt_len)==FAILURE)
{
php_printf("param error. example: calculate(123, 456, '+')\n");
RETURN_NULL();
}
if(1!= opt_len)
{
php_printf("param error. example: calculate(123, 456, '+')\n");
RETURN_NULL();
}
switch (opt[0])
{
case '+':
return_value->type= IS_LONG;
return_value->value.lval= num1+ num2;
break;
case '-':
return_value->type= IS_LONG;
return_value->value.lval= num1- num2;
break;
case '*':
return_value->type= IS_LONG;
return_value->value.lval= num1* num2;
break;
case '/':
return_value->type= IS_DOUBLE;
return_value->value.lval= num1*1.0/ num2;
break;
default:
return_value->type= IS_LONG;
return_value->value.lval=0;
break;
}
}
看到上面代码,不知道大家有没有疑惑,return_value是怎么来的?
return_value是你宏PHP_FUNCTION宏中定义的,PHP_FUNCTION会声明这个变量,并且将这个变量赋值为NULL,我们来看一下宏定义:
#define ZEND_NAMED_FUNCTION(name) void name(INTERNAL_FUNCTION_PARAMETERS)
#define ZEND_FUNCTION(name) ZEND_NAMED_FUNCTION(ZEND_FN(name))
#define INTERNAL_FUNCTION_PARAMETERSint ht, zval *return_value, \
zval **return_value_ptr, zval *this_ptr, intreturn_value_used TSRMLS_DC
从上面的宏定义来看,return_value就是zval的一个指针,php用这个变量来指向函数的返回值。其实我们也有一个宏可以进行简单的返回,宏定义如下,RETVAL_*(v),*表示的就是各种变量类型,v表示变量的值,例如RETVAL_LONG(34),将返回一个long类型的数值,其值为34。
00:14啦,说点题外话,小时候非常喜欢过年,但是长大后发现自己害怕了。不知道你有没有同感?
不说啦,大家有问题给我发邮件niujiaming0819@163.com,或者联系我qq947847775.
现在太晚啦,大家晚安。
本节我们讲一下php的数组,在php中,数组使用HashTable实现的。本节中我们先详细的介绍一下HashTable,然后再讲讲如何使用HastTable
所谓的变长结构体,其实是我们C语言结构体的一种特殊用法,并没有什么新奇之处。我们先来看一下变长结构体的一种通用定义方法。
typedef struct bucket {
int n;
char key[30];
char value[1];
}Bucket;
我们定义了一个结构体Bucket,我们希望用这个结构体存放学生的个人简介。其中key用来存在学生的姓名,value用来存放学生的简介。大家可能很好奇,我们的value声明了长度为1. 1个char能存多少信息呀?
其实,对于变长结构体,我们在使用的使用不能直接定义变量,例如:Bucket bucket; 您要是这样使用,value肯定存储不了多少信息。对于变长结构体,我们在使用的时候需要先声明一个变长结构体的指针,然后通过malloc函数分配函数空间,我们需要用到的空间长度是多少,我们就可以malloc多少。通用的使用方法如下:
Bucket*pBucket;
pBucket =malloc(sizeof(Bucket)+ n *sizeof(char));
其中n就是你要使用value的长度。如果这样使用的话,value指向的字符串不久变长了吗!
我们先看一下HashTable的定义
struct _hashtable;
typedef struct bucket {
ulong h;//当元素是数字索引时使用
uint nKeyLength;//当使用字符串索引时,这个变量表示索引的长度,索引(字符串)保存在最后一个元素aKey
void *pData;//用来指向保存的数据,如果保存的数据是指针的话,pDataPtr就指向这个数据,pData指向pDataPtr
void *pDataPtr;
struct bucket *pListNext;//上一个元素
struct bucket *pListLast;//下一个元素
struct bucket *pNext;//指向下一个bucket的指针
struct bucket *pLast;//指向上一个bucket的指针
char arKey[1];//必须放在最后,主要是为了实现变长结构体
}Bucket;
typedef struct _hashtable {
uint nTableSize; //哈希表的大小
uint nTableMask; //数值上等于nTableSize - 1
uint nNumOfElements; //记录了当前HashTable中保存的记录数
ulongnNextFreeElement; //指向下一个空闲的Bucket
Bucket *pInternalPointer; //这个变量用于数组反转
Bucket *pListHead; //指向Bucket的头
Bucket *pListTail; //指向Bucket的尾
Bucket **arBuckets;
dtor_func_tpDestructor; //函数指针,数组增删改查时自动调用,用于某些清理操作
zend_bool persistent; //是否持久
unsigned char nApplyCount;
zend_boolbApplyProtection; //和nApplyCount一起起作用,防止数组遍历时无限递归
#if ZEND_DEBUG
int inconsistent;
#endif
}HashTable;
希望大家能好好看看上面的定义,有些东西我将出来反而会说不明白,不如大家看看代码浅显明了。PHP的数组,其实是一个带有头结点的双向链表,其中HashTable是头,Bucket存储具体的结点信息。
11.3 HashTable内部函数分析
#defineHASH_PROTECT_RECURSION(ht) \
if ((ht)->bApplyProtection) { \
if ((ht)->nApplyCount++ >= 3){ \
zend_error(E_ERROR, "Nestinglevel too deep - recursive dependency?"); \
} \
}
这个宏主要用来防止循环引用。
#defineZEND_HASH_IF_FULL_DO_RESIZE(ht) \
if ((ht)->nNumOfElements >(ht)->nTableSize) { \
zend_hash_do_resize(ht); \
}
这个宏的作用是检查目前HashTable中的元素个数是否大于了总的HashTable的大小,如果个数大于了HashTable的大小,那么我们就重新分配空间。我们看一下zend_hash_do_resize
static intzend_hash_do_resize(HashTable*ht)
{
Bucket **t;
IS_CONSISTENT(ht);
if ((ht->nTableSize<<1)>0){ /* Let's double the table size */
t = (Bucket**) perealloc_recoverable(ht->arBuckets,
(ht->nTableSize<<1)*sizeof(Bucket*), ht->persistent);
if (t){
HANDLE_BLOCK_INTERRUPTIONS();
ht->arBuckets = t;
ht->nTableSize = (ht->nTableSize<<1);
ht->nTableMask = ht->nTableSize-1;
zend_hash_rehash(ht);
HANDLE_UNBLOCK_INTERRUPTIONS();
return SUCCESS;
}
return FAILURE;
}
return SUCCESS;
}
从上面的代码中我们可以看出,HashTable在分配空间的时候,新分配的空间等于原有空间的2倍。
这个函数是用来初始化HashTable的,我们先看一下代码:
ZEND_API int_zend_hash_init(HashTable*ht, uint nSize,hash_func_t pHashFunction,dtor_func_t pDestructor,zend_bool persistent ZEND_FILE_LINE_DC)
{
uint i = 3; //HashTable的大小默认无2的3次方
Bucket **tmp;
SET_INCONSISTENT(HT_OK);
if (nSize>=0x80000000){
ht->nTableSize = 0x80000000;
} else{
while ((1U << i)< nSize){
i++;
}
ht->nTableSize = 1 << i;
}
ht->nTableMask = ht->nTableSize-1;
ht->pDestructor = pDestructor;
ht->arBuckets =NULL;
ht->pListHead =NULL;
ht->pListTail =NULL;
ht->nNumOfElements = 0;
ht->nNextFreeElement = 0;
ht->pInternalPointer = NULL;
ht->persistent = persistent;
ht->nApplyCount =0;
ht->bApplyProtection = 1;
/* Uses ecalloc() so that Bucket* == NULL */
if (persistent){
tmp = (Bucket **)calloc(ht->nTableSize,sizeof(Bucket*));
if (!tmp){
return FAILURE;
}
ht->arBuckets = tmp;
} else{
tmp = (Bucket **)ecalloc_rel(ht->nTableSize,sizeof(Bucket*));
if (tmp){
ht->arBuckets = tmp;
}
}
return SUCCESS;
}
可以看出,HashTable的大小被初始化为2的n次方,另外我们看到有两种内存方式,一种是calloc,一种是ecalloc_rel,这两中内存分配方式我们细讲了,有兴趣的话大家可以自己查一查。
这个函数向HashTable中添加或者修改元素信息
ZEND_API int_zend_hash_add_or_update(HashTable*ht,constchar*arKey, uintnKeyLength,void*pData, uintnDataSize,void**pDest,int flag ZEND_FILE_LINE_DC)
{
ulong h;
uint nIndex;
Bucket *p;
IS_CONSISTENT(ht);
if (nKeyLength<=0){
#if ZEND_DEBUG
ZEND_PUTS("zend_hash_update: Can't put inempty key\n");
#endif
return FAILURE;
}
h = zend_inline_hash_func(arKey, nKeyLength);
nIndex = h & ht->nTableMask;
p = ht->arBuckets[nIndex];
while (p!=NULL){
if ((p->h== h)&&(p->nKeyLength==nKeyLength)){
if (!memcmp(p->arKey, arKey,nKeyLength)){
if (flag &HASH_ADD){
return FAILURE;
}
HANDLE_BLOCK_INTERRUPTIONS();
#if ZEND_DEBUG
if (p->pData == pData){
ZEND_PUTS("Fatalerror in zend_hash_update: p->pData == pData\n");
HANDLE_UNBLOCK_INTERRUPTIONS();
return FAILURE;
}
#endif
if (ht->pDestructor){
ht->pDestructor(p->pData);
}
UPDATE_DATA(ht, p, pData,nDataSize);
if (pDest) {
*pDest = p->pData;
}
HANDLE_UNBLOCK_INTERRUPTIONS();
return SUCCESS;
}
}
p = p->pNext;
}
p = (Bucket*)pemalloc(sizeof(Bucket)-1+nKeyLength, ht->persistent);
if (!p){
return FAILURE;
}
memcpy(p->arKey, arKey, nKeyLength);
p->nKeyLength = nKeyLength;
INIT_DATA(ht, p, pData, nDataSize);
p->h = h;
CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]);
if (pDest){
*pDest = p->pData;
}
HANDLE_BLOCK_INTERRUPTIONS();
CONNECT_TO_GLOBAL_DLLIST(p, ht);
ht->arBuckets[nIndex]= p;
HANDLE_UNBLOCK_INTERRUPTIONS();
ht->nNumOfElements++;
ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* Ifthe Hash table is full, resize it */
return SUCCESS;
}
#define CONNECT_TO_BUCKET_DLLIST(element, list_head) \
(element)->pNext= (list_head); \
(element)->pLast= NULL; \
if((element)->pNext) { \
(element)->pNext->pLast= (element); \
}
这个宏是将bucket加入到bucket链表中
我们只是简单的介绍一下HashTable,如果你想细致的了解HashTable的话,建议您看看php的源代码,关于HashTable的代码在Zend/zend_hash.h 和Zend/zend_hash.c中。
zend_hash_add_empty_element 给函数增加一个空元素
zend_hash_del_key_or_index 根据索引删除元素
zend_hash_reverse_apply 反向遍历HashTable
zend_hash_copy 拷贝
_zend_hash_merge 合并
zend_hash_find 字符串索引方式查找
zend_hash_index_find 数值索引方法查找
zend_hash_quick_find 上面两个函数的封装
zend_hash_exists 是否存在索引
zend_hash_index_exists 是否存在索引
zend_hash_quick_exists 上面两个方法的封装
11.4 C扩展常用HashTable函数
虽然HashTable看起来有点复杂,但是使用却是很方便的,我们可以用下面的函数对HashTable进行初始化和赋值。
2005 年地方院校招生人数
PHP语法 | C语法 | 意义 |
$arr = array() | array_init(arr); | 初始化数组 |
$arr[] = NULL; | add_next_index_null(arr); |
|
$arr[] = 42; | add_next_index_long(arr, 42); |
|
$arr[] = true; | add_next_index_bool(arr, 1); |
|
$arr[] = 3.14; | add_next_index_double(3.14); |
|
$arr[] = ‘foo’; | add_next_index_string(arr, “foo”, 1); | 1的意思进行字符串拷贝 |
$arr[] = $myvar; | add_next_index_zval(arr, myvar); |
|
$arr[0] = NULL; | add_index_null(arr, 0); |
|
$arr[1] = 42; | add_index_long(arr, 1, 42); |
|
$arr[2] = true; | add_index_bool(arr, 2, 1); |
|
$arr[3] = 3.14; | add_index_double(arr, 3, 3,14); |
|
$arr[4] = ‘foo’; | add_index_string(arr, 4, “foo”, 1); |
|
$arr[5] = $myvar; | add_index_zval(arr, 5, myvar); |
|
$arr[“abc”] = NULL; | add_assoc_null(arr, “abc”); |
|
$arr[“def”] = 711; | add_assoc_long(arr, “def”, 711); |
|
$arr[“ghi”] = true; | add_assoc_bool(arr, ghi”, 1); |
|
$arr[“jkl”] = 1.44; | add_assoc_double(arr, “jkl”, 1.44); |
|
$arr[“mno”] = ‘baz’; | add_assoc_string(arr, “mno”, “baz”, 1); |
|
$arr[‘pqr’] = $myvar; | add_assoc_zval(arr, “pqr”, myvar); |
|
说了这么多,我们实验一下。
任务:返回一个数组,数组中的数据如下:
Array
(
[0] => for test
[42] => 123
[for test. for test.] => 1
[array] => Array
(
[0] => 3.34
)
)
代码实现:
PHP_FUNCTION(test)
{
zval* t;
array_init(return_value);
add_next_index_string(return_value,"for test",1);
add_index_long(return_value,42,123);
add_assoc_double(return_value,"for test. for test.",1.0);
ALLOC_INIT_ZVAL(t);
array_init(t);
add_next_index_double(t,3.34);
add_assoc_zval(return_value,"array", t);
}
很简单吧,还记得return_value吗?
呵呵,有问题联系我吧。不多说了 iamnew<niujiaming0819@gmail.com>或者QQ947847775
前面我们讲到了在C扩展中的变量,函数,使用数组等,也给大家介绍了HashTable和数组的实现。本节我们接触一个php中另外一个常见的类型:资源。前面我们讲到zval,其实资源也是zval描述的。如果我们用zval描述资源需要向php注册其底层的数据结构。
我们现在头文件中定义一个结构体,然后声明一个常量用来表示资源的名字。这部分的代码如下:
typedef struct__person
{
char *name;
long name_len;
long age;
} Person;
#definePHP_PERSON_RES_NAME "Person"
PHP_FUNCTION(test_res);
我们首先声明一个结构体,用来保存资源数据,有生命了一个常量PHP_PERSON_RES_NAME用来表示资源的名称,宏PHP_FUNCTION想必不需要介绍了,我们用这个宏定义一个测试函数。
编辑我们的.c文件,在ZEND_DECLARE_MODULE_GLOBALS语句前面添加一个真正的全局整数。
static int le_resource;
在PHP扩展中,只有很少几个地方需要声明全局变脸个,目录入口标识符(List entry identifiers)(le_*)是其中之一。这些值只是由一个查找表用来吧资源类型和他们的文本名字和析构方法相关联,他们不需要是线程安全的。你的扩展将在MINIT步骤为其导出的每个资源生成一个唯一的编号。现在把它加入你的扩展,在PHP_MINIT_FUNCTION(resource)的顶部放入下面的代码:
PHP_MINIT_FUNCTION(resource)
{
le_resource = zend_register_list_destructors_ex(NULL,NULL,
PHP_PERSON_RES_NAME, module_number);
return SUCCESS;
}
到目前为止我们已经想系统内部注册了资源,剩下了就是资源的接收、返回、使用了。
本小结的任务很简单,就是初始化资源Person。谈到资源的初始化,无非就是申请内存,赋值,无他。详细我们看看代码。
PHP_FUNCTION(test_res)
{
Person *p;
char *name;
int name_len;
long age;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"sl",&name,&name_len,&age)== FAILURE)
{
RETURN_FALSE;
}
if(name_len<1)
{
php_error_docref(NULLTSRMLS_CC,E_WARNING,"No name given.");
RETURN_FALSE;
}
if(age<0|| age>255)
{
php_error_docref(NULLTSRMLS_CC,E_WARNING,"age not illgel.");
RETURN_FALSE;
}
p = emalloc(sizeof(Person));
p->name = estrndup(name, name_len);
p->name_len = name_len;
p->age = age;
ZEND_REGISTER_RESOURCE(return_value, p, le_resource);
}
上面的代码,我们是在test_res函数中实现的。这段代码的意思是:接收两个参数name和age,其中用name初始化结构体Person中的name,用age初始化结构体Person中的age,用name的长度初始化结构体Person中的name_len。空间申请和赋值操纵完成之后我们通过ZEND_REGISTER_RESOURCE宏将资源赋值给return_value返回。(还记得return_value是什么东西吗?)
上面一节我们讲到了怎么样初始化资源,并且讲到了如何将资源作为函数的返回值返回。本节我们介绍一下函数如何将资源作为参数接收。接收资源的代码如下:
PHP_FUNCTION(test_res_param)
{
Person *p;
zval *zp;
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"r",&zp)== FAILURE)
{
RETURN_FALSE;
}
ZEND_FETCH_RESOURCE(p, Person*,&zp,-1, PHP_PERSON_RES_NAME, le_resource);
php_printf("Hello ");
PHPWRITE(p->name, p->name_len);
php_printf("! age: %d\n", p->age);
RETURN_TRUE;
}
本节暂不更新
本节暂不更新
本节暂不更新
本节暂不更新