字节码解释器

介绍和概述

本文档描述了 PyPy 字节码解释器和相关虚拟机功能的实现。

PyPy 的字节码解释器具有类似于 CPython 虚拟机的结构:它处理从 Python 源代码解析和编译的代码对象。它在 pypy/interpreter/ 目录中实现。熟悉 CPython 实现的人会很容易识别出类似的概念。主要区别在于使用 对象空间 间接来对对象执行操作,以及内置模块的组织方式(此处 描述)。

代码对象是源代码的经过良好预处理的结构化表示,它们的主要内容是字节码。我们使用与 CPython 2.7 相同的紧凑字节码格式,在字节码集中存在细微差异。我们的字节码编译器实现为一系列灵活的传递(标记器、词法分析器、解析器、抽象语法树构建器和字节码生成器)。后期的传递基于 CPython 标准库中的 compiler 包,并进行了各种改进和错误修复。字节码编译器(位于 pypy/interpreter/astcompiler/)现在已集成,并与 PyPy 的其余部分一起翻译。

代码对象包含有关其各自函数、类和模块主体源代码的压缩信息。解释此类代码对象意味着实例化和初始化一个 帧类,然后调用其 frame.eval() 方法。此主要入口点初始化适当的命名空间,然后解释每个字节码指令。Python 的标准库包含 lib-python/2.7/dis.py 模块,该模块允许检查虚拟机的字节码指令

>>> import dis
>>> def f(x):
...     return x + 1
>>> dis.dis(f)
2         0 LOAD_FAST                0 (x)
          3 LOAD_CONST               1 (1)
          6 BINARY_ADD
          7 RETURN_VALUE

CPython 和 PyPy 是基于堆栈的虚拟机,即它们没有寄存器,而是将对象推入堆栈并从堆栈中拉出对象。字节码解释器只负责实现控制流并将黑盒对象推入和拉出此值堆栈。字节码解释器不知道如何对这些黑盒 (包装) 对象执行操作,它将这些操作委托给 对象空间。但是,为了在程序执行中实现条件分支,它需要获得有关包装对象的最小知识。因此,每个对象空间都必须提供一个 is_true(w_obj) 操作,该操作返回一个解释器级布尔值。

为了理解解释器的内部工作原理,必须认识到解释器级别和应用程序级别代码的概念。简而言之,解释器级别代码直接在机器上执行,调用应用程序级别函数会导致字节码解释的间接调用。但是,必须特别注意异常,因为应用程序级别的异常被包装成OperationErrors,因此与普通的解释器级别异常区分开来。有关OperationErrors的更多信息,请参见应用程序级别异常

解释器实现提供机制,允许调用者不知道特定函数调用是否会导致字节码解释或直接在解释器级别执行。两种基本类型的网关类分别将解释器级别函数暴露给应用程序级别执行(interp2app)或允许在解释器级别透明地调用应用程序级别帮助程序(app2interp)。

字节码解释器的另一个任务是负责将它的基本代码、框架、模块和函数对象暴露给应用程序级别代码。这种运行时自省和修改能力是通过解释器描述符实现的(另请参见 Raymond Hettingers 的Python 描述符指南,PyPy 广泛使用此模型)。

一个重要的复杂性在于函数参数解析。Python 作为一种语言,提供了灵活的方式来为特定函数调用提供和接收参数。它不仅需要特别注意正确处理这个问题,而且还给注释阶段带来了困难,注释阶段对字节码解释器、参数解析和网关代码进行全程序分析,以推断所有跨函数调用传递的值的类型。

正是由于这个原因,PyPy 采用在初始化时生成专门的框架类和函数,以便注释器只看到相当静态的程序流,在函数调用时具有同构的名称-值赋值。

字节码解释器实现类

框架类

框架的概念在执行程序中,特别是在虚拟机中是普遍存在的。它们有时被称为执行框架,因为它们保存着关于代码对象执行的关键信息,而代码对象通常与 Python 函数直接相关。框架实例保存以下状态

  • 本地作用域,保存名称-值绑定,通常通过“快速作用域”实现,它是一个包含包装对象的数组
  • 一个块栈,包含关于函数控制流的(嵌套)信息(例如whiletry结构)
  • 一个值栈,字节码解释从该栈中提取对象并向其中放入结果。(locals_stack_w实际上是一个包含本地作用域和值栈的单个列表。)
  • 全局字典的引用,包含模块级名称-值绑定
  • 调试信息,从中可以构建当前行号和文件位置以用于跟踪

此外,框架类本身还有一些方法,这些方法实现了代码对象中实际存在的字节码。PyFrame类的这些方法是在不同的文件中添加的

代码类

PyPy 的代码对象包含与 CPython 的代码对象中相同的信息。它们与函数对象不同,因为它们只是源代码的不可变表示,不包含执行状态或对框架中找到的执行环境的引用。框架和函数对代码对象有引用。以下是代码属性的列表

  • co_flags 标志,指示此代码对象是否具有嵌套作用域/生成器等。
  • co_stacksize 栈在执行代码时可以达到的最大深度
  • co_code 实际的字节码字符串
  • co_argcount 此代码对象期望的参数数量
  • co_varnames 传递给此代码对象的 所有参数名称 的元组
  • co_nlocals 局部变量的数量
  • co_names 代码对象中使用的所有名称的元组
  • co_consts 代码对象中使用的预构建常量对象(“字面量”)的元组
  • co_cellvars 包含用于从嵌套作用域访问值的单元格的元组
  • co_freevars 来自“上层”作用域的单元格名称的元组
  • co_filename 编译此代码对象的源文件
  • co_firstlineno 代码对象在其源文件中的第一个行号
  • co_name 代码对象的名称(通常是函数名称)
  • co_lnotab 用于计算与字节码对应的行号的辅助表

函数和方法类

PyPy Function 类(在 pypy/interpreter/function.py 中)表示 Python 函数。一个 Function 具有以下主要属性

  • func_doc 文档字符串(或 None)
  • func_name 函数的名称
  • func_code 表示函数源代码的 Code 对象
  • func_defaults 函数的默认值(在函数定义时构建)
  • func_dict 用于其他(用户定义的)函数属性的字典
  • func_globals 对全局字典的引用
  • func_closure 单元格引用的元组

Functions 类还提供一个 __get__ 描述符,它创建一个 Method 对象,其中包含对实例或类的绑定。最后,FunctionsMethods 都提供了一个 call_args() 方法,该方法在给定一个 Arguments 类实例的情况下执行函数。

参数类

Argument 类(在 pypy/interpreter/argument.py 中)负责解析传递给函数的参数。Python 具有相当复杂的参数传递概念

  • 位置参数
  • 按名称指定的关键字参数
  • 在函数定义时定义的位置参数的默认值
  • “星号参数”允许函数接受剩余的位置参数
  • “星号关键字参数”允许函数接受其他任意名称-值绑定

此外,一个 Function 对象可以绑定到一个类或实例,在这种情况下,底层函数的第一个参数将成为绑定对象。 Arguments 提供了允许所有这些参数解析并负责错误报告的方法。

模块类

一个 Module 实例表示通常从执行模块的源文件构建的执行状态。除了这样的模块的全局 __dict__ 字典之外,它还具有以下应用程序级属性

  • __doc__ 模块的文档字符串
  • __file__ 从中实例化此模块的源文件名
  • __path__ 用于相对导入的状态

除了用于导入应用程序级文件的基本模块外,还有一个更精细的 MixedModule 类(参见 pypy/interpreter/mixedmodule.py),它允许在应用程序级别和解释器级别定义名称-值绑定。参见 __builtin__ 模块的 pypy/module/__builtin__/__init__.py 文件以获取示例,以及更高层次的 编码指南中的模块章节

网关类

PyPy 的一个独特特性是能够轻松跨越解释型代码和机器级代码之间的障碍(通常称为 解释器级别和应用程序级别 之间的区别)。请注意,用于双向跨越障碍的相应代码(在 pypy/interpreter/gateway.py 中)比较复杂,主要是因为类型推断注释器需要跟踪跨越这些障碍的对象类型。

使解释器级别函数在应用程序级别可用

为了使解释器级别函数在应用程序级别可用,可以调用 pypy.interpreter.gateway.interp2app(func)。此类函数通常接受一个 space 参数和任意数量的位置参数。此外,此类函数可以定义一个 unwrap_spec,告诉 interp2app 逻辑如何在实际调用解释器级别函数之前解包应用程序级别提供的参数。例如,解释器描述符(例如用于分配和构造模块实例的 Module.__new__ 方法)使用此类代码定义

Module.typedef = TypeDef("module",
    __new__ = interp2app(Module.descr_module__new__.im_func,
                         unwrap_spec=[ObjSpace, W_Root, Arguments]),
    __init__ = interp2app(Module.descr_module__init__),
                    # module dictionaries are readonly attributes
    __dict__ = GetSetProperty(descr_get_dict, cls=Module),
    __doc__ = 'module(name[, doc])\n\nCreate a module object...'
    )

上面提到的 __new__ 关键字参数引用的实际 Module.descr_module__new__ 解释器级别方法定义如下

def descr_module__new__(space, w_subtype, __args__):
    module = space.allocate_instance(Module, w_subtype)
    Module.__init__(module, space, None)
    return space.wrap(module)

总结一下,interp2app 机制负责将应用程序级别的访问或调用适当路由到内部解释器级别对象,并提供足够的精度和提示以使类型推断注释器满意。

从解释器级别调用应用程序级别代码

应用程序级别代码 通常更可取。因此,我们通常希望从解释器级别调用应用程序级别代码。这通过网关的 app2interp 机制完成,我们通常在模块的定义时调用它。它生成一个钩子,看起来像一个解释器级别函数,接受一个空间和任意数量的参数。在解释器级别调用函数时,调用方通常不需要知道其调用的函数是通过 PyPy 解释器运行的,还是直接在机器上执行(在翻译之后)。

以下是一个示例,展示了如何在 PyPy 中实现 Python 语言的元类查找算法

app = gateway.applevel(r'''
    def find_metaclass(bases, namespace, globals, builtin):
        if '__metaclass__' in namespace:
            return namespace['__metaclass__']
        elif len(bases) > 0:
            base = bases[0]
            if hasattr(base, '__class__'):
                    return base.__class__
            else:
                    return type(base)
        elif '__metaclass__' in globals:
            return globals['__metaclass__']
        else:
            try:
                return builtin.__metaclass__
            except AttributeError:
                return type
''', filename=__file__)

find_metaclass  = app.interphook('find_metaclass')

解释器级别钩子 find_metaclass 使用来自 pypy/interpreter/pyopcode.py 中的 BUILD_CLASS 操作码实现的五个参数调用

def BUILD_CLASS(f):
    w_methodsdict = f.valuestack.pop()
    w_bases       = f.valuestack.pop()
    w_name        = f.valuestack.pop()
    w_metaclass = find_metaclass(f.space, w_bases,
                                 w_methodsdict, f.w_globals,
                                 f.space.wrap(f.builtin))
    w_newclass = f.space.call_function(w_metaclass, w_name,
                                       w_bases, w_methodsdict)
    f.valuestack.push(w_newclass)

请注意,在后面的某个时刻,我们可以重写解释器级别的 find_metaclass 实现,而无需修改调用方。

内省和描述符

Python 传统上对字节码解释器相关对象有一个非常广泛的内省模型。在 PyPy 和 CPython 中,对这些对象的读写访问被路由到描述符。当然,在 CPython 中,这些是在 C 中实现的,而在 PyPy 中,它们是在解释器级别的 Python 代码中实现的。

所有 函数代码模块 类的实例也是 W_Root 实例,这意味着它们可以在应用程序级别表示。如今,PyPy 对象空间在遇到对解释器级别对象的访问时,需要使用基本描述符查找:对象空间通过 getclass 方法向包装对象询问其类型,然后调用类型的 lookup(name) 函数以接收描述符函数。大多数 PyPy 的内部对象描述符定义在 pypy/interpreter/typedef.py 的末尾。您可以使用这些定义作为应用程序级别可见的解释器类属性的参考。