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

首先需要明确几个问题。

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

你可能感兴趣的:([Golang实现JVM第六篇]实现Native方法)