PHP引擎实现(一)

本来这篇分析是作为一次内部分享而写的,然后就懒癌发作,一直没有写完,到目前也只是写了大约三分之一吧,原因之一也是PHP深入下去还是比较复杂的。最近空闲下来,还是觉得应该把这篇都写完吧。

手动分割线===================

一段PHP脚本,到底最终是如何执行的呢?我们可以通过下面这一段最简单的代码,PHP的HelloWorld,看一步步看看到底PHP是如何执行的。

<?php
    echo 'Hello ' . 'World';
    echo 'Hello ', 'World';
?>

为啥要输出两次呢,当然是刻意构造好的,下面需要用的到 :-)

首先需要说明的是,也是大家都知道的,PHP是一个脚本语言,意味着,PHP代码不用经过编译,便可以直接运行,而运行PHP脚本的虚拟机,就是Zend Engine。

首先说说什么是虚拟机,这个词我们还是能经常听的到,有虚拟机比如VMware,VirtualBox这些可以模拟一台真正的计算机,有的虚拟机比如常见的JVM,用来执行Java字节码,某种意义上来说,如Python的解释器CPython,PHP的Zend,也都是虚拟机的一种。
在计算机领域,有句名言

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。

这句话,在这里也是适用的,Intel的CPU,使用的是x86指令集,大约有2000多条指令,我们可以称之为OPCode如果加上64位扩展x86_64的话,那就更多了,这么多的指令,就使得从源代码到机器指令的”翻译”变得太复杂了。看一下JVM,大约只有100多条指令,同样的Python也只有100多条指令,这就相当于,语言的虚拟机,充当了语言本身和CPU之间的一个中间层,有了这个中间层,语言的设计就变得简单了很多。因为对于”上层”的语言来说,虚拟机就是它的 “CPU”,这个”CPU”,执行的就是这个语言的OPCode,而真正执行机器码的物理CPU,对它来说就透明不可见了。

说了这么多,还是来看看这些虚拟机的OPCode都长啥样吧。

首先,看看Java的,同样还是Hello World。

0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3; //String Hello, world!
5: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

其中 ‘#’ + 数字,就是在引用上面定义的一些静态量,这里省略了,内容就是注释里所写的。

再来看看Python的:

0 LOAD_CONST      0 // ('Hello World.')
3 PRINT_ITEM
4 PRINT_NEWLINE
5 LOAD_CONST      1 // (None)
8 RETURN_VALUE

好像很类似哦,至少风格很接近。

接下来就看看PHP的吧。

PHP有一个插件,PHP-VLD,通过它,可以把PHP脚本的OPCodes打印出来。

line   #* E I O op    fetch  ext  return  operands
---------------------------------------------------------
2   0  E >   CONCAT               ~0      'Hello+', 'World'
    1        ECHO                         ~0
3   2        ECHO                         'Hello+'
    3        ECHO                         'World'
5   4      > RETURN                       1

好像和Java/Python的不太一样了,简单看来,Java/Python的OPCode的操作参数很简单。而看一看PHP的呢,不仅操作的参数,而且还有返回值,fetch,ext这好几列。

实际上,虚拟机是一种抽象的计算机,是对真实计算机的虚拟和模拟,现在的计算机有不同的指令集架构(ISA: Instruction Set Architecture)。ISA是处理器的一个部分,不同的处理器会有不同的架构,最常见的有3种:

  • 基于栈的Stack Machines: 操作数保存在栈上。 而不是使用寄存器来保存,现在很少有真实机器采用这个模型。对于虚拟机来说因为指令空间占用少, 并且实现简单。
  • 基于累加器的Accumulator Machines。这个模型使用称作累加器(Accumulator)的的寄存器来保存 一个操作数以及操作的结果。
  • 基于通用寄存器的General-Purpose-Register Machines,这些寄存器没有特殊的用途。 编译器可以将操作数保存在这些寄存器中。

JVM和CPython就是基于栈的虚拟机,而Zend VM,则是基于通用寄存器的虚拟机,这也就是为什么Java/Python的OPCodes和PHP的看起来差距的原因。

好了,铺垫的部分说完了,下面就要说说一段PHP代码,到底是如何转换成上面的OPCodes的吧。

一般的,对于编译型语言来说,编译程序把一个源程序翻译成目标程序的工作过程分为词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成等几个阶段,而PHP这种解释型语言稍有不同,只有词法分析;语法分析和中间代码生成这三个阶段,但是,第一步永远都是词法分析。

在PHP里,提供的一个函数token_get_all(),通过这个函数,可以把一段PHP代码所有的Tokens都获取出来,相当于对这段代码进行相应的词法分析,对于开头的那段代码,词法分析的结果大概是这样的:

Array
(
    [0] => Array
        (
            [0] => 376
            [1] => <?php
            [2] => 1
        )
    [1] => Array
        (
            [0] => 379
            [1] =>         
            [2] => 2
        )
    [2] => Array
        (
            [0] => 319
            [1] => echo
            [2] => 2
        )
    [3] => Array
        (
            [0] => 379
            [1] =>  
            [2] => 2
        )
    [4] => Array
        (
            [0] => 318
            [1] => 'Hello '
            [2] => 2
        )
    [5] => Array
        (
            [0] => 379
            [1] =>  
            [2] => 2
        )
    [6] => .
    [7] => Array
        (
            [0] => 379
            [1] =>  
            [2] => 2
        )
    [8] => Array
        (
            [0] => 318
            [1] => 'World'
            [2] => 2
        )
    [9] => ;
    [10] => Array
        (
            [0] => 379
            [1] => 
        
            [2] => 2
        )
    [11] => Array
        (
            [0] => 319
            [1] => echo
            [2] => 3
        )
    [12] => Array
        (
            [0] => 379
            [1] =>  
            [2] => 3
        )
    [13] => Array
        (
            [0] => 318
            [1] => 'Hello '
            [2] => 3
        )
    [14] => ,
    [15] => Array
        (
            [0] => 379
            [1] =>  
            [2] => 3
        )
    [16] => Array
        (
            [0] => 318
            [1] => 'World'
            [2] => 3
        )
    [17] => ;
    [18] => Array
        (
            [0] => 379
            [1] => 
            
            [2] => 3
        )
    [19] => Array
        (
            [0] => 378
            [1] => ?>
            [2] => 4
        )
)

可以看到,每个Token都对应着一个数字376,379,318,319等等,这些,都是在Zend的zend_language_parser.h文件中定义的。

...
#define T_ENCAPSED_AND_WHITESPACE 317
#define T_CONSTANT_ENCAPSED_STRING 318
#define T_ECHO 319
...
#define T_OPEN_TAG 376
#define T_OPEN_TAG_WITH_ECHO 377
#define T_CLOSE_TAG 378
#define T_WHITESPACE 379
...

可以看到,每个Token都被分配了一个唯一的标识。
PHP使用了re2c来生成词法分析器,在Zend中zend_language_scanner.l 文件里,定义了re2c的规则文件,通过它,就可以生成PHP的词法分析器。生成的词法分析器就是zend_language_scanner.c文件。

参考:

  1. https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
  2. http://www.php-internals.com/book/