本篇文章基于的PHP commit为:217f6e16d625abd9ce2ae1ae92421f77945649df
我们的测试脚本如下:
1 |
|
首先,我们需要关注的第一个函数是zend_register_attribute_ce:
1 | void zend_register_attribute_ce(void) |
这个函数会在PHP模块初始化的阶段被调用,用来注册PHP内部类PhpAttribute。这个类非常的有用,类似于民间版注解的@Annotation,可以用来定义一个注解类。
OK,我们来看看zend_register_attribute_ce这个函数,其中:
1 | zend_hash_init(&internal_validators, 8, NULL, NULL, 1); |
用来初始化注解的验证器,比如说,限制这个注解只能够用在类上面。目前,验证器是空的。
1 | INIT_CLASS_ENTRY(ce, "PhpAttribute", NULL); |
用来定义一个PhpAttribute类,并且这个类是final的。
1 | zend_compiler_attribute_register(zend_ce_php_attribute, zend_attribute_validate_phpattribute); |
可以看出,zend_compiler_attribute_register主要做两件事情,第一件事情是把zend_attribute_validate_phpattribute这个验证器添加到internal_validators这个哈希表里面。
第二件事情是把PhpAttribute注解的名字添加到zend_ce_php_attribute->attributes里面。
这样,PhpAttribute这个类算是创建完了。
接下来,就开始编译我们的这个PHP脚本了。在编译的过程中,一个很重要的函数是zend_compile_attributes:
1 | static void zend_compile_attributes(HashTable **attributes, zend_ast *ast, uint32_t offset, int target) /* {{{ */ |
编译的这个ast节点它是ZEND_AST_ATTRIBUTE_LIST类型的list节点。可以看出,这实际上就开始编译我们的Bean注解了。
首先,这个list节点的第一个子节点el->child[0]是ZEND_AST_ZVAL类型的节点,里面保存了一个字符串,而这个字符串就是我们注解的名字Bean。并且,我们发现,这个字符串是通过函数zend_resolve_class_name_ast来解析的,说明这个注解的名字必须符合PHP类名的命名规范。
然后,这个list节点的第二个节点el->child[1]是ZEND_AST_ARG_LIST类型的list节点。我们可以很容易的知道,实际上就对应了Bean(1, 2)中的1和2,这两个都是ZEND_AST_ZVAL类型的节点。
在获取到args之后,调用了以下函数:
1 | zend_attribute *attr = zend_add_attribute(attributes, 0, offset, name, args ? args->children : 0); |
(其中,attributes是我们定义的Foo类的attributes哈希表)
我们来看看这个zend_add_attribute函数会做些什么事情:
1 | ZEND_API zend_attribute *zend_add_attribute(HashTable **attributes, zend_bool persistent, uint32_t offset, zend_string *name, uint32_t argc) |
其中:
1 | if (*attributes == NULL) { |
用来判断Foo类的attributes哈希表是否分配了内存,没有分配的话,就分配一下。
1 | zend_attribute *attr = pemalloc(ZEND_ATTRIBUTE_SIZE(argc), persistent); |
用来分配一个zend_attribute的内存。我们看一下ZEND_ATTRIBUTE_SIZE这个宏:
1 |
首先是求zend_attribute结构体的大小,然后再为分配argc - 1个zval的内存空间。为什么还要分配argc - 1个zval的内存空间呢?我们来看看zend_attribute这个结构体:
1 | typedef struct _zend_attribute { |
我们发现,这个结构体最后一个成员是zval argv[1],所以,我们发现,这个实际上是一个柔性数组。所以,我们需要为argv额外分配内存。而argc的大小就是2。因为我们需要保存1和2两个值。
分配完了zend_attribute内存之后,就开始使用zend_attribute了。
1 | attr->name = zend_string_copy(name); |
保存注解原始的名字,也就是Bean。
1 | attr->lcname = zend_string_tolower_ex(attr->name, persistent); |
保存注解的小写名字,也就是bean。
1 | attr->argc = argc; |
保存注解参数的个数,这里是2。
1 | zend_hash_next_index_insert_ptr(*attributes, attr); |
最后,把zend_attribute插入Foo类的attributes哈希表。通过这个操作,我们可以知道,同一个类的注解可以有多个,因为底层使用数组保存的注解信息。
我们继续回到zend_compile_attributes函数里面:
1 | for (j = 0; j < args->children; j++) { |
计算注解的两个参数的值,然后保存到对应的argv里面。
到此位置,我们已经编译完成了注解的语法树。接着,就是验证这个注解是否合法了:
1 | // Validate internal attribute |
所以,总结一下编译注解后的结果:
把注解名字和参数保存在一个zend_attribute的结构体里面,然后再把这个zend_attribute插入到对应的类结构体对象的attributes里面。这样,我们后续只要拿到了类的结构体指针,我们就可以拿到我们注解的内容,包括注解的名字和注解的参数。