4个版本
0.4.0 | 2021年7月29日 |
---|---|
0.1.1-releaseAttempt5 | 2021年7月26日 |
0.1.1-releaseAttempt4 | 2021年7月24日 |
0.1.1-releaseAttempt3 |
|
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
基本概念
虚拟机读取由多个独立部分组成的代码,这些部分称为块,包含可执行代码和常量(例如1
、2
或""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
可以被理解为接受虚拟机状态和指令接收的参数列表的简单函数,并修改虚拟机状态。但它还具有简化定义新指令的几个特性。 Const
、UnaryOp
和 BinaryOp
分别简化了零元、一元和二元运算符指令的创建。
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
,其中 a1
和 a2
对应于 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
表示,而 chunks2
由 02 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]
}
解析代码
CodeParser
和 ConstantParser
是简化字节码解析的有用抽象。然而,使用它们并非必需,你可以按任何你想要的方式创建一个 Code
结构体。
CodeParser
假设所有块常量都通过一个唯一的 ID 和一个字节数组在二进制文件中表示。每种常量都应该由一个单独的 ConstantParser
解析。
例如,如果我们有一个持有 i32
的 IntConstant
,我们可以定义一个解析器
// 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