深入探索RustPython:用Rust构建的Python解释器

深入探索RustPython:用Rust构建的Python解释器

本文深度讲解在github上收获20.8k Star的爆火项目—RustPython,文章和一般讲解文章不同,旨在让读者更好的理解 RustPython 的整体架构,所以顺序按概述、核心虚拟机、对象系统、标准库、编译管道、项目组织的结构走,单独读本文可能你并不能很好的理解,但是读过本文之后你在去精读 RustPython 你会发现更容易上手。

原文地址:RustPython

1. 概述

在编程语言的世界里,跨语言的融合与创新从未停止。而我们今天的主角RustPython就是一个完全用Rust语言实现的Python 3解释器,这一特性让它在众多Python实现中脱颖而出。RustPython的目标非常明确,它致力于提供一个完全的Python - 3环境,且完全由Rust实现,而非依赖CPython的绑定。同时,它追求一种简洁的实现方式,避免兼容性方面的hack手段。这种设计理念使得RustPython具有独特的优势,它可以作为一个库和shell环境使用,借助Rust的特性,让Python能够被用作Rust应用程序的编程语言,还能立即通过WebAssembly在浏览器中编译运行,让用户可以轻松地在浏览器中运行Python代码。

2. 核心虚拟机

2.1 虚拟机的作用与地位

在RustPython中,核心虚拟机(VM)扮演着至关重要的角色,它是执行Python字节码的核心组件,负责将编译生成的字节码转化为实际的操作并运行。rustpython-vm这个crate承担了运行虚拟机以执行Python指令的重要工作,vm/src目录包含了实现读取和评估循环的代码,这些循环用于获取和分发指令,是虚拟机运作的核心环节。

虚拟机的存在使得Python代码能够脱离源代码,以更高效的字节码形式被执行。对于程序设计人员来说,理解虚拟机的工作原理有助于深入掌握RustPython的运行机制,从而更好地进行代码优化和调试。

2.2 虚拟机的实现结构

从代码组织来看,vm/src目录下的结构清晰地反映了虚拟机的组成部分。其中,vm/src/builtins目录存放了用于表示不同Python对象及其方法的Rust代码,vm/src/stdlib则是用Rust实现的Python标准库模块。

虚拟机目前被实现为一个栈式机器,这意味着它使用一个栈来存储中间结果。这种设计在很多解释型语言的虚拟机中较为常见,栈式结构简单直观,便于实现和理解。

以下是一些与虚拟机相关的代码片段,能帮助我们更好地理解其结构:

vm/Cargo.toml中,定义了rustpython-vm crate的相关信息和依赖:

[package]
name = "rustpython-vm"
description = "RustPython virtual machine."
include = ["src/**/*.rs", "Cargo.toml", "build.rs", "Lib/**/*.py"]
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
repository.workspace = true
license.workspace = true

[features]
default = ["***piler", "wasmbind", "stdio"]
stdio = []
importlib = []
encodings = ["importlib"]
vm-tracing-logging = []
flame-it = ["flame", "flamer"]
freeze-stdlib = ["encodings"]
jit = ["rustpython-jit"]
threading = ["rustpython-***mon/threading"]
***piler = ["parser", "codegen", "rustpython-***piler"]
ast = ["ruff_python_ast", "ruff_text_size"]
codegen = ["rustpython-codegen", "ast"]
parser = ["ast"]
serde = ["dep:serde"]
wasmbind = ["rustpython-***mon/wasm_js", "chrono/wasmbind", "wasm-bindgen"]

[dependencies]
rustpython-***piler = { workspace = true, optional = true }
rustpython-codegen = { workspace = true, optional = true }
rustpython-***mon = { workspace = true }
rustpython-derive = { workspace = true }
rustpython-jit = { workspace = true, optional = true }

# 其他依赖...

这些配置信息展示了虚拟机所依赖的其他组件,如编译器、代码生成器等,以及一些可选功能,如JIT编译、WebAssembly绑定等,这也体现了虚拟机在整个RustPython项目中的核心地位,需要与多个模块协同工作。

2.3 虚拟机的工作流程

虚拟机的工作流程大致可以分为几个关键步骤:接收字节码、解析字节码指令、执行指令并操作栈、处理异常等,流程如下所示:

当Python代码经过编译生成字节码后,虚拟机会获取这些字节码。然后,虚拟机通过一个循环来逐条读取字节码指令。对于每条指令,虚拟机进行解析,确定需要执行的操作,例如加载变量、执行算术运算、调用函数等。

在执行过程中,栈式机器的特性得到充分体现。例如,当执行加法运算时,虚拟机会从栈顶弹出两个操作数,进行加法运算后,再将结果压入栈中。这种基于栈的操作方式使得指令的实现相对简单,每条指令只需要关注对栈的操作即可。

以下是src/lib.rs中与虚拟机运行相关的代码片段,展示了虚拟机如何处理不同的运行模式:

fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> PyResult<()> {
    #[cfg(feature = "flame-it")]
    let main_guard = flame::start_guard("RustPython main");

    let scope = setup_main_module(vm)?;

    if !vm.state.settings.safe_path {
        // 处理路径相关设置
        vm.run_code_string(
            vm.new_scope_with_builtins(),
            "import sys; sys.path.insert(0, '')",
            "<embedded>".to_owned(),
        )?;
    }

    // 处理site模块导入
    let site_result = vm.import("site", 0);
    if site_result.is_err() {
        warn!(
            "Failed to import site, consider adding the Lib directory to your RUSTPYTHONPATH \
             environment variable",
        );
    }

    let is_repl = matches!(run_mode, RunMode::Repl);
    if !vm.state.settings.quiet
        && (vm.state.settings.verbose > 0 || (is_repl && std::io::stdin().is_terminal()))
    {
        // 输出欢迎信息
        eprintln!(
            "Wel***e to the magnificent Rust Python {} interpreter \u{1f631} \u{1f596}",
            env!("CARGO_PKG_VERSION")
        );
        eprintln!(
            "RustPython {}.{}.{}",
            vm::version::MAJOR,
            vm::version::MINOR,
            vm::version::MICRO,
        );

        eprintln!("Type \"help\", \"copyright\", \"credits\" or \"license\" for more information.");
    }
    let res = match run_mode {
        RunMode::***mand(***mand) => {
            debug!("Running ***mand {***mand}");
            vm.run_code_string(scope.clone(), &***mand, "<stdin>".to_owned())
                .map(drop)
        }
        RunMode::Module(module) => {
            debug!("Running module {module}");
            vm.run_module(&module)
        }
        RunMode::InstallPip(installer) => install_pip(installer, scope.clone(), vm),
        RunMode::Script(script) => {
            debug!("Running script {}", &script);
            vm.run_script(scope.clone(), &script)
        }
        RunMode::Repl => Ok(()),
    };
    if is_repl || vm.state.settings.inspect {
        shell::run_shell(vm, scope)?;
    } else {
        res?;
    }

    #[cfg(feature = "flame-it")]
    {
        main_guard.end();
        if let Err(e) = write_profile(&vm.state.as_ref().settings) {
            error!("Error writing profile information: {}", e);
        }
    }
    Ok(())
}

这段代码展示了虚拟机在不同运行模式(如执行命令、模块、脚本、REPL等)下的处理逻辑。它首先设置主模块,处理路径和模块导入,然后根据不同的运行模式执行相应的操作,最后处理性能分析等后续工作。这清晰地体现了虚拟机作为核心执行组件的角色,能够根据不同的输入形式来执行相应的Python代码。

2.4 虚拟机的关键特性

RustPython的虚拟机具有一些关键特性,使得它能够高效、准确地执行Python代码。

  1. 它支持多种运行模式,如执行单个命令、运行模块、执行脚本以及交互式的REPL模式,这为用户提供了灵活的使用方式,满足不同场景下的需求。
  2. 虚拟机具有良好的可扩展性。通过vm.add_native_modulevm.add_frozen等方法,可以向虚拟机添加自定义的原生模块和冻结的代码,这使得开发者能够根据自己的需求扩展RustPython的功能。

例如,在src/lib.rs的示例代码中:

use rustpython_vm::{pymodule, py_freeze};
fn main() {
    rustpython::run(|vm| {
        vm.add_native_module("my_mod".to_owned(), Box::new(my_mod::make_module));
        vm.add_frozen(py_freeze!(source = "def foo(): pass", module_name = "other_thing"));
    });
}

#[pymodule]
mod my_mod {
    use rustpython_vm::builtins::PyStrRef;

    #[pyfunction]
    fn do_thing(x: i32) -> i32 {
        x + 1
    }

    #[pyfunction]
    fn other_thing(s: PyStrRef) -> (String, usize) {
        let new_string = format!("hello from rust, {}!", s);
        let prev_len = s.as_str().len();
        (new_string, prev_len)
    }
}

这段代码展示了如何向虚拟机添加自定义的原生模块和冻结的代码,体现了虚拟机的可扩展性。开发者可以通过这种方式将自己的Rust代码集成到RustPython中,为Python提供新的功能。

  1. 虚拟机还具备一些高级特性的支持,如可选的JIT(即时编译)功能。通过启用jit特性,虚拟机可以尝试对热点代码进行即时编译,将字节码转化为机器码,从而提高代码的执行效率。虽然这一功能目前还处于实验阶段,但它展示了RustPython在性能优化方面的潜力。

2.5 虚拟机与其他组件的交互

虚拟机并非孤立存在,它需要与RustPython的其他组件紧密协作,才能完成Python代码的执行过程。

  1. 虚拟机与编译器关系密切。编译器负责将Python源代码编译为字节码,而虚拟机则负责执行这些字节码。在rustpython-vm的依赖中,rustpython-***piler是一个可选依赖,当启用***piler特性时,虚拟机可以利用编译器来处理代码的编译过程。

  2. 虚拟机还与解析器(parser)和代码生成器(codegen)存在交互。解析器将Python源代码转换为抽象语法树(AST),代码生成器则将AST转换为字节码,虚拟机则接收并执行这些字节码。这种协作关系确保了从源代码到最终执行结果的完整流程。

  3. 虚拟机还需要与标准库和内置对象进行交互。vm/src/stdlib目录下的标准库模块和vm/src/builtins目录下的内置对象为虚拟机提供了丰富的功能支持,使得Python代码能够调用各种标准函数和操作各种内置类型。

例如,当Python代码中调用print函数时,虚拟机会找到对应的内置函数实现,并执行相应的操作,这背后就涉及到虚拟机与内置对象模块的交互。

3. 对象系统

3.1 对象系统的核心概念

在Python中,有一个核心的理念:“一切皆对象”。RustPython完全遵循这一理念,其对象系统是实现这一理念的基础。在RustPython中,所有的数据类型,无论是基本类型(如整数、字符串)还是复杂类型(如列表、字典、函数等),都被表示为对象。

对象系统的核心作用是统一管理和操作这些不同类型的数据,使得它们能够在虚拟机中被一致地处理。对于程序设计人员来说,理解RustPython的对象系统有助于理解Python中各种数据类型的行为和交互方式。

3.2 对象的核心实现

RustPython中对象的核心实现位于vm/src/object/core.rs文件中。在这个文件中,定义了Python对象的基本结构和行为,为所有具体的对象类型提供了统一的接口。

在Rust中,由于其强类型和所有权特性,实现一个灵活的对象系统需要一些特殊的设计。RustPython通过定义PyObject trait或类似的结构来实现对象的多态性,使得不同类型的对象能够被统一处理。

以下是对对象核心实现的一些关键方面的分析:

  • 对象的表示:在RustPython中,对象通常被表示为一种引用类型,可能使用Rc(引用计数)或Arc(原子引用计数)来管理内存,确保对象在适当的时候被释放。这种内存管理方式与Python的垃圾回收机制类似,通过引用计数来跟踪对象的使用情况。

  • 对象的类型信息:每个对象都包含类型信息,这使得虚拟机能够知道对象的类型,并调用相应的方法。类型信息通常包括对象的名称、方法列表、属性等。

  • 通用方法:核心对象定义了一些通用的方法,如__repr____str____add__等,这些方法为对象提供了基本的行为。具体的对象类型可以重写这些方法,以实现特定的功能。

3.3 内置对象的实现

vm/src/builtins目录包含了各种Python内置对象及其方法的Rust实现。这些内置对象包括整数(int)、字符串(str)、列表(list)、字典(dict)、函数(function)等,它们是Python编程的基础。

每种内置对象都有其特定的实现方式,以满足Python语言规范对该类型的要求。

以整数对象为例,在RustPython中,整数对象需要支持各种算术运算、比较操作等。其实现可能基于Rust的整数类型或大整数库,以支持任意精度的整数运算,符合Python中整数的特性。

以下是一个假设的整数对象方法实现的示例(基于RustPython的设计理念):

// 假设的整数对象结构
struct PyInt {
    value: i64, // 或者使用大整数类型
}

impl PyInt {
    // 加法运算
    fn __add__(&self, other: &dyn PyObject) -> Result<Box<dyn PyObject>, Error> {
        if let Some(other_int) = other.downcast_ref::<PyInt>() {
            Ok(Box::new(PyInt {
                value: self.value + other_int.value,
            }))
        } else {
            Err(Error::TypeError("Unsupported operand type for +".to_string()))
        }
    }

    // 其他方法...
}

这个示例展示了整数对象的加法方法如何实现,它首先检查另一个操作数是否也是整数对象,如果是则进行加法运算,否则返回类型错误。这符合Python中对整数加法的行为定义。

对于字符串对象,其实现需要支持字符串的拼接、切片、查找等操作。Rust的字符串处理能力为实现这些功能提供了良好的基础,但需要适配Python的字符串语义,如Unicode支持、不可变性等。

列表对象则需要支持动态添加、删除元素,切片操作等。其内部可能基于Rust的Vec数据结构来实现,以高效地管理元素的存储和访问。

3.4 对象的方法调用机制

在RustPython中,对象的方法调用是一个复杂但关键的过程。当调用一个对象的方法时,虚拟机会经历以下几个步骤:

  1. 查找方法:虚拟机会根据方法名在对象的类型信息中查找对应的方法。如果找不到,则会沿着继承链向上查找,直到找到为止或确定方法不存在。

  2. 参数处理:方法通常需要接收参数,虚拟机会对传入的参数进行处理,包括类型检查、参数数量匹配等,以确保方法能够正确接收和处理参数。

  3. 执行方法:找到方法并处理好参数后,虚拟机会执行方法的代码。方法的代码可能是用Rust实现的内置方法,也可能是用Python编写的字节码。

  4. 返回结果:方法执行完成后,会返回一个结果对象,虚拟机会将这个结果对象压入栈中,供后续操作使用。

例如,当调用列表的append方法时,虚拟机会在列表对象的类型信息中找到append方法,检查传入的参数是否符合要求,然后执行append方法的代码,将元素添加到列表中,并返回None对象。

3.5 对象的内存管理

RustPython的对象系统采用了引用计数的内存管理方式,这与CPython类似。每个对象都有一个引用计数,当对象被创建或被引用时,引用计数增加;当对象的引用被释放时,引用计数减少。当引用计数变为零时,对象所占用的内存会被释放。

这种内存管理方式简单有效,能够及时释放不再使用的对象,避免内存泄漏。但它也存在一些局限性,例如无法处理循环引用的情况。为了解决这个问题,RustPython可能会在未来引入垃圾回收机制,以补充引用计数的不足。

在Rust中实现引用计数可以使用RcArc类型。Rc适用于单线程环境,而Arc适用于多线程环境。RustPython根据具体的使用场景选择合适的引用计数类型,以确保内存管理的效率和正确性。

3.6 自定义对象

除了内置对象,RustPython也支持用户自定义对象,这通过类(class)来实现。当用户定义一个类时,RustPython会创建一个对应的类型对象,该类型对象包含了类的属性和方法。当创建类的实例时,实例对象会包含该类的属性,并可以调用类中定义的方法。

自定义对象的实现基于RustPython的对象系统框架,遵循与内置对象相同的接口和行为规范。这使得自定义对象能够与内置对象无缝交互,共同构成Python丰富的数据类型系统。

例如,用户定义一个Person类:

class Person:
    def __init__(self, name):
        self.name = name
    
    def greet(self):
        return f"Hello, my name is {self.name}"

在RustPython中,这个类会被表示为一个类型对象,Person类的实例则是该类型对象的实例。__init__方法用于初始化实例的属性,greet方法则是实例的一个方法,可以被调用以返回问候语。

虚拟机会为Person类创建相应的类型信息,包含__init__greet等方法。当创建Person实例时,会分配相应的内存,并调用__init__方法进行初始化。当调用greet方法时,虚拟机会按照方法调用机制查找并执行该方法。

综上所述,RustPython的对象系统是其核心组成部分,它实现了Python中“一切皆对象”的理念,为各种数据类型提供了统一的管理和操作方式。从内置对象到自定义对象,从方法调用到内存管理,对象系统的设计和实现确保了RustPython能够正确、高效地执行Python代码,为用户提供与标准Python相似的编程体验。同时,借助Rust的特性,RustPython的对象系统在安全性和性能方面也具有一定的优势。

4. 标准库:兼容与扩展的平衡之道

Python的强大之处不仅在于其简洁的语法,更在于其丰富的标准库。RustPython作为一个兼容Python 3的解释器,在标准库的实现上采取了"拿来主义"与"原生实现"相结合的策略,既保证了兼容性,又利用Rust的特性提升了性能和安全性。

4.1 标准库的双重结构

RustPython的标准库由两个主要部分组成:从CPython移植的纯Python代码和用Rust原生实现的核心模块。这种混合架构既最大化了兼容性,又充分发挥了Rust的优势。

4.1.1 从CPython移植的纯Python代码(Lib目录)

Lib目录是RustPython兼容性的关键,它直接复制了CPython标准库中的纯Python代码。这一做法的优势显而易见:无需重新实现大量成熟的代码,就能快速获得完整的标准库支持。

# 来自 .github/copilot-instructions.md
- `Lib/` - CPython's standard library in Python (copied from CPython). 
  **IMPORTANT**: Do not edit this directory directly; The only allowed operation is copying files from CPython.

这一目录的代码完全来自CPython,并且有严格的规定:不允许直接修改,只能从CPython复制。这确保了代码的兼容性和稳定性。例如,Lib/test目录包含了大量的Python测试用例,这些测试用例可以直接用于验证RustPython的兼容性。

但这种"拿来主义"也带来了挑战。由于RustPython的实现细节与CPython存在差异,部分从CPython移植的代码可能无法直接运行。为此,RustPython团队采取了一些临时措施:

# 来自 .github/copilot-instructions.md
- Tests in `Lib/test` often use one of the following markers:
  - Add a `# TODO: RUSTPYTHON` ***ment when modifications are made
  - `unittest.skip("TODO: RustPython <reason>")`
  - `unittest.expectedFailure` with `# TODO: RUSTPYTHON <reason>` ***ment

这些标记帮助开发者跟踪哪些测试用例需要适配RustPython,随着项目的发展,这些标记会逐渐被移除,以实现更完整的兼容性。

4.1.2 Rust原生实现的标准库模块

除了移植的纯Python代码,RustPython还有一部分标准库是用Rust原生实现的。这些模块主要分为两类:核心必需模块和扩展模块。

  1. 核心必需模块(vm/src/stdlib)

这些模块是Python运行所必需的,使用Rust实现可以提高性能和可靠性。

# 来自 DEVELOPMENT.md
The `vm/src` directory also contains the implementation of the
Python Standard Library modules in Rust (`vm/src/stdlib`).

例如,vm/src/stdlib目录下实现了sysos等核心模块。这些模块与Python的运行时紧密相关,用Rust实现可以更好地与虚拟机集成,提升性能。

  1. 非核心扩展模块(stdlib目录)

这些模块是对标准库的补充,虽然不是运行Python所必需的,但能提供丰富的功能。

# 来自 .github/copilot-instructions.md
- `stdlib/` - Non-essential Python standard library modules implemented in Rust 
  (useful but not required for core functionality)

这些模块包括一些工具类库和扩展功能,用Rust实现可以利用Rust的内存安全和并发特性,提供更可靠的功能。

4.2 标准库的实现细节

RustPython的标准库实现不仅仅是简单的功能移植,还需要考虑与Rust生态的集成和性能优化。

4.2.1 与Rust类型系统的桥接

Python的动态类型系统与Rust的静态类型系统存在显著差异,标准库的实现需要处理这种差异。RustPython通过vm/src/convert.rs中的类型转换机制,实现了Python对象和Rust类型之间的无缝转换。

// 类型转换相关的核心trait(简化版)
pub trait TryFromObject: Sized {
    fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self>;
}

pub trait TryFromBorrowedObject: Sized {
    fn try_from_borrowed_object(vm: &VirtualMachine, obj: &PyObjectRef) -> PyResult<Self>;
}

这些trait允许Rust类型定义如何从Python对象转换而来,使得标准库函数可以方便地接收Python传递的参数,并返回Python可以理解的结果。

4.2.2 模块注册机制

Rust实现的标准库模块需要注册到虚拟机中,才能被Python代码导入和使用。RustPython提供了一套宏系统来简化这一过程。

// 来自 vm/src/lib.rs
pub use rustpython_derive::*;

rustpython_derive crate提供了#[pymodule]等宏,允许开发者用声明式的方式定义Python模块。例如:

#[pymodule]
mod sys {
    use rustpython_vm::prelude::*;

    #[pyfunction]
    fn getsizeof(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<usize> {
        Ok(vm.size_of(&obj))
    }

    // 其他函数和变量...
}

通过这种方式,Rust函数可以被注册为Python模块的函数,实现了Rust代码向Python的暴露。

4.2.3 标准库的初始化

在虚拟机启动时,需要初始化标准库。这一过程在InterpreterConfig中完成:

// 来自 src/lib.rs
let mut config = InterpreterConfig::new().settings(settings);
#[cfg(feature = "stdlib")]
{
    config = config.init_stdlib();
}

init_stdlib方法会注册所有Rust实现的标准库模块,并确保Lib目录下的纯Python模块可以被正确导入。

4.2.4 标准库的测试

为了确保标准库的正确性,RustPython采用了多层次的测试策略:

  1. Rust单元测试:测试Rust实现的内部逻辑。
  2. Python测试用例:运行Lib/test目录下的测试用例,验证兼容性。
  3. 集成测试:在extra_tests目录下提供额外的集成测试。
# 来自 .github/copilot-instructions.md
# Run Python snippets tests
cd extra_tests
pytest -v

# Run the Python test module
cargo run --release -- -m test ${TEST_MODULE}
cargo run --release -- -m test test_unicode # to test test_unicode.py

这些测试确保了标准库的实现既符合Python的规范,又能在RustPython环境下正确运行。

4.3 标准库的挑战与解决方案

在实现标准库的过程中,RustPython团队面临着诸多挑战,其中最主要的是兼容性和性能之间的平衡。

4.3.1 兼容性挑战

Python标准库的某些部分依赖于CPython的内部实现细节,这给RustPython的兼容带来了困难。例如,某些模块可能使用了CPython的C API,而这些API在RustPython中并不存在。

为了解决这一问题,RustPython采取了以下策略:

  1. 模拟CPython的行为:对于依赖CPython内部实现的功能,RustPython会尽可能模拟其行为,即使内部实现方式不同。
  2. 提供替代实现:对于无法直接模拟的功能,RustPython会提供功能相当的替代实现。
  3. 逐步适配:随着项目的发展,逐步提高兼容性,移除临时的适配代码。
4.3.2 性能优化

虽然Rust本身比C有更好的内存安全特性,但解释器的性能仍然是一个挑战。RustPython在标准库中采用了多种优化策略:

  1. 利用Rust的静态分发:对于热点函数,使用Rust的静态分发特性,减少动态调度的开销。
  2. 内存池:对于频繁创建和销毁的对象(如小整数、短字符串),使用内存池减少内存分配的开销。
  3. 惰性初始化:某些资源密集型的功能采用惰性初始化,只有在首次使用时才分配资源。
4.3.3 扩展与定制

RustPython的标准库设计允许用户方便地扩展和定制。通过add_native_module方法,用户可以向虚拟机添加自己的Rust实现的模块:

// 来自 src/lib.rs 的示例
vm.add_native_module("my_mod".to_owned(), Box::new(my_mod::make_module));

这种灵活性使得RustPython可以作为一个嵌入式解释器,为Rust应用程序提供Python脚本支持,同时通过自定义模块实现Rust和Python的无缝交互。

5. 编译管道:从源代码到执行的旅程

编译管道是解释器的核心部分,它负责将Python源代码转换为可执行的字节码,并最终在虚拟机中执行。RustPython的编译管道借鉴了CPython的设计,但采用Rust重新实现,带来了更好的安全性和可维护性。

5.1 编译管道的整体架构

RustPython的编译管道可以分为四个主要阶段:词法分析(Lexing)、语法分析(Parsing)、语义分析与字节码生成(Code Generation),以及字节码执行(Execution)。

# 来自 DEVELOPMENT.md
Together, these crates provide the functions of a programming language and
enable a line of code to go through a series of steps:
- parse the line of source code into tokens
- determine if the tokens are valid syntax
- create an Abstract Syntax Tree (AST)
- ***pile the AST into bytecode
- execute the bytecode in the virtual machine (VM).

这一流程与大多数编译器的工作流程相似,但针对Python的动态特性做了特殊处理。

5.2 词法分析:将源代码转换为令牌

词法分析是编译过程的第一步,它将原始的源代码字符串转换为有意义的令牌(tokens)。

5.2.1 词法分析器的实现

RustPython的词法分析器由rustpython-parser crate实现,位于***piler/parser/src目录下。

# 来自 DEVELOPMENT.md
- `rustpython-parser` (implementation in `***piler/parser/src`)

词法分析器的核心任务是识别Python的关键字、标识符、字面量、运算符等。例如,对于代码x = 42 + y,词法分析器会生成以下令牌序列:Identifier(x), Assign, Integer(42), Plus, Identifier(y)

5.2.2 处理Python的特殊语法

Python的语法有一些特殊之处,如缩进敏感、多行语句等,这给词法分析带来了挑战。

RustPython的词法分析器通过跟踪缩进级别来处理缩进敏感的语法。它会将缩进的变化转换为IndentDedent令牌,使得后续的语法分析可以正确理解代码块的结构。

对于多行语句,词法分析器会处理反斜杠\和括号来确定语句的结束位置,确保多行语句被正确解析为一个整体。

5.3 语法分析:构建抽象语法树(AST)

语法分析器(Parser)接收词法分析器生成的令牌序列,根据Python的语法规则构建抽象语法树(AST)。AST是源代码的结构化表示,它捕获了代码的语法结构和语义信息。

5.3.1 AST的结构

RustPython的AST定义在***piler/ast/src目录下,使用Rust的结构体和枚举来表示不同的Python语法结构。

# 来自 DEVELOPMENT.md
- `***piler/src`: python ***pilation to bytecode
  - `core/src`: python bytecode representation in rust structures
  - `parser/src`: python lexing, parsing and ast

例如,表达式、语句、函数定义等都有对应的AST节点类型:

// 简化的AST节点示例
enum Expr {
    Constant(Constant),
    Name(Name),
    BinOp(BinOp),
    // 其他表达式类型...
}

enum Stmt {
    Assign(Assign),
    FunctionDef(FunctionDef),
    If(If),
    // 其他语句类型...
}

这些AST节点不仅包含了语法信息,还包含了位置信息(行号、列号),这对于错误提示和调试非常有用。

5.3.2 语法错误处理

语法分析器在遇到不符合Python语法规则的令牌序列时,会生成语法错误。RustPython的语法分析器会尽可能提供有用的错误信息,包括错误位置和可能的修复建议。

例如,对于代码if x = 5:,语法分析器会检测到条件表达式中使用了赋值运算符=而不是比较运算符==,并生成相应的错误提示。

5.3.3 代码生成辅助

除了构建AST,语法分析器还会进行一些初步的语义分析,为后续的字节码生成做准备。例如,它会检查变量的使用是否符合Python的作用域规则,函数调用的参数数量是否正确等。

5.4 语义分析与字节码生成

语义分析与字节码生成是编译管道的核心阶段,它将AST转换为虚拟机可以执行的字节码。这一阶段由rustpython-***piler crate实现。

# 来自 DEVELOPMENT.md
The `rustpython-***piler` crate's purpose is to transform the AST (Abstract Syntax
Tree) to bytecode. The implementation of the ***piler is found in the
`***piler/src` directory.
5.4.1 符号表与作用域管理

在生成字节码之前,编译器需要构建符号表(Symbol Table)来跟踪变量的作用域和绑定信息。这对于正确生成访问变量的字节码至关重要。

Python的作用域规则相对复杂,包括全局作用域、局部作用域、嵌套作用域等。RustPython的编译器会为每个函数和类创建一个符号表,记录变量的声明和使用情况。

例如,对于嵌套函数:

def outer():
    x = 10
    def inner():
        print(x)
    inner()

编译器会识别出inner函数中的x是来自outer函数的非局部变量,并生成相应的字节码来正确访问这个变量。

5.4.2 字节码的表示

Python的字节码是一种抽象的机器指令,它由操作码(opcode)和操作数(operand)组成。RustPython在***piler/core/src/bytecode.rs中定义了字节码的表示。

# 来自 DEVELOPMENT.md
Implementation of bytecode structure in Rust is found in the `***piler/core/src`
directory. `***piler/core/src/bytecode.rs` contains the representation of
instructions and operations in Rust.

字节码指令的定义类似于:

// 简化的字节码指令示例
#[derive(Debug, Clone, PartialEq)]
pub enum OpCode {
    LOAD_CONST(usize), // 加载常量,操作数是常量池索引
    LOAD_FAST(usize),  // 加载局部变量,操作数是局部变量索引
    STORE_FAST(usize), // 存储局部变量,操作数是局部变量索引
    BINARY_ADD,        // 加法运算
    // 其他指令...
}

每个指令都有特定的语义,虚拟机通过执行这些指令来完成相应的操作。

5.4.3 从AST到字节码的转换

编译器遍历AST,为每个节点生成相应的字节码序列。这一过程需要考虑Python的各种语言特性,如动态类型、异常处理、生成器等。

以简单的加法表达式a + b为例,编译器会生成以下字节码:

LOAD_FAST a    # 加载变量a到栈顶
LOAD_FAST b    # 加载变量b到栈顶
BINARY_ADD     # 弹出栈顶的两个值,相加后将结果压入栈顶

对于更复杂的结构,如条件语句:

if x > 5:
    print("x is large")
else:
    print("x is small")

编译器会生成包含条件跳转的字节码:

LOAD_FAST x       # 加载x
LOAD_CONST 5      # 加载常量5
***PARE_OP GT     # 比较x > 5,结果压栈
POP_JUMP_IF_FALSE else_label  # 如果结果为假,跳转到else分支
LOAD_GLOBAL print # 加载print函数
LOAD_CONST "x is large"
CALL_FUNCTION 1   # 调用print函数
JUMP end_label    # 跳转到结束
else_label:
LOAD_GLOBAL print
LOAD_CONST "x is small"
CALL_FUNCTION 1
end_label:
LOAD_CONST None
RETURN_VALUE

这种字节码生成过程需要深入理解Python的语义,确保生成的字节码能够正确实现源代码的逻辑。

5.4.4 字节码优化

为了提高执行效率,RustPython的编译器会对生成的字节码进行一些优化。虽然目前的优化相对简单,但为未来更复杂的优化奠定了基础。

常见的优化包括:

  1. 常量折叠:在编译时计算常量表达式的值,如2 + 3会被直接替换为5
  2. 空操作消除:移除无效的空操作,如连续的NOP指令。
  3. 局部变量优化:优化局部变量的访问,减少栈操作。

这些优化虽然简单,但能显著提高某些代码的执行效率。

5.5 字节码执行

字节码的执行是编译管道的最后阶段,由RustPython的虚拟机负责。虚拟机是一个栈式机器,它通过操作栈来执行字节码指令。

5.5.1 虚拟机的执行循环

虚拟机的核心是一个执行循环,它不断地从字节码中取出指令并执行:

// 简化的虚拟机执行循环
fn run_code(&self, code: &CodeObject, frame: &mut Frame) -> PyResult {
    let mut pc = 0; // 程序计数器,指向当前要执行的指令
    while pc < code.bytecode.len() {
        let opcode = &code.bytecode[pc];
        pc += 1; // 前进到下一条指令
        self.execute_opcode(opcode, frame, &mut pc)?;
    }
    Ok(())
}

execute_opcode方法根据指令类型执行相应的操作,如加载变量、执行运算、调用函数等。

5.5.2 栈操作

栈式虚拟机的大部分操作都围绕着操作栈进行。例如,LOAD_FAST指令将变量的值压入栈顶,BINARY_ADD指令弹出栈顶的两个值,执行加法后将结果压回栈顶。

操作栈的实现通常使用一个动态数组,在RustPython中,栈的定义类似于:

// 简化的栈定义
pub struct Frame {
    stack: Vec<PyObjectRef>,
    // 其他帧信息...
}

impl Frame {
    pub fn push(&mut self, value: PyObjectRef) {
        self.stack.push(value);
    }

    pub fn pop(&mut self) -> PyObjectRef {
        self.stack.pop().expect("Stack underflow")
    }

    // 其他栈操作方法...
}

栈操作的效率直接影响虚拟机的性能,Rust的Vec类型提供了高效的栈操作支持。

5.5.3 异常处理

Python的异常处理机制在字节码层面通过SETUP_EXCEPTRAISE_VARARGS等指令实现。当发生异常时,虚拟机会查找当前调用栈中最近的异常处理块,并跳转到相应的代码执行。

RustPython的虚拟机在执行过程中会维护一个异常状态,当执行RAISE_VARARGS指令时,会设置这个状态,并开始查找异常处理块。

// 简化的异常处理示例
fn execute_raise(&mut self, frame: &mut Frame) -> PyResult {
    let exc = frame.pop();
    // 设置当前异常
    self.set_exception(exc);
    // 查找异常处理块
    self.find_exception_handler(frame)
}

这种异常处理机制确保了Python的try...except...finally语句能够正确工作。

5.5.4 函数调用与栈帧

函数调用是通过CALL_FUNCTION指令实现的。当执行函数调用时,虚拟机会创建一个新的栈帧(Frame),用于存储函数的局部变量、操作栈等信息。

// 简化的函数调用示例
fn execute_call_function(&mut self, frame: &mut Frame, arg_count: usize) -> PyResult {
    // 从栈中弹出参数和函数对象
    let args = (0..arg_count).map(|_| frame.pop()).rev().collect();
    let func = frame.pop();
    // 创建新的栈帧
    let new_frame = self.create_frame(&func, args);
    // 执行函数
    let result = self.run_frame(new_frame);
    // 将结果压入调用者的栈
    frame.push(result?);
    Ok(())
}

栈帧的管理是虚拟机的核心功能之一,它确保了函数调用的正确嵌套和变量作用域的隔离。

5.6 即时编译(JIT)的实验性支持

为了进一步提高性能,RustPython正在实验性地支持即时编译(JIT)技术。JIT编译器可以将频繁执行的字节码转换为机器码,从而显著提高执行速度。

# 来自 .github/copilot-instructions.md
- `jit/` - Experimental JIT ***piler implementation

RustPython的JIT实现基于Cranelift代码生成器,这是一个由Mozilla开发的轻量级代码生成库。JIT模块的Cargo.toml文件显示了相关依赖:

# 来自 jit/Cargo.toml
cranelift = "0.119"
cranelift-jit = "0.119"
cranelift-module = "0.119"

目前,JIT支持还处于实验阶段,只实现了部分功能。但这一方向为RustPython未来的性能提升提供了可能。

6. 项目组织:模块化与可扩展性的工程实践

一个大型开源项目的组织架构直接影响其可维护性和可扩展性。RustPython采用了模块化的设计理念,将整个项目分为多个功能明确的子 crate,每个 crate 负责特定的功能。这种组织结构使得代码更易于理解、测试和扩展。

6.1 项目整体结构

RustPython的代码库包含多个子目录,每个目录对应一个或多个 crate,负责解释器的不同部分。

# 来自 .github/copilot-instructions.md
## Repository Structure

- `src/` - Top-level code for the RustPython binary
- `vm/` - The Python virtual machine implementation
  - `builtins/` - Python built-in types and functions
  - `stdlib/` - Essential standard library modules implemented in Rust, required to run the Python core
- `***piler/` - Python ***piler ***ponents
  - `parser/` - Parser for converting Python source to AST
  - `core/` - Bytecode representation in Rust structures
  - `codegen/` - AST to bytecode ***piler
- `Lib/` - CPython's standard library in Python (copied from CPython)
- `derive/` - Rust macros for RustPython
- `***mon/` - ***mon utilities
- `extra_tests/` - Integration tests

这一结构清晰地划分了项目的各个组成部分,使得开发者可以快速定位到自己感兴趣的模块。

6.2 核心 crate 解析

6.2.1 rustpython-vm:虚拟机核心

vm/目录包含了RustPython的虚拟机实现,是整个项目的核心。

# 来自 DEVELOPMENT.md
The `rustpython-vm` crate has the important job of running the virtual machine that
executes Python's instructions. The `vm/src` directory contains code to
implement the read and evaluation loop that fetches and dispatches
instructions.

vm/src/lib.rs是虚拟机的入口点,定义了VirtualMachineInterpreter等核心结构:

// 来自 vm/src/lib.rs
pub use self::vm::{Context, Interpreter, Settings, VirtualMachine};

虚拟机的主要功能包括:

  1. 执行字节码指令
  2. 管理对象和内存
  3. 处理异常
  4. 实现Python的内置类型和函数(vm/src/builtins
  5. 提供核心标准库模块(vm/src/stdlib
6.2.2 编译器相关 crate

编译器相关的功能被分为多个 crate,协同工作完成从源代码到字节码的转换。

  1. rustpython-parser:负责词法分析和语法分析,生成AST。
# 来自 DEVELOPMENT.md
- `rustpython-parser` (implementation in `***piler/parser/src`)
  1. rustpython-***piler-core:定义字节码的表示形式。
# 来自 ***piler/codegen/Cargo.toml
rustpython-***piler-core = { workspace = true }
  1. rustpython-codegen:将AST转换为字节码。
# 来自 .github/copilot-instructions.md
- `***piler/` - Python ***piler ***ponents
  - `codegen/` - AST to bytecode ***piler
  1. rustpython-***piler:整合上述组件,提供完整的编译功能。
# 来自 DEVELOPMENT.md
- `rustpython-***piler` (implementation in `***piler/src`)

这些 crate 之间通过清晰的接口交互,例如rustpython-codegen依赖rustpython-***piler-core来使用字节码的定义,rustpython-***piler则依赖rustpython-parserrustpython-codegen来完成整个编译过程。

6.2.3 辅助 crate

除了核心的虚拟机和编译器,RustPython还有多个辅助 crate,提供各种支持功能。

  1. rustpython-derive:提供宏定义,简化Python模块和函数的注册。
# 来自 vm/src/lib.rs
#[macro_use]
extern crate rustpython_derive;
  1. rustpython-***mon:提供通用工具函数和类型。
# 来自 vm/src/lib.rs
pub use rustpython_***mon as ***mon;
  1. rustpython-literal:处理Python字面量的解析和转换。
# 来自 ***piler/codegen/Cargo.toml
rustpython-literal = {workspace = true }

这些辅助 crate 提取了通用功能,避免了代码重复,提高了代码的可维护性。

6.2.4 应用与测试相关目录
  1. src/:包含RustPython的命令行入口点,负责解析命令行参数、设置环境等。
// 来自 src/lib.rs
/// The main cli of the `rustpython` interpreter.
pub fn run(init: impl FnOnce(&mut VirtualMachine) + 'static) -> ExitCode {
    // 解析命令行参数、初始化虚拟机、执行相应操作
}
  1. Lib/:包含从CPython移植的纯Python标准库。

  2. extra_tests/:包含额外的集成测试和代码片段。

# 来自 architecture/architecture.md
## Additional packages
### extra_tests

Integration and snippets that test for additional edge-cases, implementation specific functionality and bug report snippets.
  1. wasm/:提供WebAssembly支持,使得RustPython可以在浏览器中运行。
# 来自 README.md
## ***piling to WebAssembly

[See this doc](wasm/README.md)
  1. example_projects/:提供示例项目,展示如何在Rust应用中嵌入RustPython。
# 来自 example_projects/aheui-rust.md
This crate shows you how to embed an entire Python project into bytecode and ship it.

6.3 跨平台支持

RustPython的项目组织也考虑了跨平台支持,特别是WebAssembly平台。

6.3.1 WebAssembly支持

wasm/目录包含了WebAssembly相关的代码和配置,使得RustPython可以被编译为WebAssembly,在浏览器中运行。

# 来自 wasm/notebook/README.md
The Notebook loads python in your browser (so you don't have to install it) then let yous play with those languages.

RustPython提供了两个WebAssembly演示:

  1. 在线演示页面
  2. RustPython Notebook

这些演示展示了RustPython在Web环境中的应用潜力。

6.3.2 条件编译

Rust的条件编译特性被广泛用于处理不同平台之间的差异。例如,在vm/src/lib.rs中:

#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
pub mod ospath;

#[cfg(windows)]
pub mod windows;

这种条件编译确保了特定平台的代码只会在相应的平台上被编译,提高了代码的可移植性。

6.4 构建系统与依赖管理

RustPython使用Cargo作为构建系统和依赖管理器,通过Cargo.tomlCargo.lock文件管理项目的依赖和构建配置。

6.4.1 工作区配置

RustPython采用Cargo工作区(workspace)来管理多个相关的crate。根目录下的Cargo.toml定义了工作区的成员:

# 根目录 Cargo.toml(简化)
[workspace]
members = [
    "vm",
    "***piler",
    "***piler/parser",
    "***piler/codegen",
    "derive",
    "***mon",
    "stdlib",
    "jit",
    # 其他成员...
]

这种配置使得可以一次性构建所有相关的crate,简化了开发流程。

6.4.2 特性标志(Feature Flags)

RustPython使用Cargo的特性标志来控制条件编译,允许用户根据需求选择不同的功能组合。

例如,vm/Cargo.toml中定义了多个特性:

# 来自 vm/Cargo.toml
[features]
default = ["***piler", "wasmbind", "stdio"]
stdio = []
importlib = []
encodings = ["importlib"]
vm-tracing-logging = []
flame-it = ["flame", "flamer"]
freeze-stdlib = ["encodings"]
jit = ["rustpython-jit"]
threading = ["rustpython-***mon/threading"]
***piler = ["parser", "codegen", "rustpython-***piler"]
# 其他特性...

这些特性允许用户启用或禁用某些功能,如JIT支持、线程支持、标准库冻结等,使得RustPython可以根据不同的应用场景进行定制。

6.4.3 依赖管理

RustPython严格管理依赖项,确保项目的稳定性和安全性。所有依赖项的版本都在Cargo.toml中明确指定,并且通过Cargo.lock文件锁定具体的版本,避免因依赖项更新而导致的兼容性问题。

同时,RustPython优先使用稳定的依赖项版本,并定期更新依赖项以获取安全补丁和性能改进。

6.5 开发与贡献支持

RustPython的项目组织也考虑了开发者的体验,提供了丰富的文档和工具支持,方便开发者贡献代码。

6.5.1 文档

项目提供了详细的文档,帮助开发者了解项目的架构和开发流程:

# 来自 .github/copilot-instructions.md
## Documentation

- Check the [architecture document](/architecture/architecture.md) for a high-level overview
- Read the [development guide](/DEVELOPMENT.md) for detailed setup instructions
- Generate documentation with `cargo doc --no-deps --all`
- Online documentation is available at [docs.rs/rustpython](https://docs.rs/rustpython/)

这些文档涵盖了从项目概述到详细开发指南的各个方面,为新开发者提供了入门帮助。

6.5.2 测试基础设施

RustPython拥有完善的测试基础设施,包括单元测试、集成测试和Python兼容性测试:

# 来自 .github/copilot-instructions.md
# Run Rust unit tests
cargo test --workspace --exclude rustpython_wasm

# Run Python snippets tests
cd extra_tests
pytest -v

# Run the Python test module
cargo run --release -- -m test ${TEST_MODULE}

这些测试确保了代码的质量和兼容性,也为开发者提供了验证自己修改的方法。

6.5.3 代码风格与规范

为了保持代码的一致性,RustPython采用了Rust社区的代码风格,并使用rustfmtclippy等工具进行代码格式化和 lint 检查。

项目的DEVELOPMENT.md文档中也包含了代码风格的指导,帮助开发者写出符合项目规范的代码。

6.6 项目的可扩展性

RustPython的模块化设计使其具有良好的可扩展性。开发者可以通过以下方式扩展RustPython的功能:

  1. 添加新的标准库模块:可以在stdlib/目录下实现新的标准库模块,或在vm/src/stdlib中添加核心模块。
  2. 扩展编译器:可以修改***piler/目录下的代码,添加新的优化或支持新的语法特性。
  3. 增强虚拟机:可以在vm/目录下添加新的内置类型或函数,或优化虚拟机的执行效率。
  4. 集成新的功能:如JIT编译器的进一步开发,或添加新的平台支持。

这种可扩展性使得RustPython能够不断发展,逐步实现对Python的完全兼容,并在性能和功能上不断提升。

总结

RustPython作为一个用Rust编写的Python解释器,在标准库实现、编译管道设计和项目组织方面都展现了出色的工程实践。通过混合使用从CPython移植的纯Python代码和Rust原生实现,RustPython在保证兼容性的同时,充分发挥了Rust的性能和安全优势。其编译管道借鉴了传统编译器的设计,将源代码逐步转换为可执行的字节码,并通过实验性的JIT支持探索进一步的性能提升。项目的模块化组织结构则确保了代码的可维护性和可扩展性,为未来的发展奠定了坚实的基础。随着RustPython的不断发展,我们有理由相信它将成为Python生态系统中一个重要的补充,为Python带来更好的性能、安全性和跨平台支持,同时也为Rust的应用开辟新的领域。如果你看到这里忍不住想要体验一下这款全新的python解释器,那你可以点击文章开头的链接,进入仓库的主页,拉去源码进行试验,也祝你有一个全新的体验,同时如果你感觉本篇文章不错的话别忘了一键三连哦。


想了解更多关于Rust语言的知识及应用,可前往华为开放原子旋武开源社区(https://xuanwu.openatom.***/),了解更多资讯~

转载请说明出处内容投诉
CSS教程网 » 深入探索RustPython:用Rust构建的Python解释器

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买