本篇文章基于的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里面。这样,我们后续只要拿到了类的结构体指针,我们就可以拿到我们注解的内容,包括注解的名字和注解的参数。