从零开始开发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

相关文章
相关标签/搜索