从零开始开发JVM语言(十三)代码生成与ASM
2019獨角獸企業重金招聘Python工程師標準>>>
目錄戳這里
如果能夠做完語義分析,得到帶類型的AST,或者更接近于虛擬機字節碼的結構,那么你離整個編譯器的“落成”就不遠了!
在這個步驟,你可以直接操作byte數組,也可以使用第三方的中間結構,也可以使用字節碼類庫。
當然,最直接的當然是字節碼庫了,例如大名鼎鼎的ASM。
#ASM ASM庫很小,但是功能非常完善。所有的屬性都有一套封裝來支持,棧的大小,StackMapTable的計算做的都很好。當然也有不好的地方,如果你給出的指令有問題它也不會報錯,有時候計算StackMapTable時會拋出IndexOutOfBoundsException,讓人摸不著頭腦。而且,mvn中下載的是壓縮版本,報錯不帶行數,也沒有源碼參考。用maven之類的進行管理時還可能出現依賴沖突。
所以正確的做法是像Spring一樣,對它做一個repackaging,其實就是保留版權信息,然后把源代碼直接往項目里放,包名調整一下。比如我這么做。
字節碼的生成本質上幾個循環就可以搞定了。 對類型需要類型名稱,修飾符,父類,接口,注解。 各字段,需要名稱,descriptor,注解。 構造方法(<init>),普通方法,static塊方法(<clinit>),它們需要名稱,descriptor,注解。相比較語法語義分析來說這一步實在是太簡單了。
這里介紹一下ASM庫的組成吧。
ASM庫主要由各個Visitor構成。而Visitor都是abstract的類型,我們實際需要用到的是Writer。
- 類 ClassWriter
- 注解 AnnotationWriter
- 字段 FieldWriter
- 方法 MethodWriter
其中,只有ClassWriter是需要手動構造的,其他幾個都是可以通過方法調用來獲取的。
new ClassWriter(ClassWriter.COMPUTE_FRAMES)建議參數中的COMPUTE_MAXS不要加上。因為棧的彈出 我們需要在字節碼生成步驟手動完成(當然,這取決于你的語義分析輸出,我的輸出是基本不帶POP的,因為POP可以很自然的由當前棧深度分析出來,只有明確需要POP的地方才在語義分析中加上)。
比方說這樣一條語句(Integer.valueOf(1)),它將返回一個Integer類型的值。但是這個值沒有被變量或者字段接收,所以需要將其pop掉。這時可以分析出當前棧深度為1,需要pop一次。于是在此加入一個pop,并將當前棧深度減1。
實際上,要考慮的不僅僅是棧深度,還要考慮棧占用一個位置還是兩個位置。比如double和long就會占用兩個棧的位置,pop時不能用pop指令,而需要使用pop2。比如這里的實現,定義了一個結構來指定占用多少的棧深度,并合理的pop出去。
由于有了這個自動pop的機制,棧最大深度也可以順便做出來,不必使用自動計算的最大深度了。
#生成器 在字節碼生成中,我定義了這樣幾個工具方法,來使代碼邏輯更明確
- int acc(List<SModifier> modifiers) 用來獲取Modifier
- String typeToDesc(STypeDef type) 用來獲取類型的descriptor
- String typeToInternalName(STypeDef type) 用來獲取internal name
- String methodDesc(STypeDef returnType, List<STypeDef> parameters) 用來獲取方法的descriptor
這些東西經常需要獲取,所以單獨拎出來實現一下。
對于字段、方法、注解等結構自然也是各管各的分開實現:
- void buildStatic(ClassWriter classWriter, List<Instruction> staticIns, List<ExceptionTable> exceptionTable) static塊
- void buildConstructor(ClassWriter classWriter, List<SConstructorDef> constructors) 構造函數
- void buildField(ClassWriter classWriter, List<SFieldDef> fields) 字段
- void buildMethod(ClassWriter classWriter, List<SMethodDef> methods) 方法
- void buildParameter(MethodVisitor methodVisitor, List<SParameter> params) 參數
- void buildAnnotation(AnnotationVisitor annotationVisitor, SAnno anno) 注解
其中buildAnnotation經常被調用,因為不管是類,字段還是方法,都可能會有注解,甚至注解中還可以包含注解。所以上述這幾個方法內部都有buildAnnotation的調用。
#指令 我實現的語義分析輸出的指令與字節碼非常接近,所以對每一個指令加一個if分支,并調用對應的方法構造指令即可。
ASM將指令依據調用所需操作數(不是棧內操作數)進行了分類。
- void visitInsn(int opcode) 不帶任何操作數的指令
- void visitIntInsn(int opcode, int operand) 只帶一個整數作為操作數的指令
- void visitVarInsn(int opcode, int var) 與局部變量相關的指令
- void visitTypeInsn(int opcode, String type) 接收一個internal name的指令
- void visitFieldInsn(int opcode, String owner, String name, String desc) 接收一個字段的指令。字段由類型/名稱/descriptor表示
- void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) 接收一個方法的指令。方法由類型/名稱/descriptor/是否為接口方法 表示
- void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) InvokeDynamic特有指令
- void visitJumpInsn(int opcode, Label label) 跳轉指令
- void visitLdcInsn(Object cst) 取常量池指令。這里的參數會自動加到常量池中去。
到此,整個編譯器主體已經完成(因為是JVM語言,所以編譯目標到字節碼即可)。然而,接下來要做的工作還有很多,Evaluator,REPL,編譯器交互,語法高亮,ide支持等。不過至少最困難的部分終于結束了!
下一篇說說Evaluator和REPL怎么實現~
最后,希望看官能夠關注我的編譯器哦~Latte
轉載于:https://my.oschina.net/wkgcass/blog/704503
總結
以上是生活随笔為你收集整理的从零开始开发JVM语言(十三)代码生成与ASM的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 虚拟化基础架构Windows 2008篇
- 下一篇: Telent 远程登录服务