php调用C代码的方法详解和zend_parse_parameters函数详解

php调用C代码的方法详解

在php程序中需要用到C代码,应该是下面两种情况:

1 已有C代码,在php程序中想直接用
2 由于php的性能问题,需要用C来实现部分功能
 
针对第一种情况,最合适的方法是用system调用,把现有C代码写成一个独立的程序。参数通过命令行或者标准输入传入,结果从标准输出读出。其次,稍麻烦一点的方法是C代码写成一个daemon,php程序用socket来和它进行通讯。
 
重点讲讲第二种情况,虽然沿用system调用的方法也可以,但是想想你的目的是优化性能,那么频繁的起这么多进程,当然会让性能下降。而写daemon的方法固然可行,可是繁琐了很多。
 
我的简单测试,同样一个算法,用C来写比用php效率能提高500倍。而用php扩展的方式,也能提高90多倍(其中的性能损失在了参数传递上了吧,我猜)。
 
所以有些时候php扩展就是我们的最佳选择了。
 
这里我着重介绍一下用C写php扩展的方法,而且不需要重新编译php。
 
首先,找到一个php的源码,php4或者php5版本的都可以,与你目标平台的php版本没有关系。
 
在源码的ext目录下可以找到名为ext_skel的脚本(windows平台使用ext_skel_win32.php)
在这个目录下执行./ext_skel --extname=hello(我用hello作为例子)
这时生成了一个目录 hello,目录下有几个文件,你只需要关心这三个:config.m4 hello.c php_hello.h
 
把这个目录拷备到任何你希望的地方,cd进去,依次执行
(安装phpize等工具 yum -y install php-devel )
phpize
./configure
make
什么也没发生,对吧?
这是因为漏了一步,打开config.m4,找到下面
dnl If your extension references something external, use with:
...
dnl Otherwise use enable:
...
这是让你选择你的扩展使用with还是enable,我们用with吧。把with那一部分取消注释。
如果你和我一样使用vim编辑器,你就会很容易发现dnl三个字母原来是表示注释的呀(这是因为vim默认带了各种文件格式的语法着色包)
 
我们修改了config.m4后,继续
phpize
./configure
make
这时,modules下面会生成hello.so和hello.la文件。一个是动态库,一个是静态库。
 
你的php扩展已经做好了,尽管它还没有实现你要的功能,我先说说怎么使用这个扩展吧!ext_skel为你生成了一个hello.php里面有调用示例,但是那个例子需要你把hello.so拷贝到php的扩展目录中去,我们只想实现自己的功能,不想打造山寨版php,改用我下面的方法来加载吧:
  1. if(!extension_loaded("hello")) {
  2. dl_local("hello.so");
  3. }
  4. function dl_local( $extensionFile ) {
  5. //make sure that we are ABLE to load libraries
  6. if( !(bool)ini_get( "enable_dl" ) || (bool)ini_get( "safe_mode" ) ) {
  7. die( "dh_local(): Loading extensions is not permitted./n" );
  8. }
  9. //check to make sure the file exists
  10. if( !file_exists(dirname(__FILE__) . "/". $extensionFile ) ) {
  11. die( "dl_local(): File '$extensionFile' does not exist./n" );
  12. }
  13. //check the file permissions
  14. if( !is_executable(dirname(__FILE__) . "/". $extensionFile ) ) {
  15. die( "dl_local(): File '$extensionFile' is not executable./n" );
  16. }
  17. //we figure out the path
  18. $currentDir = dirname(__FILE__) . "/";
  19. $currentExtPath = ini_get( "extension_dir" );
  20. $subDirs = preg_match_all( "////" , $currentExtPath , $matches );
  21. unset( $matches );
  22. //lets make sure we extracted a valid extension path
  23. if( !(bool)$subDirs ) {
  24. die( "dl_local(): Could not determine a valid extension path [extension_dir]./n" );
  25. }
  26. $extPathLastChar = strlen( $currentExtPath ) - 1;
  27. if( $extPathLastChar == strrpos( $currentExtPath , "/" ) ) {
  28. $subDirs--;
  29. }
  30. $backDirStr = "";
  31. for( $i = 1; $i <= $subDirs; $i++ ) {
  32. $backDirStr .= "..";
  33. if( $i != $subDirs ) {
  34. $backDirStr .= "/";
  35. }
  36. }
  37. //construct the final path to load
  38. $finalExtPath = $backDirStr . $currentDir . $extensionFile;
  39. //now we execute dl() to actually load the module
  40. if( !dl( $finalExtPath ) ) {
  41. die();
  42. }
  43. //if the module was loaded correctly, we must bow grab the module name
  44. $loadedExtensions = get_loaded_extensions();
  45. $thisExtName = $loadedExtensions[ sizeof( $loadedExtensions ) - 1 ];
  46. //lastly, we return the extension name
  47. return $thisExtName;
  48. }//end dl_local()

这样的好处是你的php扩展可以随你的php代码走,绿色扩展。

 
随后一个让人关心的问题是,如何添加函数、实现参数传递和返回值
 
添加函数步骤如下:
php_hello.h:
PHP_FUNCTION(confirm_hello_compiled);// 括号里面填写函数名
hello.c
zend_function_entry hello_functions[] = {
    PHP_FE(confirm_hello_compiled,  NULL)       /* 这里添加一行 */
    {NULL, NULL, NULL}  /* Must be the last line in hello_functions[] */
};
PHP_FUNCTION(confirm_hello_compiled) 
{// 这里写函数体
}
要实现的函数原型其实都一个样,用宏PHP_FUNCTION来包装了一下,另外呢,在hello_functions里面添加了一行信息,表示你这个模块中有这个函数了。
 
那么都是一样的函数原型,如何区分返回值与参数呢?
我给一个例子:
  1. PHP_FUNCTION(hello_strdiff)
  2. {
  3. char *r1 = NULL, *r2 = NULL;
  4. int n = 0, m = 0;
  5. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &r1, &n, &r2, &m) == FAILURE) {
  6. return;
  7. }
  8. while(n && m && *r1 == *r2) {
  9. r1++;
  10. r2++;
  11. n--;
  12. m--;
  13. }
  14. if(n == 0) RETURN_LONG(m);
  15. if(m == 0) RETURN_LONG(n);
  16. int d[n+1][m+1];
  17. int cost;
  18. int i,j;
  19. for(i = 0; i <= n; i++) d[i][0] = i;
  20. for(j = 0; j <= m; j++) d[0][j] = j;
  21. for(i = 1; i <= n; i++) {
  22. for(j = 1; j <= m; j++) {
  23. if(r1[i-1] == r2[j-1]) cost = 0;
  24. else cost = 1;
  25. int a = MIN(d[i-1][j]+1,d[i][j-1]+1);
  26. a = MIN(a, d[i-1][j-1]+cost);
  27. d[i][j] = a;
  28. }
  29. }
  30. RETURN_LONG(d[n][m]);
  31. }

这是一个求两个字符串差异度的算法,输入参数两个字符串,返回整型。

参数的传递看这里
zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &r1, &n, &r2, &m)
把这个当成是scanf来理解好了。
类型说明见下表:
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*

如果想实现可选参数的话,例如一个字符串,一个浮点,再加一个可选的bool型,可以用"sd|b"来表示。

和scanf有一点不同的是,对于字符串,你要提供两个变量来存储,一个是char *,存字符串的地址,一个int,来存字符串的长度。这样有必要的时候,你可以安全的处理二进制数据。
 
那么返回值怎么办呢?
使用下面一组宏来表示:
RETURN_STRING
RETURN_LONG
RETURN_DOUBLE
RETURN_BOOL
RETURN_NULL
注意RETURN_STRING有两个参数
当你需要复制一份字符串时使用
RETURN_STRING("Hello World", 1);
否则使用
RETURN_STRING(str, 0);
这里涉及到了模块中内存的分配,当你申请的内存需要php程序中去释放的话,请参照如下表
Traditional Non-Persistent Persistent
malloc(count)
calloc(count, num)
emalloc(count)
ecalloc(count, num)
pemalloc(count, 1)*
pecalloc(count, num, 1)
strdup(str)
strndup(str, len)
estrdup(str)
estrndup(str, len)
pestrdup(str, 1)
pemalloc() & memcpy()
free(ptr) efree(ptr) pefree(ptr, 1)
realloc(ptr, newsize) erealloc(ptr, newsize) perealloc(ptr, newsize, 1)
malloc(count * num + extr)** safe_emalloc(count, num, extr) safe_pemalloc(count, num, extr)

一般我们使用Non-Persistent中列出的这些好了。

 
基本上就是这样,可以开始写一个php的扩展了。
从我目前的应用来看,能操纵字符串就够用了,所以我就只能介绍这么多了,如果要详细一点的呢,例如php数组怎么处理,可以参考

更好的文章:http://www.toplee.com/blog/56.html#pp1

更详细的呢,可以参考php手册中的《Zend API:深入 PHP 内核》一章
不过这些资料都是英文的。
 
 php扩展基础

本节没有介绍关于脚本引擎基本构造的一些知识,而是直接进入扩展的编码讲解中,因此不要担心你无法立刻获得对扩展整体把握的感觉。假设你正在开发一个网站,需要一个把字符串重复n次的函数。下面是用PHP写的例子:

function self_concat($string, $n)

{

$result = "";

for ($i = 0; $i < $n; $i++) {

$result .= $string;

}

return $result;

}

self_concat("One", 3) returns "OneOneOne".

self_concat("One", 1) returns "One".

假设由于一些奇怪的原因,你需要时常调用这个函数,而且还要传给函数很长的字符串和大值n。这意味着在脚本里有相当巨大的字符串连接量和内存重新分配过程,以至显著地降低脚本执行速度。如果有一个函数能够更快地分配大量且足够的内存来存放结果字符串,然后把$string重复n次,就不需要在每次循环迭代中分配内存。

为扩展建立函数的第一步是写一个函数定义文件,该函数定义文件定义了扩展对外提供的函数原形。该例中,定义函数只有一行函数原形self_concat() :

string self_concat(string str, int n)

函数定义文件的一般格式是一个函数一行。你可以定义可选参数和使用大量的PHP类型,包括: bool, float, int, array等。

保存为myfunctions.def文件至PHP原代码目录树下。

该是通过扩展骨架(skeleton)构造器运行函数定义文件的时机了。该构造器脚本叫ext_skel,放在PHP原代码目录树的ext/目录下(PHP原码主目录下的README.EXT_SKEL提供了更多的信息)。假设你把函数定义保存在一个叫做myfunctions.def的文件里,而且你希望把扩展取名为myfunctions,运行下面的命令来建立扩展骨架

./ext_skel --extname=myfunctions --proto=myfunctions.def

这个命令在ext/目录下建立了一个myfunctions/目录。你要做的第一件事情也许就是编译该骨架,以便编写和测试实际的C代码。编译扩展有两种方法:

☞  作为一个可装载模块或者DSO(动态共享对象)

☞  静态编译到PHP

因为第二种方法比较容易上手,所以本章采用静态编译。如果你对编译可装载扩展模块感兴趣,可以阅读PHP原代码根目录下的README.SELF-CONTAINED_EXTENSIONS文件。为了使扩展能够被编译,需要修改扩展目录ext/myfunctions/下的config.m4文件。扩展没有包裹任何外部的C库,你需要添加支持--enable-myfunctions配置开关到PHP编译系统里(–with-extension 开关用于那些需要用户指定相关C库路径的扩展)。可以去掉自动生成的下面两行的注释来开启这个配置。

PHP_ARG_ENABLE(myfunctions, whether to enable myfunctions support,

[ --enable-myfunctions                Include myfunctions support])

现在剩下的事情就是在PHP原代码树根目录下运行./buildconf,该命令会生成一个新的配置脚本。通过查看./configure --help输出信息,可以检查新的配置选项是否被包含到配置文件中。现在,打开你喜好的配置选项开关和--enable-myfunctions重新配置一下PHP。最后的但不是最次要的是,用make来重新编译PHP。

ext_skel应该把两个PHP函数添加到你的扩展骨架了:打算实现的self_concat()函数和用于检测myfunctions 是否编译到PHP的confirm_myfunctions_compiled()函数。完成PHP的扩展开发后,可以把后者去掉。

<?php

print confirm_myfunctions_compiled("myextension");

?>

运行这个脚本会出现类似下面的输出:

"Congratulations! You have successfully modified ext/myfunctions

config.m4. Module myfunctions is now compiled into PHP."

另外,ext_skel脚本生成一个叫myfunctions.php的脚本,你也可以利用它来验证扩展是否被成功地编译到PHP。它会列出该扩展所支持的所有函数。

现在你学会如何编译扩展了,该是真正地研究self_concat()函数的时候了。

下面就是ext_skel脚本生成的骨架结构:

/* {{{ proto string self_concat(string str, int n)

*/

PHP_FUNCTION(self_concat)

}

char *str = NULL;

int argc = ZEND_NUM_ARGS();

int str_len;

long n;

if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)

return;

php_error(E_WARNING, "self_concat: not yet implemented");

}

/* }}} */

 
 
 
 
 
 
zend_parse_parameters 详解

自动生成的PHP函数周围包含了一些注释,这些注释用于自动生成代码文档和vi、Emacs等编辑器的代码折叠。函数自身的定义使用了宏PHP_FUNCTION(),该宏可以生成一个适合于Zend引擎的函数原型。逻辑本身分成语义各部分,取得调用函数的参数和逻辑本身。

为了获得函数传递的参数,可以使用zend_parse_parameters()API函数。下面是该函数的原型:

zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, …);

第一个参数是传递给函数的参数个数。通常的做法是传给它ZEND_NUM_ARGS()。(ZEND_NUM_ARGS() 来表示对传入的参数“有多少要多少”)这是一个表示传递给函数参数总个数的宏。第二个参数是为了线程安全,总是传递TSRMLS_CC宏,后面会讲到。第三个参数是一个字符串,指定了函数期望的参数类型,后面紧跟着需要随参数值更新的变量列表。因为PHP采用松散的变量定义和动态的类型判断,这样做就使得把不同类型的参数转化为期望的类型成为可能。例如,如果用户传递一个整数变量,可函数需要一个浮点数,那么zend_parse_parameters()就会自动地把整数转换为相应的浮点数。如果实际值无法转换成期望类型(比如整形到数组形),会触发一个警告。

下表列出了可能指定的类型。我们从完整性考虑也列出了一些没有讨论到的类型。

类型指定符

对应的C类型

描述

l

long

符号整数

d

double

浮点数

s

char *, int

二进制字符串,长度

b

zend_bool

逻辑型(1或0)

r

zval *

资源(文件指针,数据库连接等)

a

zval *

联合数组

o

zval *

任何类型的对象

O

zval *

指定类型的对象。需要提供目标对象的类类型

z

zval *

无任何操作的zval

为了容易地理解最后几个选项的含义,你需要知道zval是Zend引擎的值容器[1]。无论这个变量是布尔型,字符串型或者其他任何类型,其信息总会包含在一个zval联合体中。本章中我们不直接存取zval,而是通过一些附加的宏来操作。下面的是或多或少在C中的zval, 以便我们能更好地理解接下来的代码。

typedef union _zval {

long lval;

double dval;

struct {

char *val;

int len;

} str;

HashTable *ht;

zend_object_value obj;

} zval;

在我们的例子中,我们用基本类型调用zend_parse_parameters(),以本地C类型的方式取得函数参数的值,而不是用zval容器。

为了让zend_parse_parameters()能够改变传递给它的参数的值,并返回这个改变值,需要传递一个引用。仔细查看一下self_concat():

if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)

return;

注意到自动生成的代码会检测函数的返回值FAILUER(成功即SUCCESS)来判断是否成功。如果没有成功则立即返回,并且由zend_parse_parameters()负责触发警告信息。因为函数打算接收一个字符串l和一个整数n,所以指定 ”sl” 作为其类型指示符。s需要两个参数,所以我们传递参考char * 和 int (str 和 str_len)给zend_parse_parameters()函数。无论什么时候,记得总是在代码中使用字符串长度str_len来确保函数工作在二进制安全的环境中。不要使用strlen()和strcpy(),除非你不介意函数在二进制字符串下不能工作。二进制字符串是包含有nulls的字符串。二进制格式包括图象文件,压缩文件,可执行文件和更多的其他文件。”l” 只需要一个参数,所以我们传递给它n的引用。尽管为了清晰起见,骨架脚本生成的C变量名与在函数原型定义文件中的参数名一样;这样做不是必须的,尽管在实践中鼓励这样做。

回到转换规则中来。下面三个对self_concat()函数的调用使str, str_len和n得到同样的值:

self_concat("321", 5);

self_concat(321, "5");

self_concat("321", "5");

str points to the string "321", str_len equals 3, and n equals 5.

str 指向字符串"321",str_len等于3,n等于5。

在我们编写代码来实现连接字符串返回给PHP的函数前,还得谈谈两个重要的话题:内存管理、从PHP内部返回函数值所使用的API!!

内存管理

用于从堆中分配内存的PHP API几乎和标准C API一样。在编写扩展的时候,使用下面与C对应(因此不必再解释)的API函数:

emalloc(size_t size);

efree(void *ptr);

ecalloc(size_t nmemb, size_t size);

erealloc(void *ptr, size_t size);

estrdup(const char *s);

estrndup(const char *s, unsigned int length);

在这一点上,任何一位有经验的C程序员应该象这样思考一下:“什么?标准C没有strndup()?”是的,这是正确的,因为GNU扩展通常在Linux下可用。estrndup()只是PHP下的一个特殊函数。它的行为与estrdup()相似,但是可以指定字符串重复的次数(不需要结束空字符),同时是二进制安全的。这是推荐使用estrndup()而不是estrdup()的原因。

在几乎所有的情况下,你应该使用这些内存分配函数。有一些情况,即扩展需要分配在请求中永久存在的内存,从而不得不使用malloc(),但是除非你知道你在做什么,你应该始终使用以上的函数。如果没有使用这些内存函数,而相反使用标准C函数分配的内存返回给脚本引擎,那么PHP会崩溃。

这些函数的优点是:任何分配的内存在偶然情况下如果没有被释放,则会在页面请求的最后被释放。因此,真正的内存泄漏不会产生。然而,不要依赖这一机制,从调试和性能两个原因来考虑,应当确保释放应该释放的内存。剩下的优点是在多线程环境下性能的提高,调试模式下检测内存错误等。

还有一个重要的原因,你不需要检查这些内存分配函数的返回值是否为null。当内存分配失败,它们会发出E_ERROR错误,从而决不会返回到扩展。

 

PHP函数中返回值

 

扩展API包含丰富的用于从函数中返回值的宏。这些宏有两种主要风格:第一种是RETVAL_type()形式,它设置了返回值但C代码继续执行。这通常使用在把控制交给脚本引擎前还希望做的一些清理工作的时候使用,然后再使用C的返回声明 ”return” 返回到PHP;后一个宏更加普遍,其形式是RETURN_type(),他设置了返回类型,同时返回控制到PHP。下表解释了大多数存在的宏。

设置返回值并且结束函数

设置返回值

宏返回类型和参数

RETURN_LONG(l)

RETVAL_LONG(l)

整数

RETURN_BOOL(b)

RETVAL_BOOL(b)

布尔数(1或0)

RETURN_NULL()

RETVAL_NULL()

NULL

RETURN_DOUBLE(d)

RETVAL_DOUBLE(d)

浮点数

RETURN_STRING(s, dup)

RETVAL_STRING(s, dup)

字符串。如果dup为1,引擎会调用estrdup()重复s,使用拷贝。如果dup为0,就使用s

RETURN_STRINGL(s, l, dup)

RETVAL_STRINGL(s, l, dup)

长度为l的字符串值。与上一个宏一样,但因为s的长度被指定,所以速度更快。

RETURN_TRUE

RETVAL_TRUE

返回布尔值true。注意到这个宏没有括号。

RETURN_FALSE

RETVAL_FALSE

返回布尔值false。注意到这个宏没有括号。

RETURN_RESOURCE(r)

RETVAL_RESOURCE(r)

资源句柄。

上一篇:Javascript 二维码生成库:QRCode


下一篇:二维码生成库phpqrcode使用小结