尝试写一个jvm吧(二)
今天清明节,刚好放假更新第二章,就是读取字节码文件。 一个更舒适的UI:https://www.mdnice.com/writing/d1c0dc8c7bbc4ef3bf5f5a2ae3e10f97 前面两节都比较简单,分别是命令行操作和读取class文件,从第三节开始估计进度会慢一些和复杂一点点,要开始写运行时和解析class文件了~
首先,我们还是通过命令行来进行操作,所以首先在command.rs
文件中加入以下指令
command
结构体
#[derive(Debug)]
pub struct Command {
pub help_flag: bool,
pub version_flag: bool,
pub info_flag: bool,
pub cp_option: String,
pub x_jre_option: String,
pub class: String,
pub args: Vec<String>,
}
命令行操作命令
opts.optopt("", "classpath", "Specify the classpath", "classpath");
opts.optopt("", "cp", "Specify the classpath", "classpath");
opts.optopt("", "Xjre", "Path to jre", "jre");
对命令进行解析
match matches.opt_str("classpath") {
Some(classpath) => {
command.cp_option = classpath;
},
None => {
match matches.opt_str("cp") {
Some(cp) => {
command.cp_option = cp;
},
None => {}
}
}
}
match matches.opt_str("Xjre") {
Some(x_jre_option) => {
command.x_jre_option = x_jre_option;
},
None => {}
}
// 未定义的参数放在 free Vec 中
if !matches.free.is_empty() {
command.class = matches.free[0].clone();
command.args = matches.free[1..].to_vec();
}
上面就是命令行解析的内容了,命令行解析完后,我们要通过读取的命令解析执行对应的步骤,比如我输入下面的命令
- java -classpath xxx/xx/xxx/xx java.lang.Object
就应该可以读取Objecgt的文件数据,不过肯定是二进制格式的,所以需要定义一个读取classpath的包。当前就暂且叫做classpath
这儿我们还是先引入一些必要依赖
Cargo.toml
文件
mockall = "0.11.3"
zip = {version = "0.6.4", default-features = false, features = ["deflate"]}
tokio = {version = "1.26.0", features = ["full"]}
- mockall 是用来写单测的,好的开发者应该对于每一个module的开发都有完善的单测验证自己的逻辑
- zip是压缩包,用于读取类似.jar 或者 .zip 或者 .war 或者 .rar 等文件,可能更多的是jar包,不过我们把其他压缩包也考虑进去
- tokio 是用来实现一个自定义线程池,可以实现IPC高性能通信那种,当然这个是一个优化点,后续会考虑慢慢实现,不做也不影响整个项目的开发
首先是项目结构,目前我们考虑有通过 绝对路径去获取class文件的,也有通过xx/*等方式获取的,也有.jar的压缩文件,也有读取多个文件的,所以我们定义四个rs文件来区分它们
classpath
中需要定义两个重要的方法
分别是 parse() 和 read_class(), 但是考虑到读取class是一个通用的方法,需要读取不同种类的文件,所以就准备定义成一个trait来使用
::: block-1
这儿说的trait,用java和go的看法来说,就是接口,不过在rust中叫特征。 :::
所以我们定义一个公共的Entry,作为trait的入口
entry.rs
/// 目前有这么几种文件对象
/// -classpath file => 目录形式的类路径 entry_dir.rs
/// -classpath file1.jar:file2.jar:file3.jar => 多个文件对象组成的类路径 entry_multiple.rs
/// -classpath file/* => 以*结尾的路径 entry_wildcard.rs
/// -classpath file.jar => 压缩文件路径 entry_compression.rs
pub trait Entry: fmt::Display {
fn read_class(&mut self, class_name: &str) -> Result<Vec<u8>, String>;
}
同时在classpath中定义我们之前说好的两个方法,分别作解析和读取文件数据。
classpath.rs
impl Classpath {
pub fn parse(jre_option: &str, cp_option: &str) -> Self {
// 加载 boot_classpath
// 加载 ext_classpath
// 加载 user_classpath
todo!()
}
}
impl Entry for Classpath {
/// 读取class数据,从boot_classpath
/// 到 ext_classpath 到 user_classpath
fn read_class(&mut self, class_name: &str) -> Result<Vec<u8>, String> {
todo!()
}
}
- boot_classpath: jvm自带类库,包含JavaSE核心类库,jvm启动时必用路径,包含jre/lib/lib/rt.jar 和 jre/lib/ext 中的jar包
- ext_classpath: jvm扩展路径,用于加载开发者自己编写的扩展类库
- user_classpath: 用户自定义路径,用于加载用户自己编写的java类
最后就是main方法的启动,在一开始我们最后是start_jvm()方法来进行启动的,所以我们也把解析class和读取class的操作放在这个方法中
main.rs
fn start_jvm(cmd: Command) {
info!("[WELCOME USE THIS JVM, It's named azh after my girlfriend, thanks]");
let mut classpath = Classpath::parse(&cmd.x_jre_option, &cmd.cp_option);
info!("classpath: {} class: {} args: {:?}", classpath, cmd.class, cmd.args);
let class_name = cmd.class.replace(".", "/");
let class_data = match classpath.read_class(&class_name) {
Ok(class_data) => class_data,
Err(err) => { panic!("Could not find or load main class {}: {}", cmd.class, err); },
};
info!("class data: {:?}", class_data)
}
上面就是整个大体步骤,整体脉络就是两步骤,解析classpath,读取class文件,下一章会具体介绍jar、j*、(jar1,jar2)等文件的具体的parse方法和三种类路径的parse方法。
可以自己先写一下试试,对于单个class文件应该不难,就是从中读取二进制数据而已。
::: block-2
#大厂##23届找工作求助阵地##我的实习求职记录##Java##实习#之前说的线程池,主要是想实现一个IPC高性能的缓存buffer,其中尽量通过Arc等指令和避免Box来做到无锁和不操作堆内存来达到高性能,不过目前还在构思当中,当前缓存的方案是想实现HotRing论文,可惜这个数据结构并不是那么容易实现,而且对于Rust写链表,相信学过的人都知道,这算是一个劝退题了~ :::