首先需要明確幾個問題荣赶。
沒有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)中都已經實現了陵刹,以后會介紹。