從零開始開發JVM語言(十)指令與InvokeDynamic

目錄戳這裏java

上一篇完成了語義分析的第三步。這一篇將開始語義分析的第四步也是最後一步,指令的解析。git

這一步目標是將Statement轉化爲指令。github

JVM針對同一事件的指令經常分爲5種:bootstrap

i,l,f,d,a

分別是 整型,長整型,浮點型,雙精度型和引用類型。jvm

比方說,從局部變量表的第0個位置讀取參數.net

iload_0   // int
lload_0   // long
fload_0   // float
dload_0   // double
aload_0   // ref

而像byte,short,char,boolean統一使用i指令來完成讀寫。調試

JVM指令雖然相似彙編卻比彙編高級不少。JVM指令的格式在第七篇中大體說了一下,不過最好仍是參考jvm specification詳細瞭解。
這裏說一說比彙編高級的地方。code

從字節碼就能看出,JVM並不要求編譯器在編譯期進行鏈接。而是記錄一個字符串,表示須要鏈接的類。好比父類就是用相似java/lang/Object來描述的。指令集中一樣,像「調用方法」,「設置、讀取字段」這樣的功能,所有都是字符串描述鏈接信息。對象

putfield #13 // some/pkg/Class.someField

invokevirtual #25 // java/io/PrintStream.println(Ljava/lang/Object;)V

雖然實際結構與註釋中的字符串有些差異,不過大體如此。具體的差別等寫到「字節碼生成」時候再說。blog

這裏須要着重說一說InvokeDynamic,由於寫jvm語言若是不是靜態類型強類型,那麼這個指令頗有用。

#InvokeDynamic

下文將InvokeDynamic簡稱indy

這是一個很不錯的指令,它讓弱類型或者動態類型語言可以很是優雅的編寫字節碼(有時候也不只僅是字節碼)。

indy工做過程爲:

  1. 從字節碼獲取調用者的lookup,方法名,MethodType,以及一些其它信息
  2. 調用BootstrapMethod獲取CallSite
  3. 使用操做數棧做爲參數調用CallSite
  4. 若是有返回值則將返回值寫入操做數棧

Java8只有在使用Lambda時才經過indy完成,個人語言Latte-lang只要在編譯期找不到要調用的方法,就會使用indy指令。

經過javap反編譯來查看,能夠看出indy長這樣:

java:

Consumer<?> c = (o)->{
    System.out.println(o);
};

指令:

0: invokedynamic #2,  0    // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;

在最下面還有一條BootstrapMethods

0: #32 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
  #33 (Ljava/lang/Object;)V
  #34 invokestatic SimpleTest.lambda$main$0:(Ljava/lang/Object;)V
  #33 (Ljava/lang/Object;)V
  1. 從字節碼獲取調用者的類型,方法名,MethodType,以及一些其它信息 調用者的lookup是由jvm獲取的,不可人爲修改。 方法名,在這裏就是accept,在indy指令處指定 MethodType,在這裏就是()Ljava/util/function/Consumer;,它也是在indy指令處指定的。 這三項爲BootstrapMethod的前三個參數,且必須指定

  2. 調用BootstrapMethod獲取CallSite BootstrapMethod在BootstrapMethods中描述,在indy中指定。 須要規定的是調用方式(invokestatic),調用的方法,以及除了上述三個必需參數外的額外參數 在調用時,這些參數將被包裝成對象並調用指定的bootstrap方法以獲取CallSite

  3. 使用操做數棧做爲參數調用CallSite 操做數棧做爲參數的個數爲MethodType括號中參數的個數。在這裏是0個,也就是不須要參數。 調用CallSite後的返回值類型爲MethodType括號後的類型,在這裏就是Ljava/util/function/Consumer;

  4. 若是有返回值則將返回值寫入操做數棧 將獲取的Consumer放入操做數棧。

若是有興趣能夠查看LambdaMetafactory源碼並加斷點調試。可是畢竟java沒有使用帶參數的indy,因此建議在java中編譯Latte-lang代碼並執行,在lt.lang.Dynamic中下斷點。

雖然indy完成了字節碼的duck type,可是,因爲java,jvm指令自己強類型的特性,這些弱類型最終仍是要經過反射之類的方式完成調用。 舉個例子,在Latte中:

method(o)
    return o + 1

定義了一個method方法,有一個o參數。而這個參數的類型是java.lang.Object類型(由於能夠向方法內傳入任何類型的值,因此這裏只能是Object類型)。

對於返回值的表達式o+1Latte對於運算符的處理是方法綁定,+綁定了_add_方法。這裏就至關於_o.add(1)_。因爲Object類型沒有add方法,因此不能用傳統的invokevirtual之類進行調用,那麼嘗試使用indy。在使用時,MethodType應當包含o1,也就是java/lang/ObjectI。可是,即便這樣,在第二步取得CallSite時,依舊得不到具體類型,Object類依舊不存在add方法。只有等到第三步有了實際參數纔可能獲得類型。因此最終仍是須要反射才能完成調用。

#解析指令 因爲語義分析前3個步驟,整個類型的體系已經創建了起來,因此全部指令均可以用對象化的形式進行描述。

例如「返回1」,在語法、語義、字節碼分別有這樣的表示:

AST: Return(NumberLiteral(1))
Ins: TReturn(IntValue(1), I)

ByteCode:
iconst_1
ireturn

咱們在語義分析中書寫形式爲Ins這種。攜帶類型信息,包括指令信息,又有二維結構。 我定義的Ins能夠在這裏看到。

使用對象定義指令有許多好處,它不但可以與字節碼相近,還能與java相近。與字節碼相近能夠方便字節碼的生成,與java相近可讓咱們很輕鬆的構造出這些指令,減小錯誤。此外,在寫exception-table或者goto時候也能夠保證指令包括的部分是完整的語句而不會只有某些表達式。

下一篇說說一些不常規的語句(例如「內部方法」、「lambda」等)該怎麼解析~

最後,但願看官可以關注個人編譯器哦~Latte

相關文章
相關標籤/搜索