[Golang實現JVM第六篇]實現Native方法

首先需要明確幾個問題荣赶。

沒有Native方法JVM什么也做不了

可能很多人認為native方法是Java里的禁區(qū)磺平,使用本地方法會犧牲可移植性先壕,而且還會有額外開銷,貌似幾乎沒有程序員會在實際項目中寫本地方法焚碌,這玩意就是個很冷門的東西。其實這種看法是錯誤的霸妹,哪怕一個Hello Word程序都是要嚴重依賴于本地方法的十电。在JDK中,你會發(fā)現任何涉及到I/O、線程操作的類鹃骂,層層追蹤源碼后最終都能找到一個對應的native調用台盯,真正把Hello World打印到控制臺的正是這些native方法。而用于啟動線程的Thread.start()方法畏线,最終也是調用了一個叫native void start0()的本地方法静盅。因為任何對硬件的操作都必須通過操作系統提供的系統調用(system call)來實現,JVM作為一個用戶程序并不具備操作硬件的能力寝殴,必須通過發(fā)起系統調用才能實現網絡I/O蒿叠、文件I/O、創(chuàng)建線程等操作蚣常。

"本地"是相對于VM實現而言的

另一個誤區(qū)是認為只要是本地方法那就一定要用C/C++實現栈虚,這也是不正確的。本地是相對于VM的執(zhí)行環(huán)境而言的史隆,如果VM是用C++寫成(如Hotspot JVM)魂务,那么C++就是這個VM的本地語言;如果JVM是用python寫成泌射,那么python就是JVM的本地語言粘姜。假如有人在瀏覽器中使用Javascript實現了一個JVM,那么這個在瀏覽器中運行的可憐的Java代碼如果想在console中打印Hello Word, 那就必須執(zhí)行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

可以看到跟普通方法一樣都有描述符和訪問標記雳刺,但是沒有字節(jié)碼。

然后再看一下如果調用本地方法javac又會生成些啥:

package com.fh;
import cn.minijvm.io.Printer;

public class ArrayTest {
    public static void main(String[] args) {
                int sum = 10;
        Printer.print(sum);
    }
}

字節(jié)碼:

        ... ... 省略
        ... ...
        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方法引用常量掖桦,跟正常調用靜態(tài)方法是完全一致的,沒啥特殊的地方供汛。

看完這些后我們就可以想出這樣一種實現思路:

  • 實現一個本地方法表枪汪,保存從【類名+方法描述符】到【go函數】的一個映射
  • 在常量池中查找到目標方法引用常量后(如上面的#2)凛俱,先判斷下此方法是否帶有Native標記,如果沒有就正常去查找字節(jié)碼循環(huán)解釋執(zhí)行料饥,如果有則查本地方法表,找到對應的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啟動過程中執(zhí)行一次:

// 完整代碼: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閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異掐隐,居然都是意外死亡狗热,警方通過查閱死者的電腦和手機钞馁,發(fā)現死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匿刮,“玉大人僧凰,你說我怎么就攤上這事∈焱瑁” “怎么了训措?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長光羞。 經常有香客問我绩鸣,道長,這世上最難降的妖魔是什么纱兑? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任呀闻,我火速辦了婚禮,結果婚禮上潜慎,老公的妹妹穿的比我還像新娘捡多。我一直安慰自己,他們只是感情好铐炫,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布局服。 她就那樣靜靜地躺著,像睡著了一般驳遵。 火紅的嫁衣襯著肌膚如雪淫奔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天堤结,我揣著相機與錄音唆迁,去河邊找鬼。 笑死竞穷,一個胖子當著我的面吹牛唐责,可吹牛的內容都是我干的。 我是一名探鬼主播瘾带,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼鼠哥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了看政?” 一聲冷哼從身側響起朴恳,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎允蚣,沒想到半個月后于颖,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡嚷兔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年森渐,在試婚紗的時候發(fā)現自己被綠了做入。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡同衣,死狀恐怖竟块,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情耐齐,我是刑警寧澤浪秘,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站蚪缀,受9級特大地震影響,放射性物質發(fā)生泄漏恕出。R本人自食惡果不足惜询枚,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望浙巫。 院中可真熱鬧金蜀,春花似錦、人聲如沸的畴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽丧裁。三九已至护桦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間煎娇,已是汗流浹背二庵。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留缓呛,地道東北人催享。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像哟绊,于是被迫代替她去往敵國和親因妙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359