美文网首页
[Golang实现JVM第六篇]实现Native方法

[Golang实现JVM第六篇]实现Native方法

作者: 司青玄 | 来源:发表于2020-08-24 16:39 被阅读0次

    首先需要明确几个问题。

    没有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."<init>":()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)中都已经实现了,以后会介绍。

    相关文章

      网友评论

          本文标题:[Golang实现JVM第六篇]实现Native方法

          本文链接:https://www.haomeiwen.com/subject/gybnjktx.html