(1159)
(126)
(350)
(56)
(91)
(182)
(193)
(138)
(23)
分类: php
2015-11-04 11:00:23
有时候,单纯依靠 php “本身”是不行的。尽管普通用户很少遇到这种情况,但一些专业性的应用则经常需要将 php 的性能发挥到极致(这里的性能是指速度或功能)。由于受到 php 语言本身的限制,同时还可能不得不把庞大的库文件包含到每个脚本当中。因此,某些新功能并不是总能被顺利实现,所以我们必须另外寻找一些方法来克服 php 的这些缺点。
了解到了这一点,我们就到了应该接触一下 php 的心脏并探究一下它的内核——可以编译成 php 并让之工作的 c 代码——的时候了。
php调用动态链接库几个必要步骤为:
1. c/c 编写动态链接库,编译打包成.so文件
2. 初始化一个新的php扩展
3. 配置、编写php扩展内容,在扩展中使用c/c 调用.so
4. 编译并添加php扩展
5. 在php应用中直接调用php扩展里暴露出来的api
为了从能运行的最简单的例子开始,所以下面的叙述可能不会严格按照上面列的步骤来写,也有可能会重复穿插着写。但总体顺序是一致的。
如果还不会用c 调用自己写的.so库,请参考我的这篇文章:
文中从什么是name mangling开始,到如何用调用一个包含简单函数的so库,再到如何从so中加载类,都有详细叙述,并附有可运行示例代码。
本文中,我们将创建一个叫“vehicle”的php扩展,其中包含一个“car”类。
将要创建的扩展会涉及到以下文件需要修改,这些文件将会出现在vehicle目录下。后面会一一叙述这些文件是怎么来的,现在只是大致了解一下。
1. 需要php源码包
如果你不是通过源码包方式安装的php,而是通过apt-get install 安装的,那么首先确保你安装了 php-devel 包,然后需要下载php源代码,然后可以跳到第2步。
如果想从源码包编译安装php,可以参考我写的另外一篇文章《linux(ubuntu12.10)搭建php开发环境(源码包方式)》,有详细的每一步的介绍。地址为:
安装完成以后,跳到第2步。
2. 制作php外部扩展
去到php源码包目录的ext目录下,我的在
/usr/local/src/php-5.3.22/ext
然后输入命令
sudo ./ext_skel --extname=vehicle
该命令会在ext目录下新建一个vehicle目录,并创建新模块“vehicle”目前所需的所有文件,包括
config.m4, php_vehicle.h, vehicle.php, credits等。
还会列出应该执行哪些后续步骤来使用新的扩展,具体如下图所示。
图中我一共使用了三个命令
$ pwd 显示我的php源码包的ext目录所在位置
$ sudo ./ext_skel --extname=vehicle 前面已解释
$ ls 列出了执行上一条命令以后,在vehicle目录下,为我们创建的文件及目录。
如上图所示,在它的指导步骤1~8中,大致了解到我们需要编译config.m4文件,然后需要运行配置,然后需要使用make命令编译等。现在不用知道具体都干些什么,接下来会分别叙述。
1. 配置php构建系统——编写config.m4
首先我们需要明确的是,为了在php扩展中使用c ,那么必须通知php构建系统使用c 编译器
通过在config.m4 文件中添加宏php_require_cxx()来实现。
使用c 的过程中,肯定会用到c 的一些库,至少需要标准库(libstdc 大多系统都是这样的)
通过在config.m4 文件中添加宏php_add_library()来实现。
将下面的代码添加到config.m4中
php_arg_enable(vehicle, [whether to enable the "vehicle" extension], [ --enable-vehicle enable "vehicle" extension support]) if test $php_vehicle != "no"; then php_require_cxx() php_subst(vehicle_shared_libadd) php_add_library(stdc , 1, vehicle_shared_libadd) php_new_extension(vehicle, vehicle.cc car.cc, $ext_shared) fi
即去掉config.m4文件中某些行前面的注释符号“dnl”,然后添加我们需要的配置。如果还不清楚,可以参考我的配置文件,内容如下图
这里的宏php_subst()是标准autoconf的ac_subst()宏的php修改版, 它在将扩展构建为共享模块时需要。
php_new_extension宏中,第一个参数代表模块的名称;第二个参数是需要编译的文件,用空格隔开;第三个参数跟宏php_subst()是一样的。
2. 编写头文件php_vehicle.h
该头文件应该包含以下内容,其中php_vehicle_extname “vehicle” 代表该扩展的名称,php_vehicle_extver则代表版本号,这些都会在php_info()中显示出来。
#ifndef php_vehicle_h #define php_vehicle_h #define php_vehicle_extname "vehicle" #define php_vehicle_extver "1.0" #ifdef have_config_h #include "config.h" #endif extern "c" { #include "php.h" } extern zend_module_entry vehicle_module_entry; #define phpext_vehicle_ptr &vehicle_module_entry; php_minit_function(vehicle); php_msutdown_function(vehicle); php_rinit_function(vehicle); php_rshutdown_function(vehicle); php_minfo_function(vehicle); #endif /* php_vehicle_h */
我的php_vehicle.h文件内容如下图所示。
要理解php_minit_function()函数,就需要了解php的启动步骤。大体就是
1
2
3
4
5
|
php_minit_function(extension_name) {
/* initialize functions, classes etc */
}
|
以上是php启动的第一步。为了先跑通整个流程,这里就不在一一叙述每一个方法以及宏的作用了。
3. 编写需要编译的文件(vehicle.cc、car.cc)
使扩展能够运行的最基本的vehicle.cc框架应该包含以下内容
#include "php_vehicle.h" php_minit_function(vehicle) { return success; } zend_module_entry vehicle_module_entry = { #if zend_module_api_no >= 20010901 standard_module_header, #endif php_vehicle_extname, null, /* functions */ php_minit(vehicle), null, /* mshutdown */ null, /* rinit */ null, /* rshutdown */ null, /* minfo */ #if zend_module_api_no >= 20010901 php_vehicle_extver, #endif standard_module_properties }; #ifdef compile_dl_vehicle extern "c" { zend_get_module(vehicle) } #endif
我的vehicle.cc文件如下图所示
有了这个文件,我们还不能能创建最基本的php扩展,因为前面我们在配置文件config.m4中指定了需要编译的还有car.cc文件,所以必须给出,即使现在里面什么也没有。
所以还需要新建一个car.cc文件,暂时让它空着。
4. 配置、编译、安装
在当前文件目录(即/usr/local/src/php-5.3.22/ext/vehicle)下执行命令
$ sudo /usr/local/php/bin/phpize
$ sudo ./configure --enable-vehicle --with-php-config=/usr/local/php/bin/php-config
如下图所示
然后执行make & make install 命令
$ sudo make
$sudo make install
安装完成以后,会看到在
/usr/local/php/lib/php/extensions/no-debug-zts-20090626/
目录下有一个新生成的so文件,名为vehicle.so,将库该文件的路径添加到php.ini配置文件中,如下图所示。
即添加这句话
“extension="/usr/local/php/lib/php/extensions/no-debug-zts-20090626/vehicle.so"
然后重启你的apache,在phpinfo()中就可以看到已经成功启动扩展vehicle了,如下图所示。
至此,最基本的框架已经搭起来了。接下来要做的工作就是如何使用php调用c/c 编写的so文件。先从简单的直接调用开始
1. 编写car.h头文件
把头文件与源码文件分开是一个不错的习惯,特别是在别人不想知道你的具体实现的时候。这里我们也采取这种方式。
#ifndef vehicle_car_h #define vehicle_car_h // a very simple car class class car { public: car(int maxgear); void shift(int gear); void accelerate(); void brake(); int getcurrentspeed(); int getcurrentgear(); private: int maxgear; int currentgear; int speed; }; #endif /* vehicle_car_h */
如上代码所示,我们先定义一个car类,然后定义几个public的方法以及私有成员变量。然后在car.cc中实现它
#include "car.h" car::car(int maxgear) { this->maxgear = maxgear; this->currentgear = 1; this->speed = 0; } void car::shift(int gear) { if (gear < 1 || gear > maxgear) { return; } currentgear = gear; } void car::accelerate() { speed = (5 * this->getcurrentgear()); } void car::brake() { speed -= (5 * this->getcurrentgear()); } int car::getcurrentspeed() { return speed; } int car::getcurrentgear() { return currentgear; }
现在我们已经定义好了car类,那么如何使其暴露给php用户空间,让php能够调用这些方法呢。
首先你需要定义一个包含有function_entry 表的php类来调用car,这个function_entry 表里就包含了每一个你想暴露给php的c 方法。这里所指的这个php类就是前面提到的vehicle.cc。更新一下vehicle.cc的内容,如 下所示。
#include "php_vehicle.h" zend_class_entry *car_ce; php_method(car, __construct) { } php_method(car, p_shift) { } php_method(car, p_accelerate) { } php_method(car, p_brake) { } php_method(car, p_getcurrentspeed) { } php_method(car, p_getcurrentgear) { } function_entry car_methods[] = { php_me(car, __construct, null, zend_acc_public | zend_acc_ctor) php_me(car, p_shift, null, zend_acc_public) php_me(car, p_accelerate, null, zend_acc_public) php_me(car, p_brake, null, zend_acc_public) php_me(car, p_getcurrentspeed, null, zend_acc_public) php_me(car, p_getcurrentgear, null, zend_acc_public) {null, null, null} }; php_minit_function(vehicle) { zend_class_entry ce; init_class_entry(ce, "car", car_methods); car_ce = zend_register_internal_class(&ce tsrmls_cc); return success; } zend_module_entry vehicle_module_entry = { #if zend_module_api_no >= 20010901 standard_module_header, #endif php_vehicle_extname, null, /* functions */ php_minit(vehicle), /* minit */ null, /* mshutdown */ null, /* rinit */ null, /* rshutdown */ null, /* minfo */ #if zend_module_api_no >= 20010901 php_vehicle_extver, #endif standard_module_properties }; #ifdef compile_dl_vehicle extern "c" { zend_get_module(vehicle) } #endif
可以看到在php_method中的函数名称与car.cc类里写的函数名称并不需要一样,你可以定义任意自己觉得合适的函数名,为了表示方便,我这里一律添加前缀"p_"。这里每一个函数都还没有具体实现。
前面提到的function_entry表在上诉代码中可以看到由很多php_me组成,每一个都代表了想要暴露给php用户空间的方法,最后一定以{null,null,null}表示结束。
现在我们已经有了c 的类,也有了php类,那么如何把两者联系起来呢?
首先你需要做的是定义一个zend_object_hander。然后定义一个结构,该结构包含了这个hander和c 的类。在php5中一个object其实就是一个hander,可以如下定义。
zend_object_handlers car_object_handlers; struct car_object { zend_object std; car *car; };
该结构就会把c 的对象与zend的对象联系起来,然后你需要把下列代码添加到你的vehicle.cc文件中去,在php_method方法之前。
void car_free_storage(void *object tsrmls_dc) { car_object *obj = (car_object *)object; delete obj->car; zend_hash_destroy(obj->std.properties); free_hashtable(obj->std.properties); efree(obj); } zend_object_value car_create_handler(zend_class_entry *type tsrmls_dc) { zval *tmp; zend_object_value retval; car_object *obj = (car_object *)emalloc(sizeof(car_object)); memset(obj, 0, sizeof(car_object)); obj->std.ce = type; alloc_hashtable(obj->std.properties); zend_hash_init(obj->std.properties, 0, null, zval_ptr_dtor, 0); zend_hash_copy(obj->std.properties, &type->default_properties, (copy_ctor_func_t)zval_add_ref, (void *)&tmp, sizeof(zval *)); retval.handle = zend_objects_store_put(obj, null, car_free_storage, null tsrmls_cc); retval.handlers = &car_object_handlers; return retval; }
然后更新一下php_minit_function,这个函数前面提到过,就是php的扩展的模块初始化函数,如下所示:
php_minit_function(vehicle) { zend_class_entry ce; init_class_entry(ce, "car", car_methods); car_ce = zend_register_internal_class(&ce tsrmls_cc); car_ce->create_object = car_create_handler; memcpy(&car_object_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers)); car_object_handlers.clone_obj = null; return success; }
可能这里对“tsrmls_dc”这个宏比较疑惑,它其实是 “ , void ***tsrm_ls”
然后编写一下构造函数
php_method(car, __construct) { long maxgear; car *car = null; zval *object = getthis(); if (zend_parse_parameters(zend_num_args() tsrmls_cc, "l", &maxgear) == failure) { return_null(); } car = new car(maxgear); car_object *obj = (car_object *)zend_object_store_get_object(object tsrmls_cc); obj->car = car; }
接着实现一下前面定义好的,但没有写内容的php_method函数。为了演示的简洁,我们就实现两个函数。
php_method(car, p_accelerate) { car *car; car_object *obj = (car_object *)zend_object_store_get_object( getthis() tsrmls_cc); car = obj->car; if (car != null) { car->accelerate(); } } php_method(car, p_getcurrentspeed) { car *car; car_object *obj = (car_object *)zend_object_store_get_object( getthis() tsrmls_cc); car = obj->car; if (car != null) { return_long(car->getcurrentspeed()); } return_null(); }
记得添加所需要的头文件car.h。
然后重新 make & make install 吧,做完以后重启apache服务器。
编写一个php脚本,去测试是否成功。测试代码如下
1
2
3
4
5
6
7
|
// create a 5 gear car
$car = new car(5);
print $car->p_getcurrentspeed(); // prints '0'
$car->p_accelerate();
print $car->p_getcurrentspeed(); // prints '5'
?>
|
运行结果如果是0 和 5就说明成功了。
自此一个简单的php调用c 类已经实现了。
假设现在我们有一个冒泡排序的.so库文件,现在我们想在php扩展中调用这个库文件里的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
extern "c" void bubble_sort(int *arr, int len)
{
int tmp;
for(int i = 0; i < len - 1; i )
for(int j = i 1; j < len; j )
{
if(arr[i] > arr[j])
{
tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
}
|
1. 更新car.h文件以及car.cc文件,添加调用so的代码,如下图所示。
car.cc 中代码如下图所示。
2. 更新vehicle.cc文件
在function_entry car_methods添加
php_me(car, p_bubble_sort_so, null, zend_acc_public)
然后添加一个php_method(car, p_bubble_sort_so),代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
php_method(car, p_bubble_sort_so)
{
zval *arr;
zval *len;
if (zend_parse_parameters(zend_num_args() tsrmls_cc, "zz", &arr, &len) == failure)
{
return;
}
switch (z_type_p(arr)) {
case is_null:
php_printf("null\n");
break;
case is_bool:
php_printf("boolean: %s\n", z_lval_p(arr)
? "true" : "false");
break;
case is_long:
php_printf("long: %ld\n", z_lval_p(arr));
break;
case is_double:
php_printf("double: %f\n", z_dval_p(arr));
break;
case is_string:
php_printf("string: ");
phpwrite(z_strval_p(arr), z_strlen_p(arr));
php_printf("\n");
break;
case is_resource:
php_printf("resource\n");
break;
case is_array:
php_printf("type is array\n");
break;
case is_object:
php_printf("object\n");
break;
default:
php_printf("unknown\n");
}
hashtable *arr_hash;
hashposition pointer;
int array_count;
zval **data;
arr_hash = z_arrval_p(arr);
array_count = zend_hash_num_elements(arr_hash);
php_printf("the array passed contains %d elements\n",
array_count);
// pass to the so
int *arr_so = (int*)malloc(sizeof(int) * array_count);
int len_so = array_count;
int i= 0;
for(zend_hash_internal_pointer_reset_ex(arr_hash, &pointer);
zend_hash_get_current_data_ex(arr_hash, (void**) &data, &pointer) == success;
zend_hash_move_forward_ex(arr_hash, &pointer))
{
if (z_type_pp(data) == is_long)
{
php_printf("%ld\t%ld\n", z_lval_pp(data),
(**data).value.lval);
arr_so[i ] = z_lval_pp(data);
}
}
car *car;
car_object *obj = (car_object*)zend_object_store_get_object(
getthis() tsrmls_cc);
car = obj->car;
if (car != null)
{
car->bubble_sort(arr_so, len_so);
}
for(i = 0; i < len_so; i )
{
php_printf("%d\n", arr_so[i]);
}
}
|
这段代码还是比较容易懂的,可能会对zval这个变量不熟悉。其实所有用户定义的变量在php中都是用zval类型来表示的,它的内部表示如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
typedef pval zval;
typedef struct _zval_struct zval;
typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct {
char *val;
int len;
} str;
hashtable *ht; /* hash table value */
struct {
zend_class_entry *ce;
hashtable *properties;
} obj;
} zvalue_value;
struct _zval_struct {
/* variable information */
zvalue_value value; /* value */
unsigned char type; /* active type */
unsigned char is_ref;
short refcount;
};
|
然后再看上面那段代码。
switch里面是写给大家的说明程序,为了展示出如何判断传入的参数类型,可以不写略过。下面这句printf代码也是为了向大家展示 z_lval_pp的用法,它其实就是对指针的指针取值,你也可以写成后面一种形式,即使**data的形式。不过建议使用第一种。
php_printf("%ld\t%ld\n", z_lval_pp(data), (**data).value.lval);
接着看代码。首先我们用zend_parse_parameters接收穿过来的参数,这里我们传递的是数组。php中数组在 zval里都是以hash表的形式储存的,所以我们这里声明一个hash表来接收这个参数。通过zend_hash_num_elements()则可以 得到hash表元素的个数,也就是数组的个数。
然后我们将传递过来的参数通过for循环复制给我们的数组,最后通过调用car的bubble_sort函数进行排序。而bubble_sort调用的就是写好的so库函数了。
3. 编译、安装、测试
重新 make & make install,然后写一个php脚本测试一下,可以如下写:
然后运行该php脚本,如果浏览器里出现了排序后的数列0,1,2,3,4,5,则代表成功了。
that's all,
enjoy!