查看手写 JAVA 虚拟机系列可以进我的博客园主页查看.
我们知道, 我们编译. java 并运行. class 文件时, 需要一些 java 命令, 如最简单的 helloworld 程序.
这里的程序最好不要加包名, 因为加了包名的话编译和运行需要有所改动.
看这里的命令. javac 为编译命令, 我们知道 java 的特点是一次编译, 到处运行. 这里的编译指的就是 javac, 对于 java 程序即. java 文件, 先要用 javac 编译成字节码. 然后将字节码 (.class 文件) 放到 java 虚拟机中运行, 即上图中的 java HelloWorld,java 虚拟机把字节码翻译成对应机器上的机器指令, 再由机器来执行具体的机器指令. 也就是说 java 程序员是直接与 java 虚拟机交互, 简介与机器交互. 所以虚拟机完成的是 java 命令, 也就是我们要完成的是 java 这个指令的功能.
那么我们把第一个目标定为, 实现简单的命令行. 即我们通过命令行可以输入一些内容, 虚拟机读取之后可以给一定的反馈.
GO 语言中有两个和命令行相关的包, 分别是 os 和 flag(java 中以类库即 jar 文件导入, go 中直接以包的形式导入).
首先在 GOPATH 目录下的 src 里面新建一个 jvmgo 文件夹作为我们的工作空间目录, jvmgo 里面再新建一个 ch01 为我们的第一个目标源码文件夹, 添加 cmd.go 文件.
在 cmd.go 里面输入如下代码(由于博客园的添加代码方式不支持 go 语言着色, 所以采用 C 语言着色, 高亮可能不太正确)
- package main
- import "flag"
- import "fmt"
- import "os"
- // 定义 Cmd 结构体
- type Cmd struct{
- helpFlag bool
- versionFlag bool
- cpOption string
- class string
- args []string
- }
- // 解析命令行参数
- func parseCmd() *Cmd {
- cmd:=&Cmd{}
- // 将 printUsage 函数传给 flag.Usage
- flag.Usage=printUsage
- // 设置各种解析的选项
flag.BoolVar(&cmd.helpFlag, "help", false, "print help message")
flag.BoolVar(&cmd.helpFlag, "?", false, "print help message")
flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit")
flag.StringVar(&cmd.cpOption, "classpath", "","classpath")
flag.StringVar(&cmd.cpOption, "cp", "","classpath")
- // 所有选项设置完成后调用 flag.Parse 解析所有选项, 如果 Parse 失败, 则调用 flag.Usage 打印帮助信息
- flag.Parse()
- // 调用 flag.Args 函数捕获未被解析的参数, 第一个参数为主类名, 后面的为传递给主类的参数
- args:=flag.Args()
- if len(args)>0{
- cmd.class=args[0]
- cmd.args=args[1:]
- }
- return cmd
- }
- func printUsage() {
- fmt.Printf("Usage:%s[-options] class [args...]\n",os.Args[0])
- }
第一行为包名, main 包, 接着引入了三个包 os,flag,fmt.os 和 flag 都是处理命令行所需的包, fmt 类似于 C 语言的 printf 和 scanf 等格式化 IO. 再往下定义了一个结构体 Cmd, 用来这个数据结构来格式化存储输入的命令行信息. helpFlag 参数为命令行是否请求 help,versionFlag 参数为命令行是否请求 version,cpOption 为命令行传入的 classpath 即目标. class 文件所在文件夹, class 为命令行传入的. class 文件名(不包括. class),args 为命令行传入的其他参数.
紧接着是一个 parseCmd 函数(go 语言有函数和方法之分, 方法调用需要 receiver, 函数调用则不需要 ), 返回值为 * Cmd, 用来解析 cmd 传过来的参数. 该函数里面先声明一个 cmd 并给这个 cmd 赋值一个新建的 Cmd 对象. go 语言中的 ":=" 为声明并赋值, 而 "=" 为赋值. 先把 printUsage 的函数赋值给 flag.Usage, 然后调用 flag 设置需要解析的选项, 全部解析完毕, 调用 Parse 函数解析所有选项. 解析成功则结束, 解析失败则调用 printUsage 打印到控制台.
flag.Args 可以捕获其他没有被解析的参数. 上面解析成功之后, 第一个参数就是主类名, 剩下的就是传给主类的参数.
工具类编写完成, 下一个是主函数. 先上主函数代码:
- package main
- import "fmt"
- func main() {
- // 调用 parseCmd 解析命令行参数
- cmd:=parseCmd()
- if cmd.versionFlag{
- // 输入了 - version 选项
- fmt.Println("version 0.0.1")
- }else if cmd.helpFlag||cmd.class==""{
- // 输入了 - help 选项
- printUsage()
- }else{
- // 启动 jvm
- stratJVM(cmd)
- }
- }
- func stratJVM(cmd *Cmd){
- fmt.Printf("classpath:%s class:%s args:%v\n",
- cmd.cpOption,cmd.class,cmd.args)
- }
跟 java 类似, 在 go 里面 main 是一个特殊的包, go 程序的入口就是 main 函数, 但是不接受任何参数, 也不能有返回值. main 函数先调用 parseCmd 解析命令行参数, 如果是 - version 则返回版本号, 如果是 - help 则返回帮助信息, 如果是其他则启动 jvm, 这里用一些输出信息 "假装" 启动了 jvm, 真正的 jvm 代码后面会加上.
至此, 对命令行的解析工作全部完成. 先展示一下整个工作目录的结构, 不然后面编译运行的时候会出错.
我们的工作目录是 D 盘下的 JVM 里的 goWorkSpace, 再下面 src,jvmgo,ch01,ch01 里面包含的是我们的 go 文件.
来测试一下, 打开一命令行, 输入 go install jvmgo\ch01. 这个命令是使用 go.exe 来 install 文件, 这个文件存在于 GOPATH 下面的文件夹(jvmgo\ch01 中), 结果如图:
然后在工作空间 (GOPATH) 的 bin 文件夹中就多出了一个 ch01.exe.
在此处打开命令行. 可以进行一些操作:
到这里, 我们的命令行工具就完成了, 虽然还没有涉及真正的虚拟机设计, 但这也是虚拟机运行的重要一步, 后面会逐渐介绍虚拟机的设计.
来源: https://www.cnblogs.com/GoForMyDream/p/8863038.html