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