bboyjing's blog

自己动手写JVM二十九【本地方法调用(一)】

我们已经知道,要想运行Java程序,除了Java虚拟机之外,还需要Java类库的配合。Java虚拟机和Java类库的配合。Java虚拟机和Java类库一起构成了Java运行环境。Java类库主要用Java语言编写,一些无法用Java语言实现的方法则使用本地语言编写,这些方法叫做本地方法。从本章开始将陆续实现一些Java类中的本地方法。为了不陷入JNI规范的细节之中,我们将使用Go语言来实现。本章的所有代码位于/native包下。

注册和查找本地方法

在开始实现本地方法之前,先实现一个本地方法注册表,用来注册和查找本地方法。新建/native/registry.go文件:

1
2
3
4
5
// 本地方法
type NativeMethod func(frame *rtdata.Frame)
// 注册表
var registry = map[string]NativeMethod{}

本地方法定义成一个函数,参数是Frame结构体,没有返回值。这个frame参数就是本地方法的工作空间,也就是连接Java虚拟机和Java类库的桥梁,后面会看到它如何发挥作用的。registry是一个哈希表,值是具体的本地方法实现,至于键是什么,先看下Register()函数:

1
2
3
4
5
// 注册本地方法
func Register(className, methodName, methodDescriptor string, method NativeMethod) {
key := className + "~" + methodName + "~" + methodDescriptor
registry[key] = method
}

类名、方法名和方法描述符加在一起才能唯一确定一个方法,所以把它们的组合作为本地方法注册表的键。继续编辑registry.go文件,在其中实现FindNativeMethod():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 查找本地方法
func FindNativeMethod(className, methodName, methodDescriptor string) NativeMethod {
key := className + "~" + methodName + "~" + methodDescriptor
if method, ok := registry[key]; ok {
return method
}
if methodDescriptor == "()V" && methodName == "registerNatives" {
return emptyNativeMethod
}
return nil
}
func emptyNativeMethod(frame *rtdata.Frame) {
// do nothing
}

之前说过,java.lang.Object等类是通过有一个叫做registerNatives()的本地方法来注册其他本地方法的,这里我们自己注册所有的本地方法实现,所以像registerNatives()这样的方法就没有太大用处了,为了避免重复代码,这里统一处理,如果遇到这样的本地方法,就返回一个空的实现。

调用本地方法

之前是用一段hack代码来跳过本地方法的执行的,现在可以来完善这段代码了。先删除/instructions/base/method_invoke_logic.go文件的InvokeMethod()方法中的hack代码删除。Java虚拟机规范并没有规定如何实现和调用本地方法,这给了我们充分的空间来发挥自己的想象力。我们将利用Java虚拟机栈来执行本地方法,所以除了删除hack代码之外,不用做任何修改。但是本地方法并没有字节码,如何利用Java虚拟机栈来执行呢?Java虚拟机规范预留了两条指令,操作码分别是0xFE和0xFF。下面将使用0xFE指令来达到目的,修改/rtdata/heap/method.go文件的newMethod()方法,下面贴出主要修改的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func newMethod(class *Class, cfMethod *classfile.MemberInfo) *Method {
method := &Method{}
method.class = class
method.copyMemberInfo(cfMethod)
method.copyAttributes(cfMethod)
// 分解方法描述符
md := parseMethodDescriptor(method.descriptor)
method.calcArgSlotCount(md.parameterTypes)
// 如果是本地方法,注入字节码和其他信息
if method.IsNative() {
method.injectCodeAttribute(md.returnType)
}
return method
}
// 注入字节码等信息
func (self *Method) injectCodeAttribute(returnType string) {
// 暂定操作数栈深度
self.maxStack = 4 // todo
self.maxLocals = self.argSlotCount
switch returnType[0] {
case 'V':
self.code = []byte{0xfe, 0xb1} // return
case 'L', '[':
self.code = []byte{0xfe, 0xb0} // areturn
case 'D':
self.code = []byte{0xfe, 0xaf} // dreturn
case 'F':
self.code = []byte{0xfe, 0xae} // freturn
case 'J':
self.code = []byte{0xfe, 0xad} // lreturn
default:
self.code = []byte{0xfe, 0xac} // ireturn
}
}

本地方法在class文件中没有Code属性,所以需要给maxStack和maxLocals字段赋值。本地方法帧的操作数栈至少要能容纳返回值,为了简化代码,暂时给maxStack字段赋值为4。因为本地方法帧的局部变量表只用来存放参数值,所以把argSlotCount赋给maxLocals字段正好。至于code字段,也就是本地方法的字节码,第一条指令都是0xFE,第二条指令则根据函数的返回值选择相应的返回指令。
下面就来实现0xFE指令,实现代码位于/instructions/reserved/invokenative.go文件中,后面就把0xFE成为invokenative指令,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 0xFE指令结构体
type INVOKE_NATIVE struct{ base.NoOperandsInstruction }
func (self *INVOKE_NATIVE) Execute(frame *rtdata.Frame) {
method := frame.Method()
className := method.Class().Name()
methodName := method.Name()
methodDescriptor := method.Descriptor()
// 从本地方法注册表中查找方法
nativeMethod := native.FindNativeMethod(className, methodName, methodDescriptor)
if nativeMethod == nil {
methodInfo := className + "." + methodName + methodDescriptor
panic("java.lang.UnsatisfiedLinkError: " + methodInfo)
}
// 执行本地方法
nativeMethod(frame)
}

顺便再把/instructions/factory.go文件中,对应的invokenative指令case注释放开。到此,准备工作就做好了,下面只要实现本地方法了。

反射

Java的反射机制非常强大,本节只涉及冰山一脚。

类和对象之间的关系

在Java中,类叶表现为普通的对象,它的类是java.lang.Class。听起来有点像鸡生蛋还是蛋生鸡的问题:类也是对象,而对象又是类的实例。看到这里,有点蒙了,为了帮助理解类和类对象之间的关系,我们想象一个简化的Java虚拟机运行时状态:方法区中只加载了两个类,java.lang.Object和java.lang.Class;堆中只通过new指令分配了一个对象。此时Java虚拟机的内存状态如下图所示:
jvmgo_46
图中区分了堆和方法区,方法区中的class_Object和class_Class分别是java.lang.Object和java.lang.Class的类数据,也就是对应我们Go项目的Class结构体;堆中的object_Object和object_Class分别是java.lang.Object和java.lang.Class的类对象,是java.lang.Class的实例,该功能下面会实现。object_XXX是单独的java.lang.Object实例。类对象我们暂且这样理解,下面继续聊反射。
Java有强大的反射能力,可以在运行期间获取类的各种信息、存取静态和实例变量、调用方法,等等。要想运用这种能力,获取类对象是第一步。在Java语言中,有两种方式可以获得类对象引用:使用类字面值和调用对象的getClass()方法:

1
2
System.out.println(String.class);
System.out.println("abc".getClass());

之前通过Object结构体的class字段建立了类和对象之间的单向关系,现在把这个关系补充完整,让它成为双向的。修改/rtdata/heap/class.go文件:

1
2
3
4
5
6
7
8
9
type Class struct {
...
// 类对象(java.lang.Class实例)
jClass *Object
}
func (self *Class) JClass() *Object {
return self.jClass
}

通过jClass字段,每个Class结构体实例都与一个类对象关联。再修改/rtdata/heap/object.go文件:

1
2
3
4
5
6
7
// Object结构体
type Object struct {
...
extra interface{}
}
// 省略Getter、Setter方法

extra字段用来记录Object结构体实例的额外信息。

下节继续将反射。