Object Handlers

在前面的博文中,已经介绍过一些 object handlers 了,也特别介绍了如何通过指定 handlers 来创建一个自定义的结构和使用clone_obj来对自定义的结构进行克隆操作。 然而,这只是开始:在 php 中,几乎所有的对象操作,都可以通过 object handlers 来实现,而且所有的魔术方法和魔术接口在内核中都是实现了对应的 object handler。此外, 一些 handlers 并没有开放给用户端的 php,例如,内部类可以自定义类的比较操作,而使用 php 代码是无法实现的。

由于 php 中有很多不同的 object handlers,这里只挑几个来讨论,其它的只给出简单的说明。

概述

下面列举出 php 中主要的 26 个(php5.6 中为 28 个)object handlers(位于 phpsrc/Zend/zend_object_handlers.h),并给出简要的说明。

zval *read_property(zval *object, zval *member, int type, const struct _zend_literal *key TSRMLS_DC)
void write_property(zval *object, zval *member, zval *value, const struct _zend_literal *key TSRMLS_DC)
int has_property(zval *object, zval *member, int has_set_exists, const struct _zend_literal *key TSRMLS_DC)
void unset_property(zval *object, zval *member, const struct _zend_literal *key TSRMLS_DC)
zval **get_property_ptr_ptr(zval *object, zval *member, const struct _zend_literal *key TSRMLS_DC)

上述 handlers 分别表示__get__set__isset__unset方法。get_property_ptr_ptr等同于__get返回一个引用类型。zend_literal *key作为这些函数的参数 起到优化作用,例如它包含了一些将属性名进行 hash 计算的结果。

zval *read_dimension(zval *object, zval *offset, int type TSRMLS_DC)
void write_dimension(zval *object, zval *offset, zval *value TSRMLS_DC)
int has_dimension(zval *object, zval *member, int check_empty TSRMLS_DC)
void unset_dimension(zval *object, zval *offset TSRMLS_DC)

上述了几个 handlers 就是内核用以实现ArrayAccess接口的。

void set(zval **object, zval *value TSRMLS_DC)
zval *get(zval *object TSRMLS_DC)

上述 handlers 用来getset一个对象的值。他们可以用来重写(从某种程度上)像+=++这样的符合赋值操作符,他们的存在主要是作为代理对象,但在实际应用中 缺很少被使用。

HashTable *get_properties(zval *object TSRMLS_DC)
HashTable *get_debug_info(zval *object, int *is_temp TSRMLS_DC)

上述 handlers 用来获取对象的属性,并返回 hashtable。前者更加通用,例如它还被用在get_object_vars函数中。而后者唯一的作用就是在类似于var_dump这种 debug 函数中打印 属性信息。因此,即使一个对象没有提供一些正式的属性,但是它仍然能够打印一些有用的 debug 信息。

union _zend_function *get_method(zval **object_ptr, char *method, int method_len, const struct _zend_literal *key TSRMLS_DC)
int call_method(const char *method, INTERNAL_FUNCTION_PARAMETERS)

get_method handler 通过查找zend_function来调用一个指定的方法。如果不存在这个指定的方法,就会触发__call-like的魔术方法来捕获所有的行为,这时候get_method 就会标记当前情况为ZEND_OVERLOADED_FUNCTION,从而使得call_method被执行。

union _zend_function *get_constructor(zval *object TSRMLS_DC)

类似于get_method handler,但是不同的是它是专门用来获取构造函数的。覆盖此 handler 通常是为了禁止用户自定义构造函数抛出错误。

int count_elements(zval *object, long *count TSRMLS_DC)

这就是内核实现Countable::count方法的方式。

int compare_objects(zval *object1, zval *object2 TSRMLS_DC)
int cast_object(zval *readobj, zval *retval, int type TSRMLSDC)

内部类拥有自定义比较操作的功能,而且能重写每种类型的类型转换操作。用户层的 php 类只能靠__toString()方法重写对象到 string 的类型转换操作。

int get_closure(zval *obj, zend_class_entry **ce_ptr, union _zend_function **fptr_ptr, zval **zobj_ptr TSRMLS_DC)

当一个对象被当做函数调用的时候,这个 handler 就会被触发执行。实际上它就是__invoke的内核实现。它的名字源于它主要是用作实现闭包。

zend_class_entry *get_class_entry(const zval *object TSRMLS_DC)
int get_class_name(const zval *object, const char **class_name, zend_uint *class_name_len, int parent TSRMLS_DC)

上面两个 handler 分别用于通过一个对象获取 class_entry 和类的名字。一般情况下不会去覆盖它们。我认为唯一有必要重写它们的情况就是当你创建一个自定义结构并没有将 标准的zend_object作为其中的子结构。

void add_ref(zval *object TSRMLS_DC)
void del_ref(zval *object TSRMLS_DC)
zend_object_value clone_obj(zval *object TSRMLS_DC)
HashTable *get_gc(zval *object, zval ***table, int *n TSRMLS_DC)

上述的操作主要用于对象维护。当一个zval开始引用一个对象的时候,add_ref将会被调用,当一个引用被移出的时候del_ref就会被调用。默认情况下,这些 handlers 会 改变 object store 中的refcount的值。同样的,它们也没有必要被重写,唯一可能的情况就是你不想使用 Zend object store,而想自定义一个。对于clone_obj,前面的文 章中已经介绍过了。get_gc会返回对象中所有的变量,所以错综复杂的依赖关系可以被适当的搜集。

使用 object handlers 来实现 array access

正如前面一篇文章中提到的,ArrayAccess接口是用来给 array buffer views 提供类似数组的行为。那么现在,我们将通过使用对应的*_dimension object handlers 来实现同样 的功能。但是使用自定义的实现方式能够避免额外的函数调用造成的开销,从而提升性能。

提供维度操作的 object handlers 分别为read_dimentionwrite_dimentionhas_dimentionunset_dimention。它们都需要传递一个对象的zval作为第一个参数,offset 作为第二个参数。由于我们的实现的是一个 ArrayBuffer,所以offset一定是一个整数类型,所以我们首先来定义一个工具函数,来从一个zval中获取整数值。

static long get_long_from_zval(zval *offset)
{
    if (Z_TYPE_P(offset) == IS_LONG) {
        return Z_LVAL_P(offset);
    } else {
        zval tmp = *offset;
        zval_copy_ctor(&tmp);
        convert_to_long(&tmp);
        return Z_LAVL(tmp);
    }
}

接下来我们来分别实现相应的 handlers。

static zval *linger_array_buffer_view_read_dimension(zval *object, zval *zv_offset, int type TSRMLS_DC)
{
	buffer_view_object *intern = zend_object_store_get_object(object TSRMLS_CC);
	zval *retval;
	long offset;

	if (intern->std.ce->parent) {
		return zend_get_std_object_handlers()->read_dimension(object, zv_offset, type TSRMLS_CC);
	}

	if (!zv_offset) {
		zend_throw_exception(NULL, "Cannot append to a typed array", 0 TSRMLS_CC);
		return NULL;
	}

	offset = get_long_from_zval(zv_offset);
	if (offset < 0 || offset >= intern->length) {
		zend_throw_exception(NULL, "Offset is outside the buffer range", 0 TSRMLS_CC);
		return NULL;
	}

	retval = linger_buffer_view_offset_get(intern, offset);
	Z_DELREF_P(retval);
	return retval;
}

static void linger_array_buffer_view_write_dimension(zval *object, zval *zv_offset, zval *value TSRMLS_DC)
{
	buffer_view_object *intern;
	long offset;
	intern = zend_object_store_get_object(object TSRMLS_CC);

	if (intern->std.ce->parent) {
		return zend_get_std_object_handlers()->write_dimension(object, zv_offset, value TSRMLS_CC);
	}

	if (!zv_offset) {
		zend_throw_exception(NULL, "Cannot append to a typed array", 0 TSRMLS_CC);
		return;
	}

	offset = get_long_from_zval(zv_offset);
	if (offset < 0 || offset > intern->length) {
		zend_throw_excpetion(NULL, "Offset is outside the buffer range", 0 TSRMLS_CC);
		return;
	}

	linger_buffer_view_offset_set(intern, offset, value);
}

static int linger_array_buffer_view_has_dimension(zval *object, zval *zv_offset,  int check_empty TSRMLS_DC)
{
	buffer_view_object *intern;
	long offset;
	intern = zend_object_store_get_object(object TSRMLS_CC);

	if (intern->std.ce->parent) {
		return zend_get_std_object_handlers()->has_dimension(object, zv_offset, check_empty TSRMLS_CC);
	}

	offset = get_long_from_zval(zv_offset);
	if (offset < 0 || offset > intern->length) {
		return 0;
	}

	if (check_empty) {
		int retval;
		zval *value = linger_buffer_view_offset_get(intern, offset);
		retval = zend_is_true(value);
		zval_ptr_dtor(&value);
		return retval;
	}

	return 1;
}

static void linger_array_buffer_view_unset_dimension(zval *object, zval *zv_offset TSRMLS_DC)
{
	zend_throw_exception(NULL, "Cannot unset offsets in a typed array", 0 TSRMLS_CC);
}

关于以上代码的写法上没什么好解释的,但是还需要补充说明一些事情。也许细心的你看到上述代码会感到一些疑惑,为什么我们会在读操作中校验 append 呢(zv_offset == NULL), 这个与上述代码中我们没有使用到的type参数有关。对于普通的读操作,例如$foo[0]type的值为BP_VAR_R,但是在其他情况下,它也可能是BP_VAR_WBP_VAR_RWBP_VAR_IS或者BP_VAR_UNSET的一种。 为了帮助理解什么时候会产生non-read的情况,举几个例子看看:

$foo[0][1];				// [0] is a read_dimension(...,BP_VAR_R),
						// [1] is a read_dimension(...,BP_VAR_R)
$foo[0][1] = $bar;		// [0] is a read_dimension(...,BP_VAR_W), [1] is a write_dimension
$foo[][1] = $bar;		// [] is a read_dimension(...,BP_VAR_W), [1] is a write_dimension
isset($foo[0][1]);		// [0] is a read_dimension(...,BP_VAR_IS), [1] is a has_dimension
unset($foo[0][1]);		// [0] is a read_dimension(...,BP_VAR_UNSET), [1] is a unset_dimension

正如你看到的其他嵌套维度访问的时候BP_VAR的类型,在这些情况下,只有最外层的访问会调用实际的操作处理器,而内层维度的都是通过访问相应类型的读处理器进行的。 所以如果使用[]添加操作符进行嵌套维度访问,read_dimension将会被调用,而且此时的 offset 为 NULL。正常情况下,我们需要根据上下文判断type的类型,来做出相应的操作,例如: isset操作不能抛出任何错误和异常,我们通过显示的检查BP_VAR_IS来做出操作:

if (type == BP_VAR_IS) {
	return &EG(uninitialized_zval_ptr);
}

但是在我们这个特殊的 ArrayBuffer 例子中,嵌套访问并没有实际的意义,所以不用担心这种情况。

继承

有一种关键的情况我们不得不考虑,那就是继承。当你自定义一个类的 object handlers 后,他会沿着继承链一直生效。所以,如果一个类继承了 ArrayBufferView 中的其中某个类,那么它将会使用和父类同样的 handlers,也就是说 派生类将再也不能使用ArrayAccess。要解决这个问题并不难,一个非常简单的方法就是分别在相关 dimension handlers 中添加一个校验当前类是否为派生类,如果是的话就直接返回标准类的相关处理器。

if (intern->std.ce->parent) {
	return zend_get_std_object_handlers()->read_dimension(object, zv_offset, type TSRMLS_CC);
}

自定义比较处理器

static int linger_array_buffer_view_compare_objects(zval *obj1, zval *obj2 TSRMLS_DC)
{
	buffer_view_object *intern1 = zend_object_store_get_object(obj1 TSRMLS_CC);
	buffer_view_object *intern2 = zend_object_store_get_object(obj2 TSRMLS_CC);

	if (memcmp(intern1, intern2, sizeof(buffer_view_object)) == 0) {
		return 0;
	} else {
		return 1;
	}
}

自定义 get_debug_info

static HashTable *linger_array_buffer_view_get_debug_info(zval *obj, int *is_temp TSRMLS_DC)
{
	buffer_view_object *intern = zend_object_store_get_object(obj TSRMLS_CC);
	HashTable *properties = Z_OBJPROP_P(obj);
	HashTable *ht;
	int i;

	ALLOC_HASHTABLE(ht);
	ZEND_INIT_SYMTABLE_EX(ht, intern->length + zend_hash_num_elements(properties), 0);
	zend_hash_copy(ht, properties, (copy_ctor_func_t) zval_add_ref, NULL, sizeof(zval *));

	*is_temp = 1;
	for (i = 0; i < intern->length; i++) {
		zval *value = linger_buffer_view_offset_get(intern, i);
		zend_hash_index_update(ht, i, (void *) &value, sizeof(zval *), NULL);
	}

	return ht;
}

至此,我们就对之前写的ArrayBufferView完成了改造,完整的代码可以访问https://github.com/iliubang/php-ArrayBuffer.git