手写JVM(二十六)-查找和调用本地方法

代码、内容参考来自于张秀宏大佬的自己动手写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标识。
标识符native可以与所有其它的iava标识符连用,但是abstract除外

 

为什么要使用Native Method ?

官网:Introduction (oracle.com)

我翻译了一下:

 

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核心技术系列)

GO语言官网:Standard library – Go Packages

Java虚拟机规范:Chapter 4. The class File Format (oracle.com)

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇