首先需要明确几个问题。
没有Native方法JVM什么也做不了
可能很多人认为native方法是Java里的禁区,使用本地方法会牺牲可移植性,而且还会有额外开销,貌似几乎没有程序员会在实际项目中写本地方法,这玩意就是个很冷门的东西。其实这种看法是错误的,哪怕一个Hello Word程序都是要严重依赖于本地方法的。在JDK中,你会发现任何涉及到I/O、线程操作的类,层层追踪源码后最终都能找到一个对应的native
调用,真正把Hello World
打印到控制台的正是这些native
方法。而用于启动线程的Thread.start()
方法,最终也是调用了一个叫native void start0()
的本地方法。因为任何对硬件的操作都必须通过操作系统提供的系统调用(system call)来实现,JVM作为一个用户程序并不具备操作硬件的能力,必须通过发起系统调用才能实现网络I/O、文件I/O、创建线程等操作。
"本地"是相对于VM实现而言的
另一个误区是认为只要是本地方法那就一定要用C/C++实现,这也是不正确的。本地是相对于VM的执行环境而言的,如果VM是用C++写成(如Hotspot JVM),那么C++就是这个VM的本地语言;如果JVM是用python写成,那么python就是JVM的本地语言。假如有人在浏览器中使用Javascript实现了一个JVM,那么这个在浏览器中运行的可怜的Java代码如果想在console中打印Hello Word, 那就必须执行JS里的console.log()
才能实现,于是JS就成了他的本地语言了,浏览器下JS没有的能力(如文件读写)那对应的JVM也无法实现。由于我们的Mini-JVM使用Go来实现的,那自然就要用Go来实现native方法了,而不是C++。同样,Go没有的能力,例如OS线程,那Mini-JVM也就没有,但是可以用协程来模拟线程,效果也差不多。
实现本地方法
首先我们要看一下javac是如何编译本地方法的,例如这个类:
package cn.minijvm.io;
public class Printer {
public static native void print(int num);
}
编译后使用javap -verbose Printer
查看:
public static native void print(int);
descriptor: (I)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
可以看到跟普通方法一样都有描述符和访问标记,但是没有字节码。
然后再看一下如果调用本地方法javac又会生成些啥:
package com.fh;
import cn.minijvm.io.Printer;
public class ArrayTest {
public static void main(String[] args) {
int sum = 10;
Printer.print(sum);
}
}
字节码:
... ... 省略
... ...
77: iload_1
78: invokestatic #2 // Method cn/minijvm/io/Printer.print:(I)V
81: return
常量池:
#1 = Methodref #4.#14 // java/lang/Object."":()V
#2 = Methodref #15.#16 // cn/minijvm/io/Printer.print:(I)V
#3 = Class #17 // com/fh/IfTest
可以看到生成了invokestatic
指令,后面跟着一个常量池下标2, 2对应常量池中的元素就是一个普通的MethodRef
方法引用常量,跟正常调用静态方法是完全一致的,没啥特殊的地方。
看完这些后我们就可以想出这样一种实现思路:
- 实现一个本地方法表,保存从【类名+方法描述符】到【go函数】的一个映射
- 在常量池中查找到目标方法引用常量后(如上面的#2),先判断下此方法是否带有
Native
标记,如果没有就正常去查找字节码循环解释执行,如果有则查本地方法表,找到对应的Go函数,从栈中取出参数后直接调用对应的Go函数;如果本地方法表中没有找到对应的函数就直接报错
本地方法表可以简单的实现如下:
// 完整代码:https://github.com/wanghongfei/mini-jvm/blob/master/vm/native_method_table.go
// 本地方法表
type NativeMethodTable struct {
MethodInfoMap map[string]*NativeMethodInfo
}
type NativeMethodInfo struct {
// 方法名
Name string
// 类的全名
FullClassName string
// 描述符;
// String getRealnameByIdAndNickname(int id,String name) 的描述符为 (ILjava/lang/String;)Ljava/lang/String;
Descriptor string
// 对应的go函数
EntryFunc NativeFunction
}
要注意这里NativeMethodTable.MethodInfoMap
不需要用线程安全的Map, 因为这个Map只会在JVM启动时初始化一次,后面就不再更改了,多线程(协程)读是安全的。
Go函数的定义如下:
// JVM的本地方法, 即go函数;
// 参数args[0]固定为MiniJVM的指针
type NativeFunction func(args ...interface{}) interface{}
这里为了能在native方法,也就是Go函数中访问到JVM中的数据,我们可以约定在调用这个函数时第一个参数一定是MiniJVM的指针,从第二个参数开始才是java native方法中声明的参数。例如Printer.printInt(int num)
这个本地方法,Mini-JVM的go函数实现可以是:
func PrintInt(args ...interface{}) interface{} {
fmt.Println(args[1])
return true
}
也就是说,只要遇到了cn.minijvm.io.Printer.printInt(10)
,那我们就调用Go的PrintInt()
函数,并且保证args[0]
是JVM指针,args[1]
是10就可以了。
这里还需要一个本地方法注册的逻辑,也就是向本地方法表中添加数据。这个逻辑只需在JVM启动过程中执行一次:
// 完整代码:https://github.com/wanghongfei/mini-jvm/blob/master/vm/mini_jvm.go
// 本地方法表
nativeMethodTable := NewNativeMethodTable()
vm.NativeMethodTable = nativeMethodTable
// 注册本地方法
nativeMethodTable.RegisterMethod("cn.minijvm.io.Printer", "print", "(I)V", PrintInt)
nativeMethodTable.RegisterMethod("cn.minijvm.io.Printer", "printInt", "(I)V", PrintInt)
nativeMethodTable.RegisterMethod("cn.minijvm.io.Printer", "printInt2", "(II)V", PrintInt2)
nativeMethodTable.RegisterMethod("cn.minijvm.io.Printer", "printChar", "(C)V", PrintChar)
nativeMethodTable.RegisterMethod("cn.minijvm.io.Printer", "printString", "(Ljava/lang/String;)V", PrintString)
nativeMethodTable.RegisterMethod("cn.minijvm.concurrency.MiniThread", "start", "(Ljava/lang/Runnable;)V", ExecuteInThread)
nativeMethodTable.RegisterMethod("cn.minijvm.concurrency.MiniThread", "sleepCurrentThread", "(I)V", ThreadSleep)
这样我们就可以通过查表的方式来找到native java方法对应的go函数了:
// 完整代码: https://github.com/wanghongfei/mini-jvm/blob/master/vm/interpreted_execution_engine.go
// 是native方法
if _, ok := flagMap[accflag.Native]; ok {
// 查本地方法表
nativeFunc, argCount := i.miniJvm.NativeMethodTable.FindMethod(def.FullClassName, methodName, methodDescriptor)
if nil == nativeFunc {
// 该本地方法尚未被支持
return fmt.Errorf("unsupported native method '%s'", method)
}
// 从操作数栈取出argCount个参数
argCount += 1
args := make([]interface{}, 0, argCount)
for ix := 0; ix < argCount; ix++ {
arg, _ := lastFrame.opStack.Pop()
args = append(args, arg)
}
// 将jvm指针放到参数里,给native方法访问jvm的能力
args[argCount - 1] = i.miniJvm
// 因为出栈顺序跟实际参数顺序是相反的, 所以需要反转数组
for ix := 0; ix < argCount / 2; ix++ {
args[ix], args[argCount - 1 - ix] = args[argCount - 1 - ix], args[ix]
}
i.miniJvm.DebugPrintHistory = append(i.miniJvm.DebugPrintHistory, args[1:]...)
// 调用go函数
nativeFunc(args...)
return nil
}
用这种简单的思路虽然做不到像真正的JVM那样允许程序员编写go函数来支持自定义的native方法,但理论上已经可以实现JDK中所有native方法了,比如线程相关的操作。到这里我们仍然连一个最简单的Hello World都实现不了,因为要实现Printer.print(String word)
,我们还需要实现Object,支持new
指令,然后支持String.class的加载和解析。当然这些在Mini-JVM(https://github.com/wanghongfei/mini-jvm)中都已经实现了,以后会介绍。