4个版本

0.4.0 2021年7月29日
0.1.1-releaseAttempt52021年7月26日
0.1.1-releaseAttempt42021年7月24日
0.1.1-releaseAttempt3 2021年7月18日
0.1.0 2021年7月18日

#292 in 模拟器

每月 29次下载

自定义许可证

44KB
981

可扩展虚拟机

简化在Rust中编写栈虚拟机

只需定义你的

  • 字节码格式
  • 指令

然后运行虚拟机!

这最初是jex_vm的一部分,为我简单的编程语言Jex编写的栈虚拟机。

入门指南

安装

只需将extendable_vm添加到Cargo.toml

[dependencies]
extendable_vm = "<latest version>"

您可以从版本页面获取最新版本。

带日志运行

如果您在二进制可执行文件中使用extendable_vm并希望查看所有虚拟机日志,则将extendable_vm添加到RUST_LOG环境变量中:RUST_LOG=extendable_vm。如果您的环境变量已经定义了一个选项列表(RUST_LOG=a,b,c),则只需追加extendable_vm:RUST_LOG=a,b,c,extendable_vm

例如,

RUST_LOG=extendable_vm ./your_binary_exec path/to/bytecode

基本概念

虚拟机读取由多个独立部分组成的代码,这些部分称为,包含可执行代码和常量(例如12""Hello World"")。虚拟机有一个操作数栈、一个调用栈,可以在一个块内部或块之间跳转。

可执行代码只是一个字节数组,它编码了一组要执行的指令。每个指令都有其唯一的标识符 —— 操作码 和它所接受的参数数量。

例如,如果指令 A操作码 = 7 接受 2 个参数,那么我们可以运行 7 1 2 7 3 4,这意味着 run A(1, 2); run A(3, 4)

要构建自己的虚拟机,你必须定义

  • 存储在字节码中的常量类型及其解析方式。有关字节码格式的更多信息,请参阅字节码格式
  • 虚拟机可以执行的指令集及其实现。有关指令的更多信息,请参阅定义指令

虚拟机状态

虚拟机状态由一个 Machine<Constant, Value> 结构表示。它存储

  • 虚拟机正在执行的代码
  • 操作数栈
  • 调用栈
  • 全局值

Constant 是字节码中常量值的类型。

Value 是虚拟机操作的运算数。

定义指令

每个指令都有其唯一的标识符 —— op_code 和用于调试的 name。还有一个 instruction_fn 函数,用于实现指令的逻辑。

pub struct Instruction<Constant, Value> {
    pub op_code: u8,
    pub name: &'static str,
    pub instruction_fn: InstructionFn<Constant, Value>,
}

InstructionFn 可以被理解为接受虚拟机状态和指令接收的参数列表的简单函数,并修改虚拟机状态。但它还具有简化定义新指令的几个特性。 ConstUnaryOpBinaryOp 分别简化了零元、一元和二元运算符指令的创建。

pub enum InstructionFn<Constant, Value> {
    // Simple function that I described above
    Raw {
        byte_arity: usize,
        instruction_fn: RawInstructionFn<Constant, Value>,
    },
    // Instruction that generates a value and pushes it onto the stack
    Const(fn() -> Value),
    // Unary operator instruction that pops the value from stack,
    // produces new value and pushes it onto the stack
    UnaryOp(fn(value: Value) -> Result<Value, Exception>),
    // The same as unary operator but pops 2 values
    BinaryOp(fn(left: Value, right: Value) -> Result<Value, Exception>),
}

// Simple function that I described above
// (mut VM State, instruction arguments) -> may return Exception
pub type RawInstructionFn<Constant, Value> = fn(
    machine: &mut Machine<Constant, Value>,
    args_ip: InstructionPointer,
) -> Result<(), Exception>;

字节码

本节描述了如何在 API 中访问字节码以及如何在二进制文件中表示字节码。

二进制文件的表示法

在二进制数据的上下文中,struct 被用作展示每个字节含义的一种方式。这里的每个结构都应被视为一个字节数组,其中每个值直接跟随前一个值(没有填充和打包)。

例如,结构 A 表示字节 a1 a2 b,其中 a1a2 对应于 a: u16,而 b 对应于 b: u8

struct A {
    a: u16,
    b: u8
}

代码

虚拟机读取 Code(字节码)并执行它。Code 由几个独立的可执行部分组成 —— Chunk。例如,每个函数应定义为一个单独的 Chunk

// API
pub struct Code<Constant> {
    pub chunks: Vec<Chunk<Constant>>,
}

// in binary file
struct _Code<Constant> {
    chunks: [_Chunk<Constant>]
}

在二进制文件中,Code 被表示为一个字节数组,其中所有块都是连接在一起的。例如,如果 chunk1 由字节 00 01 表示,而 chunks202 03 表示。那么代码 [chunk1, chunk2]00 01 02 03

每个 Chunk 都包含几个 constants 和可执行的 code,它只是一个字节数组。

// API
pub struct Chunk<Constant> {
    pub constants: Vec<Constant>,
    pub code: Vec<u8>,
}

// in binary file
struct _Chunk<Constant> {
    // number of constants
    n_constants: u8,
    // array of constants of size `n_constants`
    // each constant is encoded as an array of bytes and is parsed by a constant parser
    constants: [Constant],
    // number of bytes in `code`
    n_code_bytes: u16,
    // executable code
    code: [u8]
}

解析代码

CodeParserConstantParser 是简化字节码解析的有用抽象。然而,使用它们并非必需,你可以按任何你想要的方式创建一个 Code 结构体。

CodeParser 假设所有块常量都通过一个唯一的 ID 和一个字节数组在二进制文件中表示。每种常量都应该由一个单独的 ConstantParser 解析。

例如,如果我们有一个持有 i32IntConstant,我们可以定义一个解析器

// in binary file
struct _IntConstant {
    // unique ID = 0
    constant_type: 0 as u8, // used only to demonstrate binary data
    // 4 bytes that represent i32
    data: [u8]
}

const INT_CONSTANT_PARSER: ConstantParser<i32> = ConstantParser {
    constant_type: 0 as u8,
    parser_fn: parse_int_constant,
};

// parses `data` and returns i32 or on exception
fn parse_int_constant(
    // the entire code
    bytes: &RawBytes,
    // points to the current reading position in `bytes`
    // initially points to the start of `data`
    pointer: &mut RawBytesPointer, 
) -> Result<i32, Exception> {
    // all read operations advance the `pointer`
    Ok(bytes.read_i32(pointer).unwrap())
}

从源代码构建

构建开发版本

cargo build

构建发布版本

cargo build --release

运行测试

cargo test

历史

我想学习编译器和编程语言,结果读了一本非常好的书 Crafting Interpreters 并制作了我的编程语言 Jex

这最初是我编程语言 jex_vm 的一个简单 VM 的一部分,我的第一个 Rust 项目。

这个库的设计灵感来源于 stack_vm,这在我不了解 Rust 的情况下进行此项目时帮助很大。

依赖

~2–11MB
~103K SLoC