PHP扩展开发完整教程(上)

第一章 PHP的生命周期

  1. php的运行方式有两种:
  1. 以模块的方式加载到web-server里去,与web-server一块解析被请求的php脚本
  2. 以fastcgi模式运行php的时候,一般需要手动启动php服务
  1. cgi是指通用网关接口,分别为每一个请求创建一个进程,fastcgi是使用持续的进程来处理请求
  2. php有多种sapi,一般最常用的是cli模式
  3. php程序的启动可以看作有两个概念上的启动,终止也有两个概念上的终止:一种是当php作为apache的一个模块的启动与终止,这次启动会初始化一些数据,与宿主有关的常驻内存的数据,终止与之相对,还有一个概念上的启动就是当apache分配一个请求过来的时候,php会有一次启动与终止;
  4. 当php随着apache诞生在内存中的时候,他会把所有已加载扩展的MINIT方法全执行一遍,在这过程中,扩展可以自定义一些自己的常量,类,资源等所有会被用户端php脚本用到的东西,这里定义的东西会随着apache常驻内存,会被所有请求使用,直到apache卸载掉php模块
  5. php内置了PHP_MINIT_FUNCTION宏函数,来实现这个功能
int time_of_minit;
PHP_MINIT_FUNCTION(walu)
{
	time_of_minit = time(NULL);
	return SUCCESS;//返回SUCCESS代表正常,返回FALIURE就不会加载这个扩展了
}
  1. 当一个请求到来的时候,php会开辟一个新的环境,并重新扫描自己的各个扩展,遍历执行它们的RINIT方法,这时候一个扩展可能会初始化在本次请求中用到的变量等,还会初始化用户端的变量等,内核预置了PHP_RINIT_FUNCTION()这个宏函数来帮我们实现这个功能
int time_of_rinit;//在RINIT里初始化,看看每次页面请求的时候是否变化。
PHP_RINIT_FUNCTION(walu)
{
	time_of_rinit=time(NULL);
	return SUCCESS;
}
  1. 当php请求执行到最后,php会启动回收程序, 它这次会执行所有已加载扩展的RSHUTDOWN方法,会释放掉这次请求用过的所有变量,内存等,内核预置了PHP_RSHUTDOWN_FUNCTION宏函数来帮助我们实现这个功能
PHP_RSHUTDOWN_FUNCTION(walu)
{
	FILE *fp=fopen("time_rshutdown.txt","a+");
	fprintf(fp,"%ld\n",time(NULL));//让我们看看是不是每次请求结束都会在这个文件里追加数据
	fclose(fp);
	return SUCCESS;
}
  1. 当Apache通知PHP自己要Stop的时候,PHP便进入MSHUTDOWN(俗称Module Shutdown)阶段, php会把所有扩展的MSHUTDOWN方法执行一次,然后把自己申请的内存全部释放,然后结束程序,内核中预置了PHP_MSHUTDOWN_FUNCTION宏函数来帮助我们实现这个功能
PHP_MSHUTDOWN_FUNCTION(walu)
{
	FILE *fp=fopen("time_mshutdown.txt","a+");
	fprintf(fp,"%ld\n",time(NULL));
	return SUCCESS;
}
  1. cli和cgi的sapi是相当特殊的,因为php的生命周期在一个请求里完成,多进程模式的php比如fastcgi模式,会先执行minit方法,然后分别针对单个请求执行rinit和rshutdown方法,直到进程结束才会执行mshutdown
  2. 线程安全 --------- 略

第二章 PHP变量在内核中的实现

  1. 所有的编程语言都要提供一种数据的存储与检索,php的变量类型是随时可以发生改变的
struct _zval_struct {
    zvalue_value value; /* 变量的值 */
    zend_uint refcount__gc;//引用计数
    zend_uchar type;    /* 变量当前的数据类型 */
    zend_uchar is_ref__gc;//是否是引用类型
};
typedef struct _zval_struct zval;
  1. 在zval基础上,php内部实现了8种变量类型,分别是 IS_NULL, IS_BOOL, IS_TRUE, IS_FALSE, IS_LONG, IS_DOUBLE, IS_STRING, IS_ARRAY, IS_OBJECT, IS_RESOURCE
  2. 通过Z_TYPE函数来判断变量的类型
void display_value(zval zv,zval *zv_p,zval **zv_pp)
{
	if( Z_TYPE(zv) == IS_NULL )
	{
		php_printf("类型是 IS_NULL!\n");
	}
	
	if( Z_TYPE_P(zv_p) == IS_LONG )
	{
		php_printf("类型是 IS_LONG,值是:%ld" , Z_LVAL_P(zv_p));
	}
	
	if(Z_TYPE_PP(zv_pp) == IS_DOUBLE )
	{
		php_printf("类型是 IS_DOUBLE,值是:%f" , Z_DVAL_PP(zv_pp) );
	}
}	
  1. string型变量比较特殊,因为内核在保存String型变量时,不仅保存了字符串的值,还保存了它的长度, 所以它有对应的两种宏组合STRVAL和STRLEN
void display_string(zval *zstr)
{
    if (Z_TYPE_P(zstr) != IS_STRING) {
        php_printf("这个变量不是字符串!\n");
        return;
    }
    PHPWRITE(Z_STRVAL_P(zstr), Z_STRLEN_P(zstr));
    //这里用了PHPWRITE宏,只要知道它是从Z_STRVAL_P(zstr)地址开始,输出Z_STRLEN_P(zstr)长度的字符就可以了。
}
  1. Array型变量的值其实是存储在C语言实现的HashTable中的, 我们可以用ARRVAL组合宏(Z_ARRVAL, Z_ARRVAL_P, Z_ARRVAL_PP)这三个宏来访问数组的值
  2. 对象是一个复杂的结构体(zend_object_value结构体),不仅存储属性的定义、属性的值,还存储着访问权限、方法等信息。 内核中定义了以下组合宏让我们方便的操作对象: OBJ_HANDLE:返回handle标识符, OBJ_HT:handle表, OBJCE:类定义, OBJPROP:HashTable的属性, OBJ_HANDLER:在OBJ_HT中操作一个特殊的handler方法
  3. 变量的创建与赋值
  zval zv;//这与PHP5中创建变量有很大的不同 PHP7中删除了ALLOC_ZVAL、ALLOC_INIT_ZVAL、MAKE_STD_ZVAL、INIT_PZVAL等
  ZVAL_LONG(&zv, 0);
  ZVAL_NULL(&zv);
  ZVAL_BOOL(&zv, true);
  ZVAL_DOUBLE(&zv, 10.5);
  ZVAL_STRINGL(&zv,"1234",5);
  1. 那就是用户在PHP中定义的变量我们都可以在一个HashTable中找到, 当PHP中定义了一个变量,内核会自动的把它的信息储存到一个用HashTable实现的符号表里,
  2. 全局作用域的符号表是在调用扩展的RINIT方法(一般都是MINIT方法里)前创建的,并在RSHUTDOWN方法执行后自动销毁。
    10.当用户在PHP中调用一个函数或者类的方法时,内核会创建一个新的符号表并激活之, 这也就是为什么我们无法在函数中使用在函数外定义的变量的原因 (因为它们分属两个符号表,一个当前作用域的,一个全局作用域的)。 如果不是在一个函数里,则全局作用域的符号表处于激活状态。
  3. 其中的 symbol_table元素可以通过EG宏来访问,它代表着PHP的全局变量,如$GLOBALS,其实从根本上来讲, $GLOBALS不过是EG(symbol_table)的一层封装而已。下面的active_symbol_table元素也可以通过EG(active_symbol_table)的方法来访问,它代表的是处于当前作用域的变量符号表。
struct _zend_executor_globals {
    ...
    HashTable symbol_table;
    HashTable *active_symbol_table;
    ...
};		
  1. 创建一个变量,并把它的值设置为’bar’
<?php
	$foo = 'bar';
?>

php5内核实现

zval *fooval;//声明一个zval变量

MAKE_STD_ZVAL(fooval);//申请内存
ZVAL_STRING(fooval, "bar", 1);//设置值为'bar'
ZEND_SET_SYMBOL( EG(active_symbol_table) ,  "foo" , fooval);//加入当前作用域符号表,设置label为foo

php7内核实现

{
    zval fooval;
    ZVAL_STRING(&fooval, "bar" );
    //原来的active_symbol_table已经不存在了
    zend_set_local_var_str("foo", sizeof("foo")-1, &fooval, 0);
}	
  1. 通过zend_hash_find()函数来找到当前某个作用域下用户已经定义好的变量
	zval **fooval;

    if (zend_hash_find(
    		&EG(active_symbol_table), //这个参数是地址,如果我们操作全局作用域,则需要&EG(symbol_table)
    		"foo",
    		sizeof("foo"),
    		(void**)&fooval
    	) == SUCCESS
    )
    {
        php_printf("成功发现$foo!");
    }
    else
    {
        php_printf("当前作用域下无法发现$foo.");
    }
  1. 内核中提供了好多函数专门来帮我们实现类型转换的功能,你需要的只是调用一个函数而已。这一类函数有一个统一的形式:convert_to_*()
ZEND_API void convert_to_long(zval *op);
ZEND_API void convert_to_double(zval *op);
ZEND_API void convert_to_null(zval *op);
ZEND_API void convert_to_boolean(zval *op);
ZEND_API void convert_to_array(zval *op);
ZEND_API void convert_to_object(zval *op);

ZEND_API void _convert_to_string(zval *op ZEND_FILE_LINE_DC);
#define convert_to_string(op) if ((op)->type != IS_STRING) { _convert_to_string((op) ZEND_FILE_LINE_CC); }

第三章 内存管理

  1. c语言里定义的字符串是常量,修改会报段错误
  2. php的函数名是大小写不敏感的,function tables里保存的都是小写字母
  3. 一个请求结束时,它能够执行与OS在一个进程终止时相同的行为。也就是说,它会隐式地释放所有的为该请求所占用的内存
  4. 内存分配防止溢出函数
void *safe_emalloc(size_t size, size_t count, size_t addtl);
void *safe_pemalloc(size_t size, size_t count, size_t addtl, char persistent);
  1. 当一个变量被第一次创建的时候,它对应的zval结构体的refcount__gc成员的值会被初始化为1,理由很简单,因为只有这个变量自己在用它,但是当你把这个变量赋值给别的变量时,refcount__gc属性便会加1变成2,因为现在有两个变量在用这个zval结构了
<?php
$a = 'Hello World';
$b = $a;
unset($a);	

以上php对应以下c程序

zval *helloval;
MAKE_STD_ZVAL(helloval);
ZVAL_STRING(helloval, "Hello World", 1);
zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);
ZVAL_ADDREF(helloval); //这句很特殊,我们显式的增加了helloval结构体的refcount
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval, sizeof(zval*), NULL);	

这个时候当我们再用unset删除$a的时候,它删除符号表里的$a的信息,然后清理它的值部分,这时它发现$a的值对应的zval结构的refcount值是2,也就是有另外一个变量在一起用着这个zval,所以unset只需把这个zval的refcount减去1就行了!
6. 写时复制机制,只有在变量发生变化的时候才会复制一份出来

$a = 1;
$b = $a;
$b += 5;

我们希望语句执行后 a 仍 然 是 1 , 而 a仍然是1,而 a1b则需要变成6。我们知道在第二句完成后内核通过让 a 和 a和 ab共享一个zval结构来达到节省内存的目的,但是现在第三句来了,这时KaTeX parse error: Expected group after '_' at position 39: …,内核首先查看refcount_̲_gc属性,如果它大于1则为这…b的zval来,并改变其值。

zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC)
{
	zval **varval, *varcopy;
	if (zend_hash_find(EG(active_symbol_table),varname, varname_len + 1, (void**)&varval) == FAILURE)
	{
		/* 如果在符号表里找不到这个变量则直接return */
		return NULL;
	}

	if ((*varval)->refcount < 2)
	{	
		//如果这个变量的zval部分的refcount小于2,代表没有别的变量在用,return
		return *varval;
	}
	
	/* 否则,复制一份zval*的值 */
	MAKE_STD_ZVAL(varcopy);
	varcopy = *varval;
	
	/* 复制任何在zval*内已分配的结构*/
	zval_copy_ctor(varcopy);

	/* 从符号表中删除原来的变量
	 * 这将减少该过程中varval的refcount的值
	 */
	zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);

	/* 初始化新的zval的refcount,并在符号表中重新添加此变量信息,并将其值与我们的新zval相关联。*/
	varcopy->refcount = 1;
	varcopy->is_ref = 0;
	zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,&varcopy, sizeof(zval*), NULL);
	
	/* 返回新zval的地址 */
	return varcopy;
} 

现在$b变量拥有了自己的zval,并且可以自由的修改它的值了。
7. 如果用户在PHP脚本中显式的让一个变量引用另一个变量时,我们的内核是如何处理的呢?

	$a = 1;
	$b = &$a;
	$b += 5; 

我们都知道$a的值也变成6了。当我们更改$b的值时,内核发现$b是$a的一个用户端引用,也就是所它可以直接改变$b对应的zval的值,而无需再为它生成一个新的不同与$a的zval。因为他知道$a和$b都想得到这次变化!
它是通过zval的is_ref__gc成员来获取这些信息的。这个成员只有两个值,就像开关的开与关一样。它的这两个状态代表着它是否是一个用户在PHP语言中定义的引用
在第一条语句($a = 1;)执行完毕后,$a对应的zval的refcount__gc等于1,is_ref__gc等于0;。
当第二条语句执行后($b = &$a;),refcount__gc属性向往常一样增长为2,而且is_ref__gc属性也同时变为了1! 最后,在执行第三条语句的时候,内核再次检查$b的zval以确定是否需要复制出一份新的zval结构来,这次不需要复制,因为我们刚才上面的get_var_and_separate函数其实是个简化版,并且少写了一个条件:

/* 如果这个zval在php语言中是通过引用的形式存在的,或者它的refcount小于2,则不需要复制。*/
if ((*varval)->is_ref || (*varval)->refcount < 2) {
	return *varval;
}   

这一次,尽管它的refcount等于2,但是因为它的is_ref等于1,所以也不会被复制。内核会直接的修改这个zval的值。
8. 如果复制和引用这两个事件被组合起来使用了该怎么办呢?

	$a = 1;
	$b = $a;
	$c = &$a;

在这种情况下,变量的值必须分离成两份完全独立的存在!$a与$c共用一个zval,$b自己用一个zval,尽管他们拥有同样的值,但是必须至少通过两个zval来实现


第四章 动手编译php

  1. 在编译php时,开启–enable-debug 会激活调试模式。它将激活PHP源码中几个非常关键的函数,最典型的功能便是在每一个请求结束后给出这一次请求中内存的泄漏情况.
cd /php-7.1.0
./configure --prefix=~/php/ --enable-debug --enable-maintainer-zts
make
make test
make clean //自愿执行,非必须

第五章 编写第一个扩展

  1. 在任意目录下创建一个目录,名字为扩展的名字(walu), 然后在walu目录下创建一个config.m4文件,并输入以下内容
/*php5 */
PHP_ARG_ENABLE(walu,
    [Whether to enable the "walu" extension],
    [  enable-walu        Enable "walu" extension support])

if test $PHP_WALU != "no"; then
    PHP_SUBST(WALU_SHARED_LIBADD)
    PHP_NEW_EXTENSION(walu, walu.c, $ext_shared)
fi

/* php7 */
 PHP_ARG_ENABLE(walu, whether to enable walu support,
 Make sure that the comment is aligned:
 [  --enable-walu           Enable walu support])
 
 if test "$PHP_WALU" != "no"; then
  PHP_NEW_EXTENSION(walu, walu.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi

PHP_ARG_ENABLE函数有三个参数,第一个参数是我们的扩展名(注意不用加引号),第二个参数是当我们运行./configure脚本时显示的内容,最后一个参数则是我们在调用./configure --help时显示的帮助信息
如果我们的扩展使用了多个文件,便可以将这多个文件名罗列在函数的参数里

//$ext_shared参数用来声明这个扩展不是一个静态模块,而是在php运行时动态加载的
PHP_NEW_EXTENSION(sample, sample.c sample2.c sample3.c, $ext_shared)
  1. 下面,我们来编写实现扩展主逻辑的源文件walu.c
//加载config.h,如果配置了的话
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

//加载php头文件
#include "php.h"


#define phpext_walu_ptr &walu_module_entry

//module entry
zend_module_entry walu_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
     STANDARD_MODULE_HEADER,
#endif
    "walu", //这个地方是扩展名称,往往我们会在这个地方使用一个宏。
    NULL, /* Functions */
    NULL, /* MINIT */
    NULL, /* MSHUTDOWN */
    NULL, /* RINIT */
    NULL, /* RSHUTDOWN */
    NULL, /* MINFO */
#if ZEND_MODULE_API_NO >= 20010901
    "2.1", //这个地方是我们扩展的版本
#endif
    STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_WALU
ZEND_GET_MODULE(walu)
#endif
  1. 编译扩展, 第一步:我们需要根据config.m4文件生成一个configure脚本、Makefile等文件,这一步有phpize来帮我们做
$ phpize
PHP Api Version: 20041225
Zend Module Api No: 20050617
Zend Extension Api No: 220050617
  1. 现在查看一下我们扩展所在的目录,会发现多了许多文件。phpize程序根据config.m4里的信息生成了许多编译php扩展必须的文件,比如生成makefiles等,这为我们省了很多的麻烦。 接下来我们运行./configure脚本, 接下来就像我们安装其它程序一样执行make; make test;即可,如果没有错误,那么在module文件夹下面便会生成我们的目标文件 —— walu.so。
  2. 让php运行时自动加载我们的扩展。那就是在php.ini里这样配置
extension_dir=/usr/local/lib/php/modules/
extension=walu.so

只要我们把walu.so这个文件放置在extension_dir配置的目录下,php就会在每次启动的时候自动加载了
6. 静态编译:没有被编译成so或者dll文件供PHP动态调用,而是直接和PHP主程序编译到一起。
7. 先让我们执行一下PHP源码根目录下的./configure --help命令。会发现输出信息并没有包含我们的扩展,这是因为这个configure脚本生成的时候,我们的扩展还没有编写呢。需要重新生成一分configure文件 即可:

./buildconf --force

现在当我们再执行./configure --help的时候,便会发现walu扩展的信息已经出现了。现在我们只需要重新走一遍PHP的编译过程,便可以把我们的扩展以静态编译的方式加入到PHP主程序中了。哦,千万不要忘记使用–enable-walu参数开启我们的扩展
8. ZEND_FUNCTION()宏函数也可以写成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

其中,zif是zend internal function的意思,zif_前缀是可供PHP语言调用的函数在C语言中的函数名称前缀。

ZEND_FUNCTION(walu_hello)
{
    php_printf("Hello World!\n");
}

上面定义了一个函数,在C语言中展开后应该是这样的:

void zif_walu_hello(INTERNAL_FUNCTION_PARAMETERS)
{
    php_printf("Hello World!\n");
}
  1. walu.c修改如下:
ZEND_FUNCTION(walu_hello)
{
    php_printf("Hello World!\n");
}

static zend_function_entry walu_functions[] = {
    ZEND_FE(walu_hello,        NULL)
    //别名
    ZEND_NAMED_FE(walu_hi,	ZEND_FN(walu_hello),	NULL)
    //另一种别名写法
    /*
#define ZEND_FALIAS(name, alias, arg_info)			ZEND_FENTRY(name, ZEND_FN(alias), arg_info, 0)
	*/
    ZEND_FALIAS(walu_hi2,walu_hello,	NULL)
    { NULL, NULL, NULL }
};

zend_module_entry walu_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
     STANDARD_MODULE_HEADER,
#endif
    "walu", //这个地方是扩展名称,往往我们会在这个地方使用一个宏。
    walu_functions, /* Functions */
    NULL, /* MINIT */
    NULL, /* MSHUTDOWN */
    NULL, /* RINIT */
    NULL, /* RSHUTDOWN */
    NULL, /* MINFO */
#if ZEND_MODULE_API_NO >= 20010901
    "2.1", //这个地方是我们扩展的版本
#endif
    STANDARD_MODULE_PROPERTIES
};

第六章 函数返回值

ZEND_FUNCTION(sample_long_wrong)
{
    zval *retval;

    MAKE_STD_ZVAL(retval);
    ZVAL_LONG(retval, 42);

    return retval;
}
  1. 上面的写法是无效,与其让扩展开发员每次都初始化一个zval并return之,zend引擎早就准备好了一个更好的方法。它在每个zif函数声明里加了一个zval*类型的形参,名为return_value,专门来解决返回值这个问题
  2. 展开代表参数声明的INTERNAL_FUNCTION_PARAMETERS宏
#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC

zval *return_value,我们在函数内部修改这个指针,函数执行完成后,内核将把这个指针指向的zval返回给用户端的函数调用者。
zval *this_ptr,如果此函数是一个类的方法,那么这个指针的含义和PHP语言中$this变量差不多。
int return_value_used,代表用户端在调用此函数时有没有使用到它的返回值。
3. 下面让我们先试验一个非常简单的例子,我先给出PHP语言中的实现

<?php
function sample_long()
{
	return 42;
}

下面是我们在编写扩展时的实现

ZEND_FUNCTION(sample_long)
{
    ZVAL_LONG(return_value, 42);
    return;
}

需要注意的是,ZEND_FUNCTION本身并没有通过return关键字返回任何有价值的东西,它只不过是在运行时修改了return_value指针所指向的变量的值而已,而内核则会把return_value指向的变量作为用户端调用此函数后的得到的返回值。
我们千万不要自己去修改return_value的is_ref__gc和refcount__gc属性,这两个属性的值会由PHP内核自动管理。
4. 与return_value有关的宏

#define RETVAL_RESOURCE(l)				ZVAL_RESOURCE(return_value, l)
#define RETVAL_BOOL(b)					ZVAL_BOOL(return_value, b)
#define RETVAL_NULL() 					ZVAL_NULL(return_value)
#define RETVAL_LONG(l) 					ZVAL_LONG(return_value, l)
#define RETVAL_DOUBLE(d) 				ZVAL_DOUBLE(return_value, d)
#define RETVAL_STRING(s, duplicate) 		ZVAL_STRING(return_value, s, duplicate)
#define RETVAL_STRINGL(s, l, duplicate) 	ZVAL_STRINGL(return_value, s, l, duplicate)
#define RETVAL_EMPTY_STRING() 			ZVAL_EMPTY_STRING(return_value)
#define RETVAL_ZVAL(zv, copy, dtor)		ZVAL_ZVAL(return_value, zv, copy, dtor)
#define RETVAL_FALSE  					ZVAL_BOOL(return_value, 0)
#define RETVAL_TRUE   					ZVAL_BOOL(return_value, 1)

#define RETURN_RESOURCE(l) 				{ RETVAL_RESOURCE(l); return; }
#define RETURN_BOOL(b) 					{ RETVAL_BOOL(b); return; }
#define RETURN_NULL() 					{ RETVAL_NULL(); return;}
#define RETURN_LONG(l) 					{ RETVAL_LONG(l); return; }
#define RETURN_DOUBLE(d) 				{ RETVAL_DOUBLE(d); return; }
#define RETURN_STRING(s, duplicate) 	{ RETVAL_STRING(s, duplicate); return; }
#define RETURN_STRINGL(s, l, duplicate) { RETVAL_STRINGL(s, l, duplicate); return; }
#define RETURN_EMPTY_STRING() 			{ RETVAL_EMPTY_STRING(); return; }
#define RETURN_ZVAL(zv, copy, dtor)		{ RETVAL_ZVAL(zv, copy, dtor); return; }
#define RETURN_FALSE  					{ RETVAL_FALSE; return; }
#define RETURN_TRUE   					{ RETVAL_TRUE; return; }
  1. 其实,zend internal function的形参中还有一个比较常用的名为return_value_used的参数,它是干嘛使的呢?它用来标志这个函数的返回值在用户端有没有用到。
<?php 
function sample_array_range() {
    $ret = array();
    for($i = 0; $i < 1000; $i++) {
        $ret[] = $i;
    }
    return $ret;
}
sample_array_range();

sample_array_range()仅仅是执行了一下而已,并没有使用到函数的返回值。函数的返回值$ret初始化并返回给调用者后根本就没有发挥作用,却白白浪费了很多内存来存储它的1000个元素,
如果返回值没有被用到,我有没有办法在函数中提前知晓并进行一些有利于性能的操作呢?我们所需要做的便是充分利用return_value_used这个参数

ZEND_FUNCTION(sample_array_range)
{
    if (return_value_used) {
        int i;
        
        //把返回值初始化成一个PHP语言中的数组
        array_init(return_value);
        for(i = 0; i < 1000; i++)
        {
            //向retrun_value里不断的添加新元素,值为i
            add_next_index_long(return_value, i);
        }
        return;
    }
    else
    {
        //抛出一个E_NOTICE级错误
        php_error_docref(NULL TSRMLS_CC, E_NOTICE,"猫了个咪的,我就知道你没用我的劳动成果!");
        RETURN_NULL();
    }
}
  1. 以引用的形式返回值
<?php
//关于PHP语言中引用形式返回值的详述,请参考PHP手册。
$a = 'china';

function &return_by_ref()
{
	global $a;
	return $a;
}

$b = &return_by_ref();
$b = "php";
echo $a;
//此时程序输出php

内核实现

#if (PHP_MAJOR_VERSION > 5) || (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION > 0)
ZEND_FUNCTION(return_by_ref)
{
	zval **a_ptr;
	zval *a;
	
	//检查全局作用域中是否有$a这个变量,如果没有则添加一个
	//在内核中真的是可以胡作非为啊,:-)
	if(zend_hash_find(&EG(symbol_table) , "a",sizeof("a"),(void **)&a_ptr ) == SUCCESS )
	{
		a = *a_ptr;
	}
	else
	{
		ALLOC_INIT_ZVAL(a);
        zend_hash_add(&EG(symbol_table), "a", sizeof("a"), &a,sizeof(zval*), NULL);
	}
	
	//废弃return_value,使用return_value_ptr来接替它的工作
	zval_ptr_dtor(return_value_ptr);
	if( !a->is_ref__gc && a->refcount__gc > 1 )
	{
		zval *tmp;
		MAKE_STD_ZVAL(tmp);
		*tmp = *a;
		zval_copy_ctor(tmp);
		tmp->is_ref__gc = 0;
		tmp->refcount__gc = 1;
		zend_hash_update(&EG(symbol_table), "a", sizeof("a"), &tmp,sizeof(zval*), NULL);
		a = tmp;
	}
	a->is_ref__gc = 1;
	a->refcount__gc++;
	*return_value_ptr = a;
}
#endif /* PHP >= 5.1.0 */

return_value_ptr是定义zend internal function时的另外一个重要参数,他是一个zval**类型的指针,并且指向函数的返回值。我们调用zval_ptr_dtor()函数后,默认的return_value便被废弃了。
这里的$a变量如果是与某个非引用形式的变量共用一个zval的话,便要进行分离
如果你编译上面的代码,使用的时候便会得到一个段错误。为了使它能够正常的工作,需要在源文件中加一些东西:

#if (PHP_MAJOR_VERSION > 5) || (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION > 0)
    ZEND_BEGIN_ARG_INFO_EX(return_by_ref_arginfo, 0, 1, 0)
    ZEND_END_ARG_INFO ()
#endif /* PHP >= 5.1.0 */

然后使用下面的代码来申明我们的定义的函数:
#if (PHP_MAJOR_VERSION > 5) || (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION > 0)
    ZEND_FE(return_by_ref, return_by_ref_arginfo)
#endif /* PHP >= 5.1.0 */

arginfo是一种特殊的结构体,用来提前向内核告知此函数具有的一些特定的性质,如本例,其将告诉内核本函数需要引用形式的返回值,所以内核不再通过return_value来获取执行结果,而是通过return_value_ptr。如果没有arginfo,那内核会预先把return_value_ptr置为NULL,当我们对其调用zval_ptr_dtor()函数时便会使程序崩溃。
7. 运行时传递引用:Call-time Pass-by-ref

<?php
function byref_calltime($a) {
    $a = '(modified by ref!)';
}

$foo = 'I am a string';

//使用&传递引用
byref_calltime(&$foo);
echo $foo;
//输出'(modified by ref!)'

内核实现

ZEND_FUNCTION(byref_calltime)
{
	zval *a;

	//将我们接收的参数传给zval *a;
	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &a) == FAILURE)
	{
		RETURN_NULL();
	}
	
	//如果a不是以引用的方式传递的。
	if (!a->is_ref__gc)
	{
        	return;
	}
	
	//将a转成字符串
	convert_to_string(a);
	
	//更改数据
	ZVAL_STRING(a," (modified by ref!)",1);
	return;
}
  1. 编译时的传递引用Compile-time Pass-by-ref,
<?php
在定义函数参数的时候加了引用符
function byref_compiletime(&$a) {
    $a = ' (modified by ref!)';
}
$foo = 'I am a string';

//这个地方我们没有加&引用符
byref_compiletime($foo);
echo $foo;
//输出 (modified by ref!)

内核实现:我们需要提前为它定义一个arginfo结构体来向内核通知此函数的这个特定行为。添加此函数到module_entry里需要这样:

ZEND_FE(byref_compiletime, byref_compiletime_arginfo)

byref_compiletime_arginfo是一个arginfo结构体

#ifdef ZEND_ENGINE_2
    ZEND_BEGIN_ARG_INFO(byref_compiletime_arginfo, 0)
        ZEND_ARG_PASS_INFO(1)
    ZEND_END_ARG_INFO()
#else /* ZE 1 */
static unsigned char byref_compiletime_arginfo[] =   { 1, BYREF_FORCE };
#endif

我们copy一份ZEND_FUNCTION(byref_calltime)的实现,并重名成ZEND_FUNCTION(byref_compiletime)就行了。或者直接弄个ZEND_FALIAS就行了:

ZEND_FALIAS(byref_compiletime,byref_calltime,byref_compiletime_arginfo)
如果用PHP不能再满足你的需求,最好的办法就是开发PHP扩展。这有一些好处: 1、增加自己的特殊功能。 2、保护自己的专利代码。 这是几年前的一篇英文文章,现在已被翻译成中文版的。 作者应该是hshq_cn。 链接是:http://bbs3.chinaunix.net/thread-1028798-1-1.html。 现我将此转变为PDF文件,仅有兴趣者参阅。同时非常感谢 原作者及hshq_cn,给我们带来的这么好的资料。里面还有一个幻灯片的,也是很有帮助的文档。另外,再提供一篇相关的文章(http://www.programbbs.com/doc/4083.htm): 编写php的extension实例的方法 所属类别:JSP 推荐指数:★★☆ 文档人气:161 本周人气:1 发布日期:2008-7-3 一、说明 前端时间因为客户的原因折腾了一下asp的扩展,在ATL的帮助下写一个asp的模块还是很容易的。不巧的时刚刚折腾完asp的COM就碰到另一个客户的问题。客户想给系统集成ICBC的接口,但是用ICBC的接口需要用他们的提供的库函数去 1. sign对发送的数据进行签名 2. getcertid获取用户证书的版本 3. verifySign对签名后的数据进行验证 问题是ICBC只给了现成的COM组件,意味在只能在Win的主机上使用。俺们公司只有linux的主机,在*nix上就要自己想办法调用ICBC给的静态库了。对此我们有两个想法 1.用ICBC的静态库做一个独立的执行文件,用PHP的系统调用函数来执行这个独立的执行文件 2.将ICBC的静态库做出一个PHP扩展 方法一应该比较简单,但是远不如方法二的灵活。搞成PHP扩展,只要服务器编译一次,服务器上的所有客户都可以用的。 有ASP的前科,俺觉得搞个PHP的也不是什么难事。操起google搜了一通,结果发现Zend已经写了一个如何编写php extension的教程: http://devzone.zend.com/article/1021-Extension-Writing-Part-I-Introduction-to-PHP-and-Zend 浏览完牛人的大作,更是信心十足,php扩展其实很简单,分七步走: 1. 制作编译配置文件:config.m4 2. 执行phpize生成扩展的框架 3. 在生成的php_xxx.h中声明自己写的函数 4. 在xxx.c中实现自己的函数 5. 编译自己的扩展 6. 将生成的xxx.so拷贝到php.ini中指定的extensions_dir 7. 在php.ini中打开xxx.so的调用 此问题问题唯一搞的地方就是在config.m4中折腾出正确的Makefile,因为Zend的教程中没有提到,俺自己也折腾了好久,才搞出来。 二、实际操作 1.建立工作环境 将php源码包解开,我的版本的php-4.4.4,转到源码包中的ext目录建立一个新的目录叫icbc,然后在icbc目录下touch三个文件config.m4、php_icbc.h、icbc.c 2.建立config.m4 内容如下: PHP_ARG_ENABLE(icbc, whether to enable ICBC support, [ --enable-icbc Enable ICBC support]) if test \"$PHP_ICBC\" = \"yes\"; then AC_DEFINE(HAVE_ICBC, 1, [Whether you have ICBC]) if test -f ./libicbcapi.a; then PHP_ADD_LIBRARY_WITH_PATH(icbcapi, ./, ICBCAPI_SHARED_LIBADD) PHP_SUBST(ICBCAPI_SHARED_LIBADD) AC_MSG_RESULT(checking for libicbcapi.a is OK) else AC_MSG_RESULT(libicbcapi.a not found) AC_MSG_ERROR(Please make sure the libicbcapi.a is in current directory) [Page] fi PHP_NEW_EXTENSION(icbc, icbc.c, $ext_shared) fi 第三行判断是否要启用icbc扩展, 第五行判断ICBC的静态库是否在当前目录(phpdir/ext/icbc)下 第六、七行将ICBC的静态库加入到编译环境中 3.在php_icbc.h中声明我们要导出的函数icbc_sign、icbc_vsign、icbc_getCertID #ifndef PHP_ICBC_H #define PHP_ICBC_H extern zend_module_entry icbc_module_entry; #define phpext_icbc_ptr &icbc_module_entry #ifdef PHP_WIN32 #define PHP_ICBC_API __declspec(dllexport) #else #define PHP_ICBC_API #endif #ifdef ZTS #include \"TSRM.h\" #endif PHP_MINIT_FUNCTION(icbc); PHP_MSHUTDOWN_FUNCTION(icbc); PHP_RINIT_FUNCTION(icbc); PHP_RSHUTDOWN_FUNCTION(icbc); PHP_MINFO_FUNCTION(icbc); /*Modify youself here*/ PHP_FUNCTION(icbc_sign); PHP_FUNCTION(icbc_vsign); PHP_FUNCTION(icbc_getCertID); /****End of Self control section***/ #ifdef ZTS #define ICBC_G(v) TSRMG(icbc_globals_id, zend_icbc_globals *, v) #else #define ICBC_G(v) (icbc_globals.v) #endif #endif /* PHP_ICBC_H */ 涉及到我们也就 PHP_FUNCTION(icbc_sign); PHP_FUNCTION(icbc_vsign); PHP_FUNCTION(icbc_getCertID); 其他的都是PHP各个状态的入口函数声明 4.编写这三个函数的实现: #ifdef HAVE_CONFIG_H #include \"config.h\" #endif #include \"php.h\" #include \"php_ini.h\" #include \"ext/standard/info.h\" #include \"php_icbc.h\" #include \"icbcapi.h\" static int le_icbc; zend_function_entry icbc_functions[] = { PHP_FE(icbc_sign,NULL) PHP_FE(icbc_vsign,NULL) PHP_FE(icbc_getCertID,NULL) {NULL, NULL, NULL} /* Must be the last line in icbc_functions[] */ }; zend_module_entry icbc_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif \"icbc\", icbc_functions, PHP_MINIT(icbc), PHP_MSHUTDOWN(icbc), PHP_RINIT(icbc), /* Replace with NULL if there’s nothing to do at request start */ PHP_RSHUTDOWN(icbc), /* Replace with NULL if there’s nothing to do at request end */ [Page] PHP_MINFO(icbc), #if ZEND_MODULE_API_NO >= 20010901 \"0.1\", /* Replace with version number for your extension */ #endif STANDARD_MODULE_PROPERTIES }; #ifdef COMPILE_DL_ICBC ZEND_GET_MODULE(icbc) #endif PHP_MINIT_FUNCTION(icbc) { return SUCCESS; } PHP_MSHUTDOWN_FUNCTION(icbc) { return SUCCESS; } PHP_RINIT_FUNCTION(icbc) { return SUCCESS; } PHP_RSHUTDOWN_FUNCTION(icbc) { return SUCCESS; } PHP_MINFO_FUNCTION(icbc) { php_info_print_table_start(); php_info_print_table_header(2, \"icbc support\", \"enabled\"); php_info_print_table_end(); } PHP_FUNCTION(icbc_sign) { char* src; int srclen; char* pkey; int keylen; char* keypass; int keypasslen; char* signedbuf; int signedbuflen; FILE* fp; char key[2000]; int rcc; if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,\"sss\",&src,&srclen,&pkey,&keylen,&keypass,&keypasslen) == FAILURE){ return; } fp = fopen(pkey,\"rb\"); if(fp == NULL) { return; } fseek(fp,2,SEEK_SET); fread((void*)key,609,1,fp); fclose(fp); if(rcc = sign(src,srclen,key,607,keypass,&signedbuf,&signedbuflen) >= 0){ base64enc(signedbuf,signedbuflen,&signedbuf,&signedbuflen); src = estrndup(signedbuf,signedbuflen); if(signedbuf != NULL) infosec_free(signedbuf); RETURN_STRING(src,1); [Page] }else{ RETURN_LONG(rcc); } }PHP_FUNCTION(icbc_vsign) { char* src; int srclen; char* cert; int certlen; char* vsignedbuf; int vsignedbuflen; FILE* fp; char vcert[2000]; int rcc; if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,\"sss\",&src,&srclen,&cert,&certlen,&vsignedbuf,&vsignedbuflen) == FAILURE){ return; } fp = fopen(cert,\"rb\"); if(fp == NULL) { return; } fread((void*)vcert,1525,1,fp); fclose(fp); base64dec(vsignedbuf,vsignedbuflen,&vsignedbuf,&vsignedbuflen); if(rcc = verifySign(src,srclen,vcert,1525,vsignedbuf,vsignedbuflen) >= 0){ if(vsignedbuf != NULL) infosec_free(vsignedbuf); RETURN_TRUE; }else{ if(vsignedbuf != NULL) infosec_free(vsignedbuf); RETURN_LONG(rcc); } } PHP_FUNCTION(icbc_getCertID) { char* arg; char* certid; int arg_len,certidlen; FILE* fp; char cert[2000]; if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,\"s\", &arg,&arg_len) == FAILURE){ return; } fp = fopen(arg,\"rb\"); if(fp == NULL) { return; } fread((void*)cert,1525,1,fp); fclose(fp); [Page] if(!getCertID(cert,1525,&certid,&certidlen)) { arg = estrndup(certid,certidlen); if(certid != NULL) infosec_free(certid); RETURN_STRING(arg,1); }else{ return; } } 先在zend_function_entry icbc_functions[]数组中放入我们的要实现的函数名,然后是一堆php各个状态入口函数,详情请看Zend的教程。最后是在PHP_FUNCTION宏定义中放我们声明函数的具体实现。具体实现时难点也就是参数的传入和结果传出,还好PHP已经为我们做了很好的抽象。在Zend的教程中也有详尽的说明,俺就不啰嗦了。关键代码照搬icbc的test.c就行了。 5.编译安装我们的库 先将ICBC的头文件考到当前目录,重命名为icbcapi.php,将静态库也cp过来,重命名为*nix的标准形式libicbcapi.a,然后运行 phpize 生成configure,运行 ./configure --enable-icbc 生成Makefile,这里有一个很搞的地方,在生成的Makefile中最后一句中指定ICBC静态库的地方错了,正确的应该是(红色标记地方): $(LIBTOOL) --mode=link $(CC) $(COMMON_FLAGS) $(CFLAGS_CLEAN) $(EXTRA_CFLAGS) $(LDFLAGS) -o $@ -export-dynamic -avoid-version -prefer-pic -module -rpath $(phplibdir) $(EXTRA_LDFLAGS) $(shared_objects_icbc) $(ICBCAPI_SHARED_LIBADD) 改好Makefile后就可以执行 make 如果一切顺利的话会在modules中得到我们的icbc.so,将我们的icbc.so拷贝到/usr/local/lib/php/extensions目录下,然后在php.ini中确认extensions_dir的值是/usr/local/lib/php/extensions,然后加入这句话 extension=icbc.so 重启apache后,就可以在php中直接调用这三个函数了 6.测试程序,要将测试的证书和key文件放到php测试文件的当前目录 <?php $realpath = dirname(__FILE__); $key = $realpath.\"/user.key\"; $cert = $realpath.\"/user.crt\"; $src = \"zzz\"; $passwd = \"12345678\"; echo \"The Cert file information is \"; echo icbc_getCertID($cert); echo \"<br>\"; $b64sdata = icbc_sign($src,$key,$passwd); echo \"The string \".$src.\" encrypt by icbc api is \".$b64sdata.\"(base64 encoded)<br>\"; echo \"Now we check it weather is correct....<br>\"; if(icbc_vsign(\"zzz\",$cert,$b64sdata)){ echo \"The signtrue to \".$src.\" is right!!<br>\"; [Page] }else{ echo \"The signtrue to \".$src.\" is wrong!!<br>\"; exit(); } ?>
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值