嗨,老铁,欢迎来到我的博客!

如果觉得我的内容还不错的话,可以关注下我在 segmentfault.com 上的直播。我主要从事 PHP 和 Java 方面的开发,《深入 PHP 内核》作者之一。

[视频直播] PHP 进阶之路 - 亿级 pv 网站架构的技术细节与套路 直播中我将毫无保留的分享我这六年的全部工作经验和踩坑的故事,以及会穿插着一些面试中的 考点难点加分点

周梦康 发表于 2016-01-27 4242 次浏览 标签 : phpPHP 扩展开发

PHP 扩展开发的文章,我均已更新至《TIPI》(下面的博文可能已经过时,以 TIPI 上的内容为准)。

安装PHP7

我现在 github 上下的最新版,然后没安装太多的扩展,因为我仅仅是作为开发调试使用嘛~

[root@localhost php-src-php-7.0.3]# ./buildconf
You should not run buildconf in a release package.
use buildconf --force to override this check.
[root@localhost php-src-php-7.0.3]# ./buildconf --force
...
[root@localhost php-src-php-7.0.3]# ./configure --prefix=/usr/local/php7 --with-config-file-path=/usr/local/php7/etc --enable-fpm --with-fpm-user=www --enable-debug
...
Thank you for using PHP.

config.status: creating php7.spec
config.status: creating main/build-defs.h
config.status: creating scripts/phpize
config.status: creating scripts/man1/phpize.1
config.status: creating scripts/php-config
config.status: creating scripts/man1/php-config.1
config.status: creating sapi/cli/php.1
config.status: creating sapi/fpm/php-fpm.conf
config.status: creating sapi/fpm/www.conf
config.status: creating sapi/fpm/init.d.php-fpm
config.status: creating sapi/fpm/php-fpm.service
config.status: creating sapi/fpm/php-fpm.8
config.status: creating sapi/fpm/status.html
config.status: creating sapi/cgi/php-cgi.1
config.status: creating ext/phar/phar.1
config.status: creating ext/phar/phar.phar.1
config.status: creating main/php_config.h
config.status: executing default commands
[root@localhost php-src-php-7.0.3]# make
[root@localhost php-src-php-7.0.3]# make install
Installing shared extensions:     /usr/local/php7/lib/php/extensions/debug-non-zts-20151012/
Installing PHP CLI binary:        /usr/local/php7/bin/
Installing PHP CLI man page:      /usr/local/php7/php/man/man1/
Installing PHP FPM binary:        /usr/local/php7/sbin/
Installing PHP FPM config:        /usr/local/php7/etc/
Installing PHP FPM man page:      /usr/local/php7/php/man/man8/
Installing PHP FPM status page:      /usr/local/php7/php/php/fpm/
Installing phpdbg binary:         /usr/local/php7/bin/
Installing phpdbg man page:       /usr/local/php7/php/man/man1/
Installing PHP CGI binary:        /usr/local/php7/bin/
Installing PHP CGI man page:      /usr/local/php7/php/man/man1/
Installing build environment:     /usr/local/php7/lib/php/build/
Installing header files:          /usr/local/php7/include/php/
Installing helper programs:       /usr/local/php7/bin/
  program: phpize
  program: php-config
Installing man pages:             /usr/local/php7/php/man/man1/
  page: phpize.1
  page: php-config.1
Installing PEAR environment:      /usr/local/php7/lib/php/
--2016-03-02 11:43:32--  https://pear.php.net/install-pear-nozlib.phar
Resolving pear.php.net... 109.203.101.62
Connecting to pear.php.net|109.203.101.62|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3579275 (3.4M) [text/plain]
Saving to: `pear/install-pear-nozlib.phar'

100%[=========================================================================================================================>] 3,579,275    341K/s   in 9.7s    

2016-03-02 11:43:47 (360 KB/s) - `pear/install-pear-nozlib.phar' saved [3579275/3579275]

[PEAR] Archive_Tar    - installed: 1.4.0
[PEAR] Console_Getopt - installed: 1.4.1
[PEAR] Structures_Graph- installed: 1.1.1
[PEAR] XML_Util       - installed: 1.3.0
[PEAR] PEAR           - installed: 1.10.1
Wrote PEAR system config file at: /usr/local/php7/etc/pear.conf
You may want to add: /usr/local/php7/lib/php to your php.ini include_path
/root/Downloads/php-src-php-7.0.3/build/shtool install -c ext/phar/phar.phar /usr/local/php7/bin
ln -s -f phar.phar /usr/local/php7/bin/phar
Installing PDO headers:          /usr/local/php7/include/php/ext/pdo/

添加软链接

ln -s /usr/local/php7/bin/php /usr/bin/php7
ln -s /usr/local/php7/bin/php-config /usr/bin/php7-config 
ln -s /usr/local/php7/bin/phpize /usr/bin/php7ize
ln -s /usr/local/php7/sbin/php-fpm /usr/sbin/php7-fpm

别忘记了拷贝配置文件

cp php.ini-production /usr/local/php7/etc/php.ini
cp sapi/fpm/init.d.php-fpm /etc/init.d/php7-fpm
chmod +x /etc/init.d/php7-fpm
cp /usr/local/php7/etc/php-fpm.conf.default /usr/local/php7/etc/php-fpm.conf
cp /usr/local/php7/etc/php-fpm.d/www.conf.default /usr/local/php7/etc/php-fpm.d/www.conf

初探扩展

本篇笔记紧紧是自己的学习过程,更规范的文档,大家可以参考鸟哥的博客 http://www.laruence.com/2009/04/28/719.html

PHP7 扩展试玩

通过框架自动生成一个最简单的扩展,到源代码目录的ext目录下

[root@localhost ext]# ./ext_skel --extname=helloworld

执行完之后会出现如下文档,后面依照下面说的8步来操作。

Creating directory helloworld
Creating basic files: config.m4 config.w32 .gitignore helloworld.c php_helloworld.h CREDITS EXPERIMENTAL tests/001.phpt helloworld.php [done].

To use your new extension, you will have to execute the following steps:

1.  $ cd ..
2.  $ vi ext/helloworld/config.m4
3.  $ ./buildconf
4.  $ ./configure --[with|enable]-helloworld
5.  $ make
6.  $ ./sapi/cli/php -f ext/helloworld/helloworld.php
7.  $ vi ext/helloworld/helloworld.c
8.  $ make

Repeat steps 3-6 until you are satisfied with ext/helloworld/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.

在执行上面的8步操作之前,我们先看下生成了哪些文件

-rw-r--r--  1 zhoumengkang  staff    11  2 27 18:40 CREDITS
-rw-r--r--  1 zhoumengkang  staff     0  2 27 18:40 EXPERIMENTAL
-rw-r--r--  1 zhoumengkang  staff  2216  2 27 18:40 config.m4
-rw-r--r--  1 zhoumengkang  staff   383  2 27 18:40 config.w32
-rw-r--r--  1 zhoumengkang  staff  5325  2 27 18:40 helloworld.c
-rw-r--r--  1 zhoumengkang  staff   514  2 27 18:40 helloworld.php
-rw-r--r--  1 zhoumengkang  staff  2381  2 27 18:40 php_helloworld.h
drwxr-xr-x  3 zhoumengkang  staff   102  2 27 18:40 tests

理解各个文件的作用

CREDITS 文件用纯文本格式列出了扩展的贡献者和(或 )维护者。此文件的主要用途是为已捆绑的扩展生成被 phpcredits() 所使用的荣誉信息。按惯例,文件的第一行应保存扩展的名称,第二行是用逗号分隔的贡献者名单。

EXPERIMENTAL 具体用途不清楚,应该是和实验相关的内容吧。

config.m4 UNIX 构建系统配置文件,指导phpize命令生成./configure脚本。(重点)

config.w32 Windows 构建系统配置文件。(Unix 环境上可以忽略)

helloworld.c 扩展程序

php_helloworld.h 扩展的头文件

helloworld.php 用于扩展编译安装之后的测试用,下面会执行使用

tests 测试脚本可以放到该目录下,后面也会使用到。

下面开始操作

[root@localhost ext]# cd ..
[root@localhost php-7.0.2]# vim ext/helloworld/config.m4

没有依赖别的库,把这几行注释去掉

依照文档说的执行./buildconf提示

[root@localhost php-7.0.2]# ./buildconf 
You should not run buildconf in a release package.
use buildconf --force to override this check.

其实phpize就是对buildconf的封装,我们使用phpize命令

[root@localhost php-7.0.2]# cd ext/helloworld/
[root@localhost helloworld]# phpize
Configuring for:
PHP Api Version:         20151012
Zend Module Api No:      20151012
Zend Extension Api No:   320151012
[root@localhost helloworld]# ./configure --with-php-config=/usr/local/php7/bin/php-config
...
[root@localhost helloworld]# make
[root@localhost helloworld]# cd ..
[root@localhost ext]# cd ..
[root@localhost php-7.0.2]# ./sapi/cli/php -f ext/helloworld/helloworld.php
Functions available in the test extension:
confirm_helloworld_compiled

Congratulations! You have successfully modified ext/helloworld/config.m4. Module helloworld is now compiled into PHP.

至此完成了文档的前6步,也就是确保了扩展已经编译到PHP中了。可以看下ext/helloworld/helloworld.php代码就明白其检测原理了。

下面通过make install把so文件拷贝扩展目录

root@localhost php-7.0.2]# cd ext/helloworld/
[root@localhost helloworld]# make install
...
Installing shared extensions:     /usr/local/php7/lib/php/extensions/no-debug-zts-20151012/

如果后面需要使用该扩展就可以在php.ini中添加该扩展并使用了。

关于tests目录里面的测试文件,可以通过make test来测试,在下面一篇中,我们将自己编写下测试文件。

通过原型来生成扩展框架

[root@localhost ext]# mkdir zmk_test
[root@localhost ext]# cd zmk_test/
[root@localhost zmk_test]# vim test.proto
int zmk_test_inc(int a)

然后通过就能生成扩展了

[root@localhost ext]# ./ext_skel --extname=zmktest1 --proto=./zmk_test/test.proto 
...
[root@localhost ext]# cd zmktest1/

然后vim zmktest1.c,发现我们原型设定的函数已经给我们生成好了,参数名都一样。

PHP_FUNCTION(zmk_test_inc)
{
        int argc = ZEND_NUM_ARGS();
        zend_long a;

        if (zend_parse_parameters(argc TSRMLS_CC, "l", &a) == FAILURE)
                return;

        php_error(E_WARNING, "zmk_test_inc: not yet implemented");
}

稍微修改下

PHP_FUNCTION(zmk_test_inc)
{
        int argc = ZEND_NUM_ARGS();
        zend_long a;

        if (zend_parse_parameters(argc TSRMLS_CC, "l", &a) == FAILURE)
                return;

        //php_error(E_WARNING, "zmk_test_inc: not yet implemented");

        RETURN_LONG(a+1);
}

然后就是和上面的扩展开发测试的一样,修改config.m4->phpize->make

这里我学习编写下单元测试,进入到tests目录,添加一个测试文件002.phpt,测试上面新增的方法,zmk_test_inc(20)期望值当然是21啦

--TEST--
Check for function zmk_test_inc
--SKIPIF--
<?php if (!extension_loaded("zmktest")) print "skip"; ?>
--FILE--
<?php
        echo zmk_test_inc(20);
?>
--EXPECT--
21

然后make test,都通过啦

[root@localhost zmktest1]# make test

Build complete.
Don't forget to run 'make test'.


=====================================================================
PHP         : /usr/local/php7/bin/php 
PHP_SAPI    : cli
PHP_VERSION : 7.0.2
ZEND_VERSION: 3.0.0
PHP_OS      : Linux - Linux localhost.localdomain 2.6.32-431.el6.x86_64 #1 SMP Fri Nov 22 03:15:09 UTC 2013 x86_64
INI actual  : /root/lnmp1.2-full/php-7.0.2/ext/zmktest1/tmp-php.ini
More .INIs  :   
CWD         : /root/lnmp1.2-full/php-7.0.2/ext/zmktest1
Extra dirs  : 
VALGRIND    : Not used
=====================================================================
TIME START 2016-01-27 16:54:42
=====================================================================
PASS Check for zmktest1 presence [tests/001.phpt] 
PASS Check for function zmk_test_set [tests/002.phpt] 
=====================================================================
TIME END 2016-01-27 16:54:42

=====================================================================
TEST RESULT SUMMARY
---------------------------------------------------------------------
Exts skipped    :    0
Exts tested     :   44
---------------------------------------------------------------------

Number of tests :    2                 2
Tests skipped   :    0 (  0.0%) --------
Tests warned    :    0 (  0.0%) (  0.0%)
Tests failed    :    0 (  0.0%) (  0.0%)
Expected fail   :    0 (  0.0%) (  0.0%)
Tests passed    :    2 (100.0%) (100.0%)
---------------------------------------------------------------------
Time taken      :    0 seconds
=====================================================================

This report can be automatically sent to the PHP QA team at
http://qa.php.net/reports and http://news.php.net/php.qa.reports
This gives us a better understanding of PHP's behavior.
If you don't want to send the report immediately you can choose
option "s" to save it.	You can then email it to qa-reports@lists.php.net later.
Do you want to send this report now? [Yns]: n
[root@localhost zmktest1]# make install
Installing shared extensions:     /usr/local/php7/lib/php/extensions/no-debug-zts-20151012/

关于报错检出

在我们自己的扩展目录里会有一个和扩展同名的php文件,也可以在那里面测试,make test不会报错,但是在php文件里运行就会有比较多的报错信息了。

php -d"extension=zmktest1.so" zmktest1.php

同时也可以在编译的时候加上--enable-debug

试试内存分配

PHP_FUNCTION(self_concat)
{
	char *str = NULL;
	int argc = ZEND_NUM_ARGS();
	size_t str_len;
	zend_long n;

	char *result;
	char *ptr;
	int result_length;

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

	result_length = (str_len * n);

	result = (char *) emalloc(result_length +1);

	ptr = result;

	while(n--){
		memcpy(ptr,str,str_len);
		ptr += str_len;
	}

	*ptr = '\0';

	RETURN_STRINGL(result,result_length);

}

扩展依赖

http://www.laruence.com/2009/08/18/1042.html

zend_module_entry结构体中的_zend_module_dep即为依赖的包

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);
   void (*globals_dtor)(void *global);
   int (*post_deactivate_func)(void);
   int module_started;
   unsigned char type;
   void *handle;
   int module_number;
   const char *build_id;
};

替换相关宏,比如下面我的扩展依赖standard扩展包

/* {{{ zmk_test_functions[]
 *
 * Every user visible function must have an entry in zmk_test_functions[].
 */
const zend_function_entry zmk_test_functions[] = {
	PHP_FE(confirm_zmk_test_compiled,	NULL)		/* For testing, remove later. */
	PHP_FE(self_concat,	NULL)
	PHP_FE(local_upload,	NULL)
	PHP_FE_END	/* Must be the last line in zmk_test_functions[] */
};
/* }}} */

static const zend_module_dep zmk_test_deps[] = {
	ZEND_MOD_REQUIRED("standard")
	ZEND_MOD_END
};

/* {{{ zmk_test_module_entry
 */
zend_module_entry zmk_test_module_entry = {
	STANDARD_MODULE_HEADER_EX,
	NULL,
	zmk_test_deps,
	"zmk_test",
	zmk_test_functions,
	PHP_MINIT(zmk_test),
	PHP_MSHUTDOWN(zmk_test),
	PHP_RINIT(zmk_test),		/* Replace with NULL if there's nothing to do at request start */
	PHP_RSHUTDOWN(zmk_test),	/* Replace with NULL if there's nothing to do at request end */
	PHP_MINFO(zmk_test),
	PHP_ZMK_TEST_VERSION,
	STANDARD_MODULE_PROPERTIES
};
/* }}} */

关于其中的依赖的意思是说,如果A依赖B,那么在A中可以使用B中定义好了的PHP_FUNCTION吗?还是别的

我尝试把self_concat的返回值写成,也就说调用了standard里的字符串函数explode

RETURN_ARR(explode("",arg));

重新编译

[root@localhost zmk_test]# phpize
...
[root@localhost zmk_test]# ./configure --with-php-config=/usr/local/php7/bin/php-config
...
[root@localhost zmk_test]# make clean
...
[root@localhost zmk_test]# make
...
[root@localhost zmk_test]# make install

但是运行提示(扩展名我定义为了zmk_test

[root@localhost zmk_test]# php -d"extension=zmk_test.so" -r "echo self_concat('aaa');"
php: symbol lookup error: /usr/local/php7/lib/php/extensions/no-debug-zts-20151012/zmk_test.so: undefined symbol: explode


嗨,老铁,欢迎来到我的博客!

如果觉得我的内容还不错的话,可以关注下我在 segmentfault.com 上的直播。我主要从事 PHP 和 Java 方面的开发,《深入 PHP 内核》作者之一。

[视频直播] PHP 进阶之路 - 亿级 pv 网站架构的技术细节与套路 直播中我将毫无保留的分享我这六年的全部工作经验和踩坑的故事,以及会穿插着一些面试中的 考点难点加分点

评论列表