代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康:JVM全套教程。
1.本地方法概述
要运行Java程序,Java虚拟机和Java类库一起构成了Java运行时环境。Java类库主要用Java语言编写,一些无法用Java语言实现的方法则使用本地语言编写,这些方法叫作本地方法。
简单地讲,一个Native Methd就是一个Java调用非Java代码的接口。一个Native Method是一个实现由非Java语言实现的,比如C的Java方法。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中你可以用extern“c”告知C++编译器去用一个C的函数。
“A native method is a Java method whose implementation isprovided by non-java code.”
在定义一个native method时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合 c/C++程序
比如 ,在java.lang包下的Object

在这之中有一个方法,就是由其他语言实现。用native标识。

为什么要使用Native Method ?
我翻译了一下:

OpenJDK类库中的本地方法是用JNI(Java Native Interface)编写的,但是要让虚拟机支持JNI规范还需要做大量的工作。为了不陷入JNI规范的细节之中,将使用Go语言来实现这些方法。
2.注册和查找本地方法
在开始实现本地方法之前,先实现一个本地方法注册表,用来注册和查找本地方法。
整体代码
在\native目录下创建registry.go,先在其中定义NativeMethod类型和registry变量,代码如下:
package native
import "jvmgo/ch09/rtda"
// 本地方法
type NativeMethod func(frame *rtda.Frame)
// 本地方法注册表
var registry = map[string]NativeMethod{}
func emptyNativeMethod(frame *rtda.Frame) {
// do nothing
}
// 注册本地方法
func Register(className, methodName, methodDescriptor string, method NativeMethod) {
//类名、方法名和方法描述符作为本地方法注册表的键
key := className + "~" + methodName + "~" + methodDescriptor
registry[key] = method
}
// 查找本地方法
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
}
代码解释
把本地方法定义成一个函数,参数是Frame结构体指针,没有返回值。这个frame参数就是本地方法的工作空间,也就是连接Java虚拟机和Java类库的桥梁。

registry变量是个哈希表,值是具体的本地方法实现,类名、方法名和方法描述符作为本地方法注册表的键。

Register()函数,类名、方法名和方法描述符加在一起才能唯一确定一个方法,所以把它们的组合作为本地方法注册表的键,Register()函数把前述三种信息和本地方法实现关联起来。

FindNativeMethod()方法,FindNativeMethod()方法根据类名、方法名和方法描述符查找本地方法实现,如果找不到,则返回nil。java.lang.Object等类是通过一个叫作registerNatives()的本地方法来注册其他本地方法的。但是后面我们将自己注册所有的本地方法实现。所以像registerNatives()这样的方法就没有太大的用处。为了避免重复代码,这里统一处理,如果遇到这样的本地方法,就返回一个空的实现。

3.调用本地方法
前面用一段hack代码来跳过本地方法的执行。
编辑\instructions\base\method_invoke_logic.go,将InvokeMethod()函数中的hack代码删除或注释:

Java虚拟机规范并没有规定如何实现和调用本地方法。
本地方法并没有字节码,如何利用Java虚拟机栈来执行呢?Java虚拟机规范预留了两条指令,操作码分别是0xFE和0xFF。
下面将使用0xFE指令来达到这个目的。打开ch09\rtda\heap\method.go文件,
为了避免newMethods()函数变得太长,我们抽取出一个newMethod()函数,代码如下:
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
}修改newMethods()函数,改动如下:
func newMethods(class *Class, cfMethods []*classfile.MemberInfo) []*Method {
methods := make([]*Method, len(cfMethods))
for i, cfMethod := range cfMethods {
methods[i] = newMethod(class, cfMethod)
}
return methods
}
injectCodeAttribute()方法也在method.go文件,本地方法在class文件中没有Code属性,所以需要给maxStack和maxLocals字段赋值。本地方法帧的操作数栈至少要能容纳返回值,为了简化代码,暂时给maxStack字段赋值为4。因为本地方法帧的局部变量表只用来存放参数值,所以把argSlotCount赋给maxLocals字段刚好。至于code字段,也就是本地方法的字节码,第一条指令都是0xFE,第二条指令则根据函数的返回值选择相应的返回指令。代码如下:
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
}
}
由于把方法描述符的解析挪到了newMethod()函数中,所以改变一下calcArgSlotCount()方法,变动如下:
// 计算方法参数占用的slot数量
func (self *Method) calcArgSlotCount(paramTypes []string) {
for _, paramType := range paramTypes {
self.argSlotCount++
// long和double占两个slot
if paramType == "J" || paramType == "D" {
self.argSlotCount++
}
}
if !self.IsStatic() {
// 如果不是静态方法,给隐藏的参数this添加一个slot位置
self.argSlotCount++ // `this` reference
}
}
实现0xFE指令:
创建\instructions\reserved\invokenative.go文件,在其中定义0xFE(后面称为invokenative)指令,这个指令不需要操作数,根据类名、方法名和方法描述符从本地方法注册表中查找本地方法实现,如果找不到,则抛出UnsatisfiedLinkError异常,否则直接调用本地方法。代码如下:
package reserved
import (
"jvmgo/ch09/instructions/base"
"jvmgo/ch09/native"
"jvmgo/ch09/rtda"
)
// Invoke native method
type INVOKE_NATIVE struct{ base.NoOperandsInstruction }
func (self *INVOKE_NATIVE) Execute(frame *rtda.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语句注释去掉。
4.参考
尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
周志明:深入理解java虚拟机
张秀宏:自己动手写Java虚拟机 (Java核心技术系列)


