Skip to content

Commit

Permalink
Merge pull request #91 from zhoumengkang/master
Browse files Browse the repository at this point in the history
扩展开发第一节
  • Loading branch information
reeze committed Mar 16, 2016
2 parents 89db639 + 96fb2e2 commit 2b68cce
Show file tree
Hide file tree
Showing 6 changed files with 648 additions and 0 deletions.
166 changes: 166 additions & 0 deletions book/chapt11/11-02-00-extension-hello-world.markdown
@@ -0,0 +1,166 @@
# 第二节 创建第一个扩展

PHP7 已经发了很久,所以本章以讲解 PHP7 扩展开发为主,附带对比说明 PHP7 和 PHP5 中的一些区别。

每小节分为两大部分,一部分为实现,一部分为实现原理,读者可以先着眼实现,后期再反观原理,以防看得太过于枯燥而削弱阅读的积极性。

悉知本章开发环境默认为 Linux 环境,后文不再特殊说明。

## 安装 PHP7

[Github](https://github.com/php/php-src/releases) 下载的最新版,没安装太多的扩展,因为我仅仅是作为开发调试使用。
为了和本机的 PHP5 环境不冲突,便使用了新如下命令,后面的讲解中,默认使用该环境。解压安装包,进入到安装包目录。

[shell]
./buildconf --force
...
./configure --prefix=/usr/local/php7 --with-config-file-path=/usr/local/php7/etc --enable-fpm --with-fpm-user=www --enable-debug
...

添加一些软连接

[shell]
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

拷贝配置文件

[shell]
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

## 创建第一个扩展

创建第一个扩展的步骤,可能很多文章都有说明,如果觉得讲解得不够详细的,可以参考本节最后附予一些参考链接。

### 1. 编写原型文件

PHP 源代码目录中提供了一个可执行文件 `ext/ext_skel`,该文件可根据指定的原型文件生成扩展代码骨架。
原型文件有点类似 C 头文件,根据其中申明的函数,生成函数骨架代码和其他相关代码。

那么如何编写原型文件呢?

比如现编写一个 tipi_hello_world 的函数,输入一个字符串 `name` ,返回 `hello world, name`

现已有一个原型文件:`ext/tipi.proto`,内容为

string tipi_hello_world (string name)

原型文件的格式,类似于 C 头文件中的函数申明的方式,返回值、函数名、形参类型、形参名。
参数用 `()` 包裹,多个参数以 `,` 分隔,函数申明末尾不需要以 `;` 结尾,一行一个函数声明。
原型文件的参数执行哪些类型呢?在后面的练习中,我们都会运用该方式生成代码骨架,所以了解参数类型很有必要。

原型文件的生成依赖于 awk 脚本 `ext/skeleton/create_stubs` ,由其中 `convert` 函数可知,其支持的参数类型有

- int,long
- bool,boolean
- double,float
- string
- array,object,mixed
- resource,handle

### 2. 生成扩展骨架

我们将扩展命令为 `tipi_demo01`,使用如下命令生成扩展骨架

[shell]
./ext_skel --extname=tipi_demo01 --proto=tipi.proto

`ext_skel` 的完整介绍,可以参考官方的 [ext_skel 脚本](http://php.net/manual/zh/internals2.buildsys.skeleton.php) 说明。
执行完毕之后,会在 `ext/` 目录生成一个 `tipi_demo01` 的目录,进入该目录,列表如下

[shell]
[root@localhost tipi_demo01]# ll -al
total 44
drwxr-xr-x 3 root root 4096 Mar 13 23:13 .
drwxr-xr-x 79 1000 1000 4096 Mar 13 23:13 ..
-rw-r--r-- 1 root root 2248 Mar 13 23:13 config.m4
-rw-r--r-- 1 root root 390 Mar 13 23:13 config.w32
-rw-r--r-- 1 root root 11 Mar 13 23:13 CREDITS
-rw-r--r-- 1 root root 0 Mar 13 23:13 EXPERIMENTAL
-rw-r--r-- 1 root root 398 Mar 13 23:13 .gitignore
-rw-r--r-- 1 root root 2397 Mar 13 23:13 php_tipi_demo01.h
drwxr-xr-x 2 root root 4096 Mar 13 23:13 tests
-rw-r--r-- 1 root root 5725 Mar 13 23:13 tipi_demo01.c
-rw-r--r-- 1 root root 517 Mar 13 23:13 tipi_demo01.php

目录文件的完整描述可以参考官方的 [组成扩展的文件](http://php.net/manual/zh/internals2.structure.files.php) 说明。
其中 `config.m4` 作为 UNIX 构建系统配置文件,指导 `phpize` 命令生成 `./configure` 脚本以及其他一系列文件。(其实 `phpize` 也是对 `buildconf` 的封装)
补充说明 `test` 目录为单元测试脚本存放目录,`make test` 的时候会使用。其语法可参考 [phpt 测试文件说明](/book/?p=E-phpt-file)

### 3. 完善扩展

现在已经生成的扩展骨架代码,由于本例比较简单,我们只需完善我们申明的函数 `tipi_hello_world` 即可。在 `tipi_demo01.c` 中找到

[c]
PHP_FUNCTION(tipi_hello_world)
{
char *name = NULL;
int argc = ZEND_NUM_ARGS();
size_t name_len;

if (zend_parse_parameters(argc TSRMLS_CC, "s", &name, &name_len) == FAILURE)
return;

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

这就是 `tipi_hello_world` 在扩展中书写方式。
我们将其改写为

[c]
PHP_FUNCTION(tipi_hello_world)
{
char *name = NULL;
int argc = ZEND_NUM_ARGS();
size_t name_len;

char *result = NULL;
char *prefix = "hello world, ";


if (zend_parse_parameters(argc TSRMLS_CC, "s", &name, &name_len) == FAILURE)
return;

result = (char *) ecalloc(strlen(prefix) + name_len + 1, sizeof(char));
strncat(result, prefix, strlen(prefix));
strncat(result, name, name_len);

RETURN_STRING(result);
}

其中 `zend_parse_parameters` 从我们在原型中定义的参数 `name` 中获取传入的字符串,分配内存之后保存在指针 `*name`中, `name_len` 为传入字符串的长度。
这里通过 `zend_parse_parameters` 来解析获取函数传入的参数,[zend_parse_parameters](/book/?p=chapt11/11-02-01-zend-parse-parameters) 的工作原理我们在下一小节中详细讲解。
值得一提的是在 PHP7 中,官方新增了 Fast Parameter Parsing API ,能够更高效的解析参数,并且可读性更强。具体的使用,我们同样也会会在下节中详细说明。
其中通过 `ecalloc` 来申请内存,具体可以参考本书的第六章的 [PHP中的内存管理](/book/?p=chapt06/06-02-php-memory-manager)

### 4. 编译安装

首先修改 `config.m4` ,去掉 `PHP_ARG_WITH``--with-tipi_demo01` 这两行前面的 `dnl` 注释。修改后如下

[shell]
PHP_ARG_WITH(tipi_demo01, for tipi_demo01 support,
dnl Make sure that the comment is aligned:
[ --with-tipi_demo01 Include tipi_demo01 support])


在扩展目录(`ext/tipi_demo01/`)中,通过 `phpize` (我们在上面已经添加了软连接 `ln -s /usr/local/php7/bin/phpize /usr/bin/php7ize`) 生成 `configure` 系列文件。

[shell]
php7ize
...
make && make install
### 5. 测试

由于扩展程序比较简单,我们直接使用命令行测试

[shell]
php7 -d "extension=tipi_demo01.so" -r "echo tipi_hello_world('tipi');"

输出了 `hello world, tipi`。至此,我们的第一个扩展就完成。
135 changes: 135 additions & 0 deletions book/chapt11/11-02-01-zend-parse-parameters.markdown
@@ -0,0 +1,135 @@
# 扩展中参数的解析

在前面 `tipi_hello_world` 的扩展例子中,通过 `zend_parse_parameters` 获取到了
`tipi_hello_world()` 函数传入的字符串参数。下面我们首先对 API 进行讲解,然后说明其原理。
最后说明 Fast Parameter Parsing API。

## zend_parse_parameters 介绍

[c]
END_API int zend_parse_parameters(int num_args, const char *type_spec, ...)

`zend_parse_parameters` 解析参数,第一个参数是传递的参数个数。通常使用 `ZEND_NUM_ARGS()` 来获取。
第二个参数是一个字符串,指定了函数期望的各个参数的类型,后面紧跟着需要随参数值更新的变量列表。
因为PHP采用松散的变量定义和动态的类型判断,这样做就使得把不同类型的参数转化为期望的类型成为可能。

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

类型指定符 |对应的C类型 |描述
------------|-------------------|-------------------------
l |long |符号整数
d |double |浮点数
s |char *, int |二进制字符串,长度
b |zend_bool |逻辑型(1或0)
r |zval * |资源(文件指针,数据库连接等)
a |zval * |联合数组
o |zval * |任何类型的对象
O |zval * |指定类型的对象。需要提供目标对象的类类型
z |zval * |无任何操作的zval

例如下面的例子

[c]
zend_parse_parameters(ZEND_NUM_ARGS(), "sl", &str, &str_len, &n)

该表达式则是获取两个参数 `str``n`,字符串的类型是`s`,需要两个参数 `char *` 字符串和 `int` 长度;数字的类型 `l` ,只需要一个参数。

## zend_parse_parameters 动态获取参数的实现原理

由其源码可知

[c]
ZEND_API int zend_parse_parameters(int num_args, const char *type_spec, ...) /* {{{ */
{
va_list va;
int retval;
int flags = 0;
va_start(va, type_spec);
retval = zend_parse_va_args(num_args, type_spec, &va, flags);
va_end(va);
return retval;
}

主要使用到了 `va_list`, `va_start`, `va_arg``va_end` 来获取可变个数的参数。
通过如下代码的练习即可明白其原理了

[c]
#include <stdarg.h>
#include <stdio.h>
int zend_parse_parameters(int num_args, const char *type_spec, ...);
int main() {
zend_parse_parameters(4, "abcd", 1, 2, 3, 4);
return 0;
}
int zend_parse_parameters(int num_args, const char *type_spec, ...) {
va_list va;
const char *spec_walk;
char c;
va_start(va, type_spec);
for (spec_walk = type_spec; *spec_walk; spec_walk++) {
c = *spec_walk;
printf("参数类型为 %c 值为: %d\n", c, va_arg(va, int));
}
va_end(va);
return 0;
}

输出结果为

[shell]
参数类型为 a 值为: 1
参数类型为 b 值为: 2
参数类型为 c 值为: 3
参数类型为 d 值为: 4

## 使用 Fast Parameter Parsing API 取代 zend_parse_parameters

官方推荐在 PHP7 中使用 Fast Parameter Parsing API,不仅效率上有所提升,并且代码表意更加明确,易读性强。
下面引用官方的说明,简短翻译说明(完整地内容请参考本节末尾的参考链接)

`PHP_FUNCTION(array_slice)` 为例

[c]
PHP_FUNCTION(array_slice)
{

/* 省略一系列的参数声明 */

#ifndef FAST_ZPP
if (zend_parse_parameters(ZEND_NUM_ARGS(), "al|zb", &input, &offset, &z_length, &preserve_keys) == FAILURE) {
return;
}
#else
ZEND_PARSE_PARAMETERS_START(2, 4)
Z_PARAM_ARRAY(input)
Z_PARAM_LONG(offset)
Z_PARAM_OPTIONAL
Z_PARAM_ZVAL(z_length)
Z_PARAM_BOOL(preserve_keys)
ZEND_PARSE_PARAMETERS_END();
#endif

/* 省略后面的逻辑代码 */

}

在没有定义 `FAST_ZPP` 的情况下,使用 `zend_parse_parameters` 来解析,
`al|zb` 表示第一个参数为 `array`,第二个参数为 `long`,第三个参数为 `zval`,第四个参数为 `bool`
并且后面两个参数为可选。

如果定了 `FAST_ZPP` (该定义在 PHP7 `Zend/zend_API.h` 中),则使用 Fast Parameter Parsing API 方式来解析参数。代码本身已经足以解释其作用了。
`ZEND_PARSE_PARAMETERS_START()` 的两个参数分别为最少参数数和最多参数数。
`Z_PARAM_ARRAY()` 则将参数视为数组,`Z_PARAM_LONG()` 将参数视为长整型。
`Z_PARAM_OPTIONAL` 则表示后面的参数为可选参数。完整地映射表关系,可参考本节最后的参考链接。

## 参考资料
* [用C/C++扩展你的PHP](http://www.laruence.com/2009/04/28/719.html)
* [PHP RFC: Fast Parameter Parsing API](https://wiki.php.net/rfc/fast_zpp)
5 changes: 5 additions & 0 deletions book/index.markdown
Expand Up @@ -106,6 +106,8 @@

- [第十一章 扩展开发][extension-dev]
* 第一节 扩展开发概述
* [第二节 创建第一个扩展][extension-hello-world]
+ [扩展中参数的解析][zend-parse-parameters]

- 第十二章 文件和流
* stream wrapper
Expand Down Expand Up @@ -237,6 +239,9 @@
[thread-process-and-concurrent]: ?p=chapt08/08-02-thread-process-and-concurrent
[thread-safe-in-php]: ?p=chapt08/08-03-zend-thread-safe-in-php

[extension-hello-world]: ?p=chapt11/11-02-00-extension-hello-world
[zend-parse-parameters]: ?p=chapt11/11-02-01-zend-parse-parameters

[php-loop]: ?p=chapt16/16-01-00-php-loop
[php-foreach]: ?p=chapt16/16-01-01-php-foreach

Expand Down
63 changes: 63 additions & 0 deletions book/sample/chapt11/11-02-00-tipi-hello-world/config.m4
@@ -0,0 +1,63 @@
dnl $Id$
dnl config.m4 for extension tipi_demo01

dnl Comments in this file start with the string 'dnl'.
dnl Remove where necessary. This file will not work
dnl without editing.

dnl If your extension references something external, use with:

PHP_ARG_WITH(tipi_demo01, for tipi_demo01 support,
dnl Make sure that the comment is aligned:
[ --with-tipi_demo01 Include tipi_demo01 support])

dnl Otherwise use enable:

dnl PHP_ARG_ENABLE(tipi_demo01, whether to enable tipi_demo01 support,
dnl Make sure that the comment is aligned:
dnl [ --enable-tipi_demo01 Enable tipi_demo01 support])

if test "$PHP_TIPI_DEMO01" != "no"; then
dnl Write more examples of tests here...

dnl # --with-tipi_demo01 -> check with-path
dnl SEARCH_PATH="/usr/local /usr" # you might want to change this
dnl SEARCH_FOR="/include/tipi_demo01.h" # you most likely want to change this
dnl if test -r $PHP_TIPI_DEMO01/$SEARCH_FOR; then # path given as parameter
dnl TIPI_DEMO01_DIR=$PHP_TIPI_DEMO01
dnl else # search default path list
dnl AC_MSG_CHECKING([for tipi_demo01 files in default path])
dnl for i in $SEARCH_PATH ; do
dnl if test -r $i/$SEARCH_FOR; then
dnl TIPI_DEMO01_DIR=$i
dnl AC_MSG_RESULT(found in $i)
dnl fi
dnl done
dnl fi
dnl
dnl if test -z "$TIPI_DEMO01_DIR"; then
dnl AC_MSG_RESULT([not found])
dnl AC_MSG_ERROR([Please reinstall the tipi_demo01 distribution])
dnl fi

dnl # --with-tipi_demo01 -> add include path
dnl PHP_ADD_INCLUDE($TIPI_DEMO01_DIR/include)

dnl # --with-tipi_demo01 -> check for lib and symbol presence
dnl LIBNAME=tipi_demo01 # you may want to change this
dnl LIBSYMBOL=tipi_demo01 # you most likely want to change this

dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,
dnl [
dnl PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $TIPI_DEMO01_DIR/$PHP_LIBDIR, TIPI_DEMO01_SHARED_LIBADD)
dnl AC_DEFINE(HAVE_TIPI_DEMO01LIB,1,[ ])
dnl ],[
dnl AC_MSG_ERROR([wrong tipi_demo01 lib version or lib not found])
dnl ],[
dnl -L$TIPI_DEMO01_DIR/$PHP_LIBDIR -lm
dnl ])
dnl
dnl PHP_SUBST(TIPI_DEMO01_SHARED_LIBADD)

PHP_NEW_EXTENSION(tipi_demo01, tipi_demo01.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi

0 comments on commit 2b68cce

Please sign in to comment.