前言

对于 php 扩展开发,很多人可能已经不那么陌生了,zend 引擎为了们提供了非常丰富了函数和 macro,来帮助我们很快速的创建一个标准的 php 类,然而,当我们在使用自定义的数据结构(struct), 并想把我们自己定义的数据结构封装成 php 的类的时候可能就会有些困惑,因为我们都知道 php 中的所有变量都是通过 zval 来存储的,而我们自定义的数据结构要怎样才能和 zval 实现完美的对接呢? 以前我通常采用的一种方式就是使用 zend 引擎提供的资源类型,因为资源类型的封装中包含了通用的数据类型,而且有很丰富的函数来操作资源,所以使用起来很简单也很方便。然而,强大的 zend 引擎真的没有其他方式扩展数据结构了吗?当然不是!下面就来介绍一个更加优雅的方式。要弄明白,首要要搞清 php 内核是如何创建对象的。

如何创建一个对象

我们首先来探讨下如何创建一个 PHP 对象。为此我们将会用到object_and_properties_init之类的一些宏。

// 创建一个SomeClass类型的对象,并且把properties_hashtable中的变量作为其属性值
zval *obj;
MAKE_STD_ZVAL(obj);
object_and_properties_init(obj, class_entry_of_SomeClass, properties_hashtable);

// 创建一个没有属性的对象
zval *obj;
MAKE_STD_ZVAL(obj);
object_init_ex(obj, class_entry_of_SomeClass);
// = object_and_properties_init(obj, class_entry_of_SomeClass, NULL)

// 创建一个stdClass的对象
zval *obj;
MAKE_STD_ZVAL(obj);
object_init(obj);
// = object_init_ex(obj, NULL) = object_and_properties_init(obj, NULL, NULL);

在上面的例子中,最后一种情况下,当你创建一个stdClass的对象后,通常将会给它添加属性。这时如果使用 zend_update_property之类的函数,是不起作用的,取而代之的是add_property宏函数:

add_property_long(obj, "id", id);
add_property_string(obj, "name", name, 1); // 1 表示使用字符串的副本
add_property_bool(obj, "isAdmin", is_admin);
// 同样有_null(), _double(), _stringl(), _resource()和_zval()

那么当一个对象被创建的时候到底发生了什么呢?想知道真相,就需要去阅读_object_and_properties_init函数的源码(位于 phpsrc/Zend/zend_API.c 中):

/* This function requires 'properties' to contain all props declared in the
 * class and all props being public. If only a subset is given or the class
 * has protected members then you need to merge the properties separately by
 * calling zend_merge_properties(). */
ZEND_API int _object_and_properties_init(zval *arg, zend_class_entry *class_type, HashTable *properties ZEND_FILE_LINE_DC TSRMLS_DC)
{
    zend_object *object;

    if (class_type->ce_flags & (ZEND_ACC_INTERFACE|ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) {
        char *what =   (class_type->ce_flags & ZEND_ACC_INTERFACE)                ? "interface"
                     :((class_type->ce_flags & ZEND_ACC_TRAIT) == ZEND_ACC_TRAIT) ? "trait"
                     :                                                              "abstract class";
        zend_error(E_ERROR, "Cannot instantiate %s %s", what, class_type->name);
    }

    zend_update_class_constants(class_type TSRMLS_CC);

    Z_TYPE_P(arg) = IS_OBJECT;
    if (class_type->create_object == NULL) {
        Z_OBJVAL_P(arg) = zend_objects_new(&object, class_type TSRMLS_CC);
        if (properties) {
            object->properties = properties;
            object->properties_table = NULL;
        } else {
            object_properties_init(object, class_type);
        }
    } else {
        Z_OBJVAL_P(arg) = class_type->create_object(class_type TSRMLS_CC);
    }
    return SUCCESS;
}

这个函数基本上只做了三件事情:首先校验其是否能够被实例化,接着会注册 class 常量,最后也是最重要的一步操作就是 是否该类有create_object的处理函数,如果有,将会被调用,如果没有,会调用一个默认的实现zend_objects_new

下面是zend_objects_new的源码(位于 phpsrc/Zend/zend_objects.c 中):

ZEND_API zend_object_value zend_objects_new(zend_object **object, zend_class_entry *class_type TSRMLS_DC)
{
    zend_object_value retval;

    *object = emalloc(sizeof(zend_object));
    (*object)->ce = class_type;
    (*object)->properties = NULL;
    (*object)->properties_table = NULL;
    (*object)->guards = NULL;
    retval.handle = zend_objects_store_put(*object, (zend_objects_store_dtor_t) zend_objects_destroy_object, (zend_objects_free_object_storage_t) zend_objects_free_object_sto
    retval.handlers = &std_object_handlers;
    return retval;
}

上面的代码中包含了三个很有趣的东西。一个是zend_object的结构,它的定义如下(位于 phpsrc/Zend/zend.h 中):

typedef struct _zend_object {
    zend_class_entry *ce;
    HashTable *properties;
    zval **properties_table;
    HashTable *guards; /* protects from __get/__set ... recursion */
} zend_object;

这就是一个标准的对象的结构。zend_objects_new函数声明了一个标准的对象结构并对其进行初始化操作。然后它调用了zend_objects_store_put函数 来把对象数据存放到一个对象存储器中。对象存存储器仅仅是一个能够动态改变大小的存放zend_object_store_buckets类型数据的数组。

下面是zend_object_store_bucket的数据结构(位于 phpsrc/Zend/zend_objects_API.h 中)

typedef struct _zend_object_store_bucket {
    zend_bool destructor_called;
    zend_bool valid;
    zend_uchar apply_count;
    union _store_bucket {
        struct _store_object {
            void *object;
            zend_objects_store_dtor_t dtor;
            zend_objects_free_object_storage_t free_storage;
            zend_objects_store_clone_t clone;
            const zend_object_handlers *handlers;
            zend_uint refcount;
            gc_root_buffer *buffered;
        } obj;
        struct {
            int next;
        } free_list;
    } bucket;
} zend_object_store_bucket;

这里的主要部分是_store_object结构,它通过void *object成员来存储对象,紧跟其后的成员分别用于是它的销毁,释放和拷贝。 接着是一些额外的成员,例如它有自己的refcount属性,因为一个存储在对象存储器中的对象可以被很多zval同时引用,php 需要 知道有多少引用以便于它是否能够被释放。此外,handlers属性和一个 GC root buffer 也被额外保存下来。

我们回到前面的zend_objects_new函数,它的最后一部操作就是将对象的handlers设置成默认的std_object_handlers

重写 create_object

当你想使用自定义的对象存储时,你至少需要重复上述的三个操作:首先你需要申明并初始化你的对象,其中它需要包含一个标准的 对象作为它结构的一部分。接着你需要将它以及它的一些处理函数一起放入对象存储器中。最后你需要将你自定义的对象处理结构物 赋值给zend_object_valuehandlers属性中。

下面是一个很简单的示例:

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_test.h"

static int le_test;

zend_class_entry *test_ce;

static zend_object_handlers test_object_handlers;

typedef	struct _test_object {
	zend_object std;
	long additional_property;
} test_object;


static void test_free_object_storage_handler(test_object *intern TSRMLS_DC)
{
	zend_object_std_dtor(&intern->std TSRMLS_CC);
	efree(intern);
}

zend_object_value test_create_object_handler(zend_class_entry *class_type TSRMLS_DC)
{
	zend_object_value retval;
	test_object *intern = emalloc(sizeof(test_object));
	memset(intern, 0, sizeof(test_object));

	zend_object_std_init(&intern->std, class_type TSRMLS_CC);
	object_properties_init(&intern->std, class_type);

	retval.handle = zend_objects_store_put(
			intern,
			(zend_objects_store_dtor_t) zend_objects_destroy_object,
			(zend_objects_free_object_storage_t) test_free_object_storage_handler,
			NULL TSRMLS_CC
			);

	/* Assign the customized object handlers */
	retval.handlers = &test_object_handlers;
	return retval;
}

const zend_function_entry test_methods[] = {
	PHP_FE_END
};


/* PHP_MINIT_FUNCTION
 */
PHP_MINIT_FUNCTION(test)
{
	zend_class_entry tmp_ce;
	INIT_CLASS_ENTRY(tmp_ce, "Test", test_methods);
	test_ce = zend_register_internal_class(&tmp_ce TSRMLS_CC);
	test_ce->create_object = test_create_object_handler;
	memcpy(&test_object_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));

	return SUCCESS;
}

/* PHP_MSHUTDOWN_FUNCTION
 */
PHP_MSHUTDOWN_FUNCTION(test)
{
	return SUCCESS;
}

/* PHP_RINIT_FUNCTION
 */
PHP_RINIT_FUNCTION(test)
{
	return SUCCESS;
}

/* PHP_RSHUTDOWN_FUNCTION
 */
PHP_RSHUTDOWN_FUNCTION(test)
{
	return SUCCESS;
}

/* PHP_MINFO_FUNCTION
 */
PHP_MINFO_FUNCTION(test)
{
	php_info_print_table_start();
	php_info_print_table_header(2, "test support", "enabled");
	php_info_print_table_end();

}

/* test_functions[]
 *
 */
const zend_function_entry test_functions[] = {
	PHP_FE_END
};

/* test_module_entry
 */
zend_module_entry test_module_entry = {
	STANDARD_MODULE_HEADER,
	"test",
	NULL,
	PHP_MINIT(test),
	PHP_MSHUTDOWN(test),
	PHP_RINIT(test),		/* Replace with NULL if there's nothing to do at request start */
	PHP_RSHUTDOWN(test),	/* Replace with NULL if there's nothing to do at request end */
	PHP_MINFO(test),
	PHP_TEST_VERSION,
	STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_TEST
ZEND_GET_MODULE(test)
#endif

Object store handlers

正如前面已经提到的,总共有三个 object storage handlers: 一个是用来做销毁操作,一个是用来处理释放操作,还有一个是用来处理克隆。 你一开始是不是感到有一点点的疑惑,为什么会同时存在dtorfree操作,它们看上去像是做的相同的事情。然而实际上是 php 有两个阶段 的对象销毁系统,首先会调用析构函数,然后才会释放空间。两个阶段相互分开,彼此独立。

特别地,在脚本执行完毕,而且所有的对象还存在于内存中的时候,php 会先调用析构函数(之后会执行 RSHUTDOWN 函数),接下来会及时释放 空间,作为关闭执行器的一部分操作。销毁和释放操作独立开是很有必要的,这样能够保证在关闭操作执行的时候不会有析构函数执行,换句 话说,你可能会遇到这种情况,你的代码在一种半关闭的状态下执行。

析构函数的另一个特性就是它不是一定会被执行,例如当执行die语句后,析构函数就会被跳过。

因此dtorfree最本质的区别就是dtor可以执行用户端的代码,但是并不是必须执行的,而free操作却总是会被执行,而且free不必 执行任何 php 代码。这就是为什么大多数情况下,你只需要定义一个自定义的释放函数,然后使用zend_objects_destroy_object作为析构函数的原因。 而zend_objects_destroy_object只提供了一个默认的行为,那就是调用__destruct方法,如果存在的话。另外,即使你用不到__destruct方法, 你也需要给该dtor赋值,否则它的派生类也不能使用__destruct方法。

至此,只剩下 clone handler 了,但是对于它的功能,从名字上我们很快就能理解到。但是它的用法却有一点点的复杂。下面是一个 clone handler 的示例:

static void test_clone_object_storage_handler(test_object *object, test_object **object_clone_target TSRMLS_DC)
{
    /* Create a new object */
    test_object *object_clone = emalloc(sizeof(test_object));
    zend_object_std_init(&object_clone->std, object->std.ce TSRMLS_CC);
    object_properties_init(&object_clone->std, object->std.ce);

    object_clone->additional_property = object->additional_property;
    *object_clone_target = object_clone;
}

然后把 clone handler 传递给zend_objects_store_put函数的最后一个参数:

retval.handle = zend_object_store_put(
            intern,
            (zend_objects_store_dtor_t) zend_objects_destroy_object,
            (zend_objects_free_object_storage_t) test_free_object_storage_handler,
            (zend_objects_store_clone_t) test_clone_object_storage_handler
            TSRMLS_CC
            );

但是这样做并不能使 clone handler 生效,默认情况下,clone handler 会被忽略。为了使他生效,你需要通过zend_objects_store_clone_obj用 默认的 clone handler 替换到自己定义的 handlers 结构:

memcpy(&test_object_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
test_object_handler.clone_obj = zend_objects_store_clone_obj;

由于覆盖标准的 clone handler(zend_objects_clone_obj)会引发一系列的问题:由于对象的属性不会被拷贝,因此 __clone方法也不会被调用。这就是为什么大多数内部类都是直接指定自己的对象拷贝函数,而不是绕额外的弯子去 使用对象存储器的对象克隆函数。这个方法看起来有点过于样板化,它的源码如下(位于 phpsrc/Zend/zend_objects.c 中):

ZEND_API zend_object_value zend_objects_clone_obj(zval *zobject TSRMLS_DC)
{
    zend_object_value new_obj_val;
    zend_object *old_object;
    zend_object *new_object;
    zend_object_handle handle = Z_OBJ_HANDLE_P(zobject);

    /* assume that create isn't overwritten, so when clone depends on the
     * overwritten one then it must itself be overwritten */
    old_object = zend_objects_get_address(zobject TSRMLS_CC);
    new_obj_val = zend_objects_new(&new_object, old_object->ce TSRMLS_CC);

    zend_objects_clone_members(new_object, new_obj_val, old_object, handle TSRMLS_CC);

    return new_obj_val;
}

这个函数首先使用zend_objects_get_address从对象存储器中获取zend_object*,然后创建一个拥有相同class_entry的对象(通过zend_objects_new), 然后调用zend_objects_clone_members操作来克隆对象的属性,而且会调用__clone方法,如果存在的话。

使用我们自定义的create_object handler 来取代zend_objects_new函数,将会使得我们自定义的 object clone handler 看起来简单许多:

static zend_object_value test_clone_handler(zval *object TSRMLS_DC)
{
	test_object *old_object = zend_object_store_get_object(object TSRMLS_CC);
	zend_object_value new_object_val = test_create_object_handler(Z_OBJCE_P(object) TSRMLS_CC);
	test_object *new_object = zend_object_store_get_object_by_handle(new_object_val.handle TSRMLS_CC);
	/* Clone properties and call __clone */
	zend_objects_clone_members(&new_object->std, new_object_val, &old_object->std, Z_OBJ_HANDLE_P(object) TSRMLS_CC);

	new_object->additional_property = old_object->additional_property;
	return new_object_val;
}

/* ... */
//MINIT
test_object_handler.clone_obj = test_clone_handler;

与 object store 的交互操作

通过上面的示例,已经展示过一些与 object store 交互的函数,例如:zend_objects_store_put,这是一个向 object store 中插入一个元素的操作。 同时也有三个函数用来从 object store 中获取元素。

zend_object_store_get_object_by_handle(),顾名思义,通过给定一个对象的 handle 来获取该对象。当你已经获取到了一个对象的 handle,却没有访问到它的 zval 时,可以调用此函数。 但是在大多数情况下,你会使用zend_object_store_get_object()函数,通过给定对象的 zval 来获取它。 第三个函数是zend_objects_get_address(),该函数类似于zend_object_store_get_object()函数,但是它返回的类型是zend_object *而不是void *。所以这个函数一般很少使用,因为 c 语言允许隐式的类型转换,从void *类型转换成其他指针类型。

以上三个函数中最重要的一个就是zend_object_store_get_object()。你将会频繁使用。下面给出一个简单的示例:

PHP_METHOD(Test, get)
{
	zval *object;
	test_object *intern;
	if (zend_parse_parameters_none() == FAILURE) {
		return;
	}
	object = getThis();
	intern = zend_object_store_get_object(object TSRMLS_CC);
	RETURN_LONG(intern->additional_property);
}

下面给出完整的示例代码:

/*
  +----------------------------------------------------------------------+
  | Test                                                                 |
  +----------------------------------------------------------------------+
  | Copyright (c) 1997-2016 The PHP Group                                |
  +----------------------------------------------------------------------+
  | This source file is subject to version 3.01 of the PHP license,      |
  | that is bundled with this package in the file LICENSE, and is        |
  | available through the world-wide-web at the following url:           |
  | http://www.php.net/license/3_01.txt                                  |
  | If you did not receive a copy of the PHP license and are unable to   |
  | obtain it through the world-wide-web, please send a note to          |
  | license@php.net so we can mail you a copy immediately.               |
  +----------------------------------------------------------------------+
  | Author: liubang <it.liubang@gmail.com>                               |
  +----------------------------------------------------------------------+
*/

/* $Id$ */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_test.h"

static int le_test;

zend_class_entry *test_ce;

static zend_object_handlers test_object_handlers;

typedef	struct _test_object {
	zend_object std;
	long additional_property;
} test_object;

static void test_free_object_storage_handler(test_object *intern TSRMLS_DC)
{
	zend_object_std_dtor(&intern->std TSRMLS_CC);
	efree(intern);
}


static void test_clone_object_storage_handler(test_object *object, test_object **object_clone_target TSRMLS_DC)
{
	/* Create a new object */
	test_object *object_clone = emalloc(sizeof(test_object));
	zend_object_std_init(&object_clone->std, object->std.ce TSRMLS_CC);
	object_properties_init(&object_clone->std, object->std.ce);

	object_clone->additional_property = object->additional_property;
	*object_clone_target = object_clone;
}

zend_object_value test_create_object_handler(zend_class_entry *class_type TSRMLS_DC)
{
	zend_object_value retval;
	test_object *intern = emalloc(sizeof(test_object));
	memset(intern, 0, sizeof(test_object));

	zend_object_std_init(&intern->std, class_type TSRMLS_CC);
	object_properties_init(&intern->std, class_type);

	retval.handle = zend_objects_store_put(
			intern,
			(zend_objects_store_dtor_t) zend_objects_destroy_object,
			(zend_objects_free_object_storage_t) test_free_object_storage_handler,
			(zend_objects_store_clone_t) test_clone_object_storage_handler
			TSRMLS_CC
			);

	/* Assign the customized object handlers */
	retval.handlers = &test_object_handlers;
	return retval;
}

static zend_object_value test_clone_handler(zval *object TSRMLS_DC)
{
	test_object *old_object = zend_object_store_get_object(object TSRMLS_CC);
	zend_object_value new_object_val = test_create_object_handler(Z_OBJCE_P(object) TSRMLS_CC);
	test_object *new_object = zend_object_store_get_object_by_handle(new_object_val.handle TSRMLS_CC);
	/* Clone properties and call __clone */
	zend_objects_clone_members(&new_object->std, new_object_val, &old_object->std, Z_OBJ_HANDLE_P(object) TSRMLS_CC);

	new_object->additional_property = old_object->additional_property;
	return new_object_val;
}

PHP_METHOD(Test, set)
{
	zval *object;
	long *val;
	test_object *intern;
	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &val) == FAILURE) {
		RETURN_FALSE;
	}
	object = getThis();
	intern = zend_object_store_get_object(object TSRMLS_CC);
	intern->additional_property = val;
	RETURN_TRUE;
}

PHP_METHOD(Test, get)
{
	zval *object;
	test_object *intern;
	if (zend_parse_parameters_none() == FAILURE) {
		return;
	}
	object = getThis();
	intern = zend_object_store_get_object(object TSRMLS_CC);
	RETURN_LONG(intern->additional_property);
}

const zend_function_entry test_methods[] = {
	PHP_ME(Test, set, NULL, ZEND_ACC_PUBLIC)
	PHP_ME(Test, get, NULL, ZEND_ACC_PUBLIC)
	PHP_FE_END
};


/* PHP_MINIT_FUNCTION
 */
PHP_MINIT_FUNCTION(test)
{
	zend_class_entry tmp_ce;
	INIT_CLASS_ENTRY(tmp_ce, "Test", test_methods);
	test_ce = zend_register_internal_class(&tmp_ce TSRMLS_CC);
	test_ce->create_object = test_create_object_handler;
	memcpy(&test_object_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
	test_object_handlers.clone_obj = test_clone_handler;

	return SUCCESS;
}

/* PHP_MSHUTDOWN_FUNCTION
 */
PHP_MSHUTDOWN_FUNCTION(test)
{
	return SUCCESS;
}

/* PHP_RINIT_FUNCTION
 */
PHP_RINIT_FUNCTION(test)
{
	return SUCCESS;
}

/* PHP_RSHUTDOWN_FUNCTION
 */
PHP_RSHUTDOWN_FUNCTION(test)
{
	return SUCCESS;
}

/* PHP_MINFO_FUNCTION
 */
PHP_MINFO_FUNCTION(test)
{
	php_info_print_table_start();
	php_info_print_table_header(2, "test support", "enabled");
	php_info_print_table_end();

}

/* test_functions[]
 *
 */
const zend_function_entry test_functions[] = {
	PHP_FE_END
};

/* test_module_entry
 */
zend_module_entry test_module_entry = {
	STANDARD_MODULE_HEADER,
	"test",
	NULL,
	PHP_MINIT(test),
	PHP_MSHUTDOWN(test),
	PHP_RINIT(test),		/* Replace with NULL if there's nothing to do at request start */
	PHP_RSHUTDOWN(test),	/* Replace with NULL if there's nothing to do at request end */
	PHP_MINFO(test),
	PHP_TEST_VERSION,
	STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_TEST
ZEND_GET_MODULE(test)
#endif

测试并运行:

$test = new Test();
var_dump($test);
$test->set(123);
var_dump($test->get());
// output
liubang@venux:~/workspace/c/test$ php tests/test.php
object(Test)#1 (0) {
}
int(123)