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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342