标签 - PHP内核

PHP内核    2019-04-25 16:24:17    8    0    0

成员方法从本质上来讲也是一种函数,所以其存储结构也和常规函数一样,存储在zend_function结构体中。 对于一个类的多个成员方法,它是以HashTable的数据结构存储了多个zend_function结构体。 和前面的成员变量一样,在类声明时成员方法也通过调用zend_initialize_class_data方法,初始化了整个方法列表所在的HashTable。 在类中我们如果要定义一个成员方法,格式如下:

1class Tipi{
2    public function t() {
3        echo 1;
4    }
5}

除去访问控制关键字,一个成员方法和常规函数是一样的,从语法解析中调用的函数一样(都是zend_do_begin_function_declaration函数), 但是其调用的参数有一些不同,第三个参数is_method,成员方法的赋值为1,表示它作为成员方法的属性。 在这个函数中会有一系统的编译判断,比如在接口中不能声明私有的成员方法。 看这样一段代码:

1interface Ifce {
2   private function method();
3}

如果直接运行,程序会报错:Fatal error: Access type for interface method Ifce::method() must be omitted in 这段代码对应到zend_do_begin_function_declaration函数中的代码,如下:

01if (is_method) {
02    if (CG(active_class_entry)->ce_flags & ZEND_ACC_INTERFACE) {
03        if ((Z_LVAL(fn_flags_znode->u.constant) & ~(ZEND_ACC_STATIC|ZEND_ACC_PUBLIC))) {
04            zend_error(E_COMPILE_ERROR, "Access type for interface method %s::%s() must be omitted",
05                CG(active_class_entry)->name, function_name->u.constant.value.str
PHP内核    2019-04-25 16:23:12    11    0    0

在上一小节,我们介绍了类的结构和声明过程,从而,我们知道了类的存储结构,接口抽象类等类型的实现方式。 在本小节,我们将介绍类的成员变量和成员方法。首先,我们看一下,什么是成员变量,什么是成员方法。

类的成员变量在PHP中本质上是一个变量,只是这些变量都归属于某个类,并且给这些变量是有访问控制的。 类的成员变量也称为成员属性,它是现实世界实体属性的抽象,是可以用来描述对象状态的数据。

类的成

PHP内核    2019-04-25 16:22:33    11    0    0

面向对象编程中我们的编程都是围绕类和对象进行的。那在PHP内部类是怎么实现的呢? 它的内存布局以及存储是怎么样的呢?继承、封装和多态又是怎么实现的呢?

类的结构

首先我们看看类是什么。类是用户定义的一种抽象数据类型,它是现实世界中某些具有共性事物的抽象。 有时我们也可以理解其为对象的类别。类也可以看作是一种复合型的结构,其需要存储多元化的数据, 如属性、方法、以及自身的一些性质等。

类和函数类似,PHP内置及PHP扩展均可以实现自己的内部类,也可以由用户使用PHP代码进行定义。 当然我们在编写代码时通常是自己定义。

使用上,我们使用class关键字进行定义,后面接类名,类名可以是任何非PHP保留字的名字。 在类名后面紧跟着一对花括号,里面是类的实体,包括类所具有的属性,这些属性是对象的状态的抽象, 其表现为PHP中支持的数据类型,也可以包括对象本身,通常我们称其为成员变量。 除了类的属性, 类的实体中也包括类所具有的操作,这些操作是对象的行为的抽象,其表现为用操作名和实现该操作的方法, 通常我们称其为成员方法或成员函数。看类示例的代码:

01class ParentClass {
02}
03  
04interface Ifce {
05        public function iMethod();
06}
07  
08final class Tipi extends ParentClass implements Ifce {
09        public static $sa 'aaa';
10        const CA = 'bbb';
11  
12        public function __constrct() {
13        }
14  
15        public function iMethod() {
16        }
17  
18        private function _access() {
19        }
20  
21        public static function access() {
22        }
23}

这里定义了一个父类ParentClass,一个接口Ifce,一个子类Tipi。子类继承父类ParentClass, 实现接口Ifce,并且有一个静态变量$sa,一个类常量 CA,一个公用方法,一个私有

PHP内核    2019-04-25 16:21:57    16    0    0

面向对象是一种编程范式,它将对象作为程序的基本单元,将程序和数据封装起来, 以此来提高程序的重用性、灵活性和可扩展性。

目前很多语言都支持面向对象编程,既然对象对象是一种范式,其实这就和具体的编程语言没有直接关系, 只不过很多语言将这个范式作为语言的基本元素,使用C语言也能够进行面向对象编程。

面向对象的程序设计中包含:

  1. 类。类是具体事物的抽象。通常类定义了事物的属性和所能完成的工作。有一点需要注意, 并不是所有的面向对象编程语言的类都具有class这个明确的实体。例如Javascript就不是基于类的。 Javascript中的类(Function)也具有类定义的特性。这也印证了面向对象只是一种编程范式。
  2. 对象。对象是类的实例。对象是具体的。
  3. 方法。方法是类定义对象可以做的事情。
  4. 继承性。继承是类的具体化,子类是比具备更多特性和行为的类。面向对象是对现实世界的一个抽象。 在很多时候的关系并不一定是继承关系。能在一定程序上实现代码的重用。
  5. 封装性、抽象性。封装性能实现的复杂性隐藏,减少出错的可能。

从我们接触PHP开始,我们最先遇到的是函数:数组操作函数,字符串操作函数,文件操作函数等等。 这些函数是我们使用PHP的基础,也是PHP自出生就支持的面向过程编程。面向过程将一个个功能封装, 以一种模块化的思想解决问题。

面向对象听起来很美,但是现实中的编程语言中很少有纯粹的面向对象的语言, 处于性能或者程序员的开发习惯,通常的编程语言都同时支持两种变编程方式。

PHP就是如此,从PHP4起开始支持面向对象编程。但PHP4的面向对象支持不太完善。 从PHP5起,PHP引入了新的对象模型(Object Model),增加了许多新特性,包括访问控制、 抽象类和final类、类方法、魔术方法、接口、对象克隆和类型提示等。 并且在近期发布的PHP5.3版本中,针对面向对象编程增加了命名空间、延迟静态绑定(Late Static Binding) 以及增加了两个魔术方法__callStatic()和__invoke()。

PHP中对象是按引用传递的,即对象进行赋值和操作的时候是按引用(reference)传递的,而不是整个对象的拷贝。

这一章我们从面向对象讲起,会说到PHP中的类,包括类的定义和实现、接口、抽象类以及与类相关的访问控制、 对象和命名空间等。除此之外也会从其存储的内部结构,类的单继承的实现,

PHP内核    2019-04-25 16:20:45    13    0    0

匿名函数在编程语言中出现的比较早,最早出现在Lisp语言中,随后很多的编程语言都开始有这个功能了, 目前使用比较广泛的Javascript以及C#,PHP直到5.3才开始真正支持匿名函数, C++的新标准C++0x也开始支持了。

匿名函数是一类不需要指定标示符,而又可以被调用的函数或子例程,匿名函数可以方便的作为参数传递给其他函数, 最常见应用是作为回调函数。

闭包(Closure)

说到匿名函数,就不得不提到闭包了,闭包是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数, 这个被应用的自由变量将和这个函数一同存在,即使离开了创建它的环境也一样,所以闭包也可认为是有函数和与其相关引用组合而成的实体。 在一些语言中,在函数内定义另一个函数的时候,如果内部函数引用到外部函数的变量,则可能产生闭包。在运行外部函数时, 一个闭包就形成了。

这个词和匿名函数很容易被混用,其实这是两个不同的概念,这可能是因为很多语言实现匿名函数的时候允许形成闭包。

使用create_function()创建"匿名"函数

前面提到PHP5.3中才才开始正式支持匿名函数,说到这里可能会有细心读者有意见了,因为有个函数是可以生成匿名函数的: create_function函数, 在手册里可以查到这个函数在PHP4.1和PHP5中就有了,这个函数通常也能作为匿名回调函数使用, 例如如下:

1<?php
2$array array(1, 2, 3, 4);
3array_walk($array, create_function('$value''echo $value'));
4?>

这段代码只是将数组中的值依次输出,当然也能做更多的事情。 那为什么这不算真正的匿名函数呢, 我们先看看这个函数的返回值,这个函数返回一个字符串, 通常我们可以像下面这样调用一个函数:

1<?php
2function a() {
3    echo 'function a';
4}
5  
6$a 'a';
7$a();
8?>

我们在实现回调函数的时候也可以采用这样的方式,例如:

1<?php
2function do_something($callback) {
3    // doing
4    # ...
5  
6    // done
7    $callback();
8}
9?>

这样就能实现在函数do_something()执

PHP内核    2019-04-25 16:18:59    12    0    0

一个函数的执行结果要返回给调用者,除了使用return功能,还有一种办法,那就是以引用的形式传递参数,然后在内部修改这个参数的值。前一种方法往往只能返回一个值,如果我们的函数执行结果具有多种数据,便需要把这些数据打包到一个数组、类等复合类型的变量中才能得以实现;但后一种方法相比而言就简单一些了。

运行时传递引用:Call-time Pass-by-ref

标题有点绕口,其实很简单,功能如以下php代码所示:

01<?php
02function byref_calltime($a) {
03    $a '(modified by ref!)';
04}
05 
06$foo 'I am a string';
07 
08//使用&传递引用
09byref_calltime(&$foo);
10echo $foo;
11//输出'(modified by ref!)'
12?>

我们在传递参数的时候使用&操作符,便可以传递$foo变量的引用过去,而不是copy一份。当我们在函数内核修改这个参数时,函数外部的$foo也跟着被一起修改了。同样的功能我们如何在扩展里实现呢,其实很简单,请看下面的源码:

01ZEND_FUNCTION(byref_calltime)
02{
03    zval *a;
04 
05    //我们我接收的参数传给zval *a;
06    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &a) == FAILURE)
07    {
08        RETURN_NULL();
09    }
10     
11    //如果a不是以应用的方式传递的。
12    if (!a->is_ref__gc)
13    {
14        return;
15    }
16     
17    //将a转成字符串
18    convert_to_string(a);
19     
20    //更改数据
21    ZVAL_STRING(a," (modified by ref!)",1);
22    return;
23}

编译时的传递引用Compile-time Pass-by-ref

如果每一次都在调用函数时候都对参数加一个&符号真是太罗嗦了,有没有一个简单的办法呢,比如在定义函数的时候便声明这个参数是引用形式的

PHP内核    2019-04-25 16:14:08    15    0    0

前面对函数的内部表示以及参数的传递,返回值都有了介绍,那函数是怎么被调用的呢?内置函数和用户定义函数在调用时会有什么不一样呢? 下面将介绍函数调用和执行的过程。

函数的调用

函数被调用需要一些基本的信息,比如函数的名称,参数以及函数的定义(也就是最终函数是怎么执行的), 从我们开发者的角度来看, 定义了一个函数我们在执行的时候自然知道这个函数叫什么名字,以及调用的时候给传递了什么参数,以及函数是怎么执行的。 但是对于Zend引擎来说,它并不能像我们这样能“看懂”php源代码,他们需要对代码进行处理以后才能执行。我们还是从以下两个小例子开始:

1<?php
2    function foo(){
3        echo "I'm foo!";
4    }  
5    foo();
6?>

下面我们先看一下其对应的opcodes:

01function name:  (null)
02line     # *  op                           fetch          ext  return  operands
03---------------------------------------------------------------------------------
04              DO_FCALL                                      0          'foo'
05              NOP                                                     
06            > RETURN                                                   1
07  
08function name:  foo
09line     # *  op                           fetch          ext  return  operands
10---------------------------------------------------------------------------------
11   4     0  >   ECHO             
PHP内核    2019-04-25 16:13:35    17    0    0

PHP语言中函数的返回值是通过return来完成了,就像下面的程序:

1<?php
2function sample_long() {
3  return 42;
4}
5$bar = sample_long();
6?>

C语言也一样使用return关键字:

1int sample_long(void) {
2    return 42;
3}
4int main(void) {
5    int bar = sample_long();
6    return 1;
7}

那我们在扩展中编写的PHP函数如何把返回值回馈给用户端的函数调用者呢?看好,这里指的是回馈给,而不是单单的return~

你也许会认为扩展中定义的函数应该直接通过return关键字来返回一个值,比如由你自己来生成一个zval并返回,就像下面这样:

1ZEND_FUNCTION(sample_long_wrong)
2{
3    zval *retval;
4 
5    MAKE_STD_ZVAL(retval);
6    ZVAL_LONG(retval, 42);
7 
8    return retval;
9}

但是,上面的写法是无效的!与其让扩展开发员每次都初始化一个zval并return之,zend引擎早就准备好了一个更好的方法。它在每个zif函数声明里加了一个zval*类型的形参,名为return_value,专门来解决返回值这个问题。在前面我们已经知道了ZEND_FUNCTION宏展开后是void name(INTERNAL_FUNCTION_PARAMETERS)的形式,现在是我们展开代表参数声明的INTERNAL_FUNCTION_PARAMETERS宏的时候了。

1#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC
  • int ht
  • zval *return_value,我们在函数内部修改这个指针,函数执行完成后,内核将把这个指针指向的zval返回给用户端的函数调用者。
  • zval **return_value_ptr,
  • zval *this_ptr,如果此函数是一个类的方法,那么这个指针的含义和PHP语言中$th
PHP内核    2019-04-25 16:13:08    40    0    0

在编程语言中,一个函数或一个方法一般都有返回值,但也存在不返回值的情况,此时,这些函数仅仅仅是处理一些事务, 没有返回,或者说没有明确的返回值,在pascal语言中它有一个专有的关键字 procedure 。 在PHP中,函数都有返回值,分两种情况,使用return语句明确的返回和没有return语句返回NULL。

return语句

当使用return语句时,PHP给用户自定义的函数返回指定类型的变量。 依旧我们查看源码的方式,对return 关键字进行词法分析和语法分析后,生成中间代码。 从 Zend/zend_language_parser.y文件中可以确认其生成中间代码调用的是 zend_do_return 函数。

01void zend_do_return(znode *expr, int do_end_vparse TSRMLS_DC) /* {{{ */
02{
03    zend_op *opline;
04    int start_op_number, end_op_number;
05  
06    if (do_end_vparse) {
07        if (CG(active_op_array)->return_reference
08                && !zend_is_function_or_method_call(expr)) {
09            zend_do_end_variable_parse(expr, BP_VAR_W, 0 TSRMLS_CC);/* 处理返回引用 */
10        else {
11            zend_do_end_variable_parse(expr, BP_VAR_R, 0 TSRMLS_CC);/* 处理常规变量返回 */
12        }
13    }
14  
15   ...// 省略  取其它中间代码操作
16  
17    opline->opcode = ZEND_RETURN;
18  
19    if (expr) {
20        opline->op1 = *expr;
21  
22        if (do_end_vparse && zend_is_function_or_method_call(expr)) {
23         
PHP内核    2019-04-25 16:12:44    20    0    0

最简单的获取函数调用者传递过来的参数便是使用zend_parse_parameters()函数。

zend_parse_parameters()函数的前几个参数我们直接用内核里宏来生成便可以了,形式为:ZEND_NUM_ARGS() TSRMLS_CC,注意两者之间有个空格,但是没有逗号。从名字可以看出,ZEND_NUM_ARGS()代表这参数的个数。

紧接着需要传递个zend_parse_parameters()函数的参数是一个用于格式化的字符串,就像printf的第一个参数一样。下面表示了最常用的几个符号。

01type_spec是格式化字符串,其常见的含义如下:
02参数   代表着的类型
03b   Boolean
04l   Integer 整型
05d   Floating point 浮点型
06s   String 字符串
07r   Resource 资源
08a   Array 数组
09o   Object instance 对象
10O   Object instance of a specified type 特定类型的对象
11z   Non-specific zval 任意类型~
12Z   zval**类型
13f   表示函数、方法名称,PHP5.1里貌似木有... ...

这个函数就像printf()函数一样,后面的参数是与格式化字符串里的格式一一对应的。一些基础类型的数据会直接映射成C语言里的类型。

01ZEND_FUNCTION(sample_getlong)
02{
03    long foo;
04    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"l", &foo) == FAILURE)
05    {
06        RETURN_NULL();
07    }
08    php_printf("The integer value of the parameter is: %ld\n", foo);
09    RETURN_TRUE;
10}

一般来说,int和long这两种数据类型的数据往往是相同的,但也有例外情况。所以我们不应改把long的数组放在一个int里,尤其是在64位平台里,那将引发一些不容易排查的Bug。所以通过zend_parse_parameter()函数接收参数时,我们应该使用内

3/9