由于本節代碼邏輯有點復雜,請參看視頻用java開發C語言編譯器 以便加深理解和掌握
上一節,我們在C程序中引入結構體,在編譯成java字節碼時,我們把結構體轉換成一個只包含公有數據成員的類,于是我們把含有結構體的C代碼成功編譯成了java字節碼,這節我們要在上一節的基礎上加大難度,把結構體變成結構體數組,看看我們的編譯器是如何把含有結構體數組的C代碼編譯成java字節碼的。完成本節代碼后,我們的編譯器能把下面C代碼編譯成java字節碼并能在虛擬機上正確運行:
struct CTag {
int x;
char c;
};
void main() {
struct CTag myTag[5];
myTag[2].x = 1;
printf("the x value of second struct object is :%d", myTag[2].x);
}
end
我們把C語言中的結構體等價于java虛擬機上的一個類,那么結構體數組自然就可以對應于java上的類數組,由此我們先看看jvm提供了那些指令讓我們操作類數組,以及這些指令的用法。
在jvm上,要想生成一個類數組,需要用到的指令是anewarray,在使用這個指令之前,我們需要在堆棧上壓入一個數值,用于表示要生成的數組長度。假定有一行java代碼如下:
String[] ss = new String[5];
要把上面的代碼轉換成虛擬機字節碼時,我們需要這么做,首先把數組的元素個數5壓入到堆棧,然后使用anewarray 指令在堆棧上創建一個String數組對象,代碼如下:
sipush 5
anewarray java/lang/String
anewarray 指令后面跟著類的類型,上面的指令在堆棧上生成了一個字符串數組對象,字符串含有5個元素,每個元素是一個指向heap上String類型實例的引用,由于我們只是生成了一個含有5個元素的數組,jvm會自動把數組中的五個元素初始化為null,上面字節碼執行后,虛擬機的堆棧情況如下:
注意到,anewarray 指令在堆棧頂部生成了一個引用,這個引用指向了一個存在heap里的一個含有5個字符串類型的數組實例,并且實例中的每個元素都指向null.
要想使用字符串數組,我們就必須使得數組中的元素都指向一個字符串實例,這就需要使用到指令aastore, 和 aaload, 例如我們想讓字符串數組的第0和第1個元素分別指向字符串"hello"和"world", 那么我們需要確保anewarray生成的字符串數組對象在堆棧頂部,接著把數組元素的下標壓入堆棧,最后再把具體字符串壓入堆棧頂部,然后執行一次aastore指令,于是要想讓ss[0]指向字符串"hello",相應的指令如下(承接上面代碼,于是字符串數組對象已經存在堆棧頂部):
astore 0
aload 0
sipush 0
ldc "hello"
aastore
由于ss[0]對象已經存在堆棧上,所以指令astore 0先把它存儲到局部變量隊列的第0個位置,然后用aload 0再次把它從局部隊列加載到堆棧頂部,這么做看似多此一舉,但這對后面的指令有作用。然后把要賦值的數組元素下標放到堆棧頂部,也就對應sipush 0, 最后把數組元素0要引用的字符串"hello"壓到堆棧頂部,因此在執行指令aastore前,堆棧情況如下:
stack: ss[0] "hello" 0
執行指令aastore后,元素ss[0]就不再是null了,它會指向字符串"hello"。指令aastore執行后,堆棧上所有元素會被清空。為了把字符串數組下標為1的元素指向字符串"world",我們需要把字符串數組對象重新加載到堆棧上,所以需要執行指令aload 0, 接著把下標1壓入堆棧,最后把字符串"world"壓入堆棧,然后再次執行aastore指令,相關代碼如下:
aload 0
sipush 0
ldc "world"
aastore
上面兩段代碼執行后,堆棧情況如下:
既然數組的元素0和1都已經指向了兩個有效的字符串,如果程序想要通過這兩個數組元素獲取他們指向的字符串,那么就需要使用指令aaload,例如要想訪問ss[0]執行的字符串"hello",那么我們需要把字符串數組對象加載到堆棧頂部,燃爆把元素下標壓入堆棧,然后執行aaload指令,代碼如下:
aload 0
sipush 0
aaload
上面代碼執行后,堆棧頂部存儲的是一個String類型的引用,這個引用指向存儲在Heap里面的字符串實例"hello"。基于這些原理,我們就可以把C語言中含有結構體數組的代碼編譯成java字節碼了,我們完全可以照貓畫虎,把上面的String對象換成結構體對象就可以了。
回到前面的C語言代碼,對照上面的原理,我們看看如何把含有結構體數組的C語言代碼編譯成字節碼。編譯器解讀代碼時,當解讀到這一句:struct CTag myTag[5];,它會創建一個Symbol對象,該對象對應的變量名稱為myTag,并記錄下,它是一個含有5個元素的數組。
當編譯器讀取語句:myTag[2].x = 1;時,它會使用anewarray指令生成一個CTag類的數組,按照前面講過的指令用法,我們的編譯器會產生如下指令:
(代碼片段1)
sipush 5
anewarray CTag
astore 0
同時編譯器此時知道,代碼想對數組中的第二個對象中的x成員賦值為1,前面講過,anewarray 指令只是生成了數組對象,數組中的每個元素會被初始化為null,此時要對第二個元素指向的對象進行賦值,那么就必須為第二個元素生成一個CTag類的實例,于是編譯器要接著生成如下指令,(代碼片段二):
aload 0
sipush 2
new CTag
dup
invokespecial CTag/<init>()V
aastore
上面指令把CTag類型數組對象加載到堆棧上后,把數組元素的下標2壓入堆棧,然后創建一個CTag實例對象,初始化后,通過aastore指令把該對象的引用存入到數組對象的第二個元素。
接著代碼需要對第二個元素指向的實例所包含的成員x賦值,于是編譯器需要生成指令,把數組第二個元素引用的實例加載到堆棧上,然后使用上一節講過的方法對類成員賦值,于是它會產生如下指令,(代碼片段3):
aload 0
sipush 2
aaload
sipush 1
putfield CTag/x I
aload 0 把數組對象加載到堆棧后,壓入要訪問的元素下標2,通過aaload指令把元素二引用的類實例加載到堆棧上,使用上節講過的指令修改類實例的成員變量。
最后編譯器解讀到語句:printf("the x value of second struct object is :%d", myTag[2].x); ,這條語句中,我們需要讀取數組第二個元素指向實例的類成員x,那么編譯器需要生成指令,把它加載到堆棧上,然后使用上一節的辦法,讀取類實例的成員變量值,于是會生成如下代碼(指令片段4):
aload 0
sipush 2
aaload
getfield CTag/x I
上面指令先把數組對象加載到堆棧上,然后把要訪問的數值元素下標2壓到堆棧,接著使用aaload指令把元素2指向的類實例加載到堆棧上,然后使用指令getfield 獲取該實例成員變量x 的值。
按照上面描述的指令生成步驟,我們要對編譯器做相應代碼修改。在解析語句myTag[2].x = 1;時,代碼會進入到UnaryNodeExecutor.java中,于是我們做如下代碼修改:
case CGrammarInitializer.Unary_LB_Expr_RB_TO_Unary:
child = root.getChildren().get(0);
symbol = (Symbol)child.getAttribute(ICodeKey.SYMBOL);
child = root.getChildren().get(1);
int index = (Integer)child.getAttribute(ICodeKey.VALUE);
try {
Declarator declarator = symbol.getDeclarator(Declarator.ARRAY);
if (declarator != null) {
Object val = declarator.getElement(index);
root.setAttribute(ICodeKey.VALUE, val);
ArrayValueSetter setter = new ArrayValueSetter(symbol, index);
root.setAttribute(ICodeKey.SYMBOL, setter);
root.setAttribute(ICodeKey.TEXT, symbol.getName());
//create array object on jvm
if (symbol.getSpecifierByType(Specifier.STRUCTURE) == null) {
//如果是結構體數組,這里不做處理,留給下一步處理
ProgramGenerator.getInstance().createArray(symbol);
ProgramGenerator.getInstance().readArrayElement(symbol, index);
}
}
....
當編譯器解析到數組讀取時會執行上面代碼,同時生成與數組讀取相關的jvm指令,這里我們先判斷讀取的數組是否是結構體,如果是,那么就不能直接生成數組讀取指令。執行完上面代碼后,又會進入下面的代碼進行執行:
case CGrammarInitializer.Unary_StructOP_Name_TO_Unary:
/*
* 當編譯器讀取到myTag.x 這種類型的語句時,會走入到這里
*/
child = root.getChildren().get(0);
String fieldName = (String)root.getAttribute(ICodeKey.TEXT);
Object object = child.getAttribute(ICodeKey.SYMBOL);
boolean isStructArray = false;
if (object instanceof ArrayValueSetter) {
//這里表示正在讀取結構體數組,先構造結構體數組
symbol = getStructSymbolFromStructArray(object);
symbol.addValueSetter(object);
isStructArray = true;
} else {
symbol = (Symbol)child.getAttribute(ICodeKey.SYMBOL);
}
....
當編譯器讀取到類似myTag.x這樣類型的代碼時,它知道此時程序正在對結構體進行處理,于是會進入到上面的代碼,由于當代碼需要讀取數組時,編譯器都會為數組的讀寫構造一個ArrayValueSetter對象,上面代碼處理的是結構體的讀寫,同時編譯器又發現傳過來的對象是一個ArrayValueSetter實例,于是編譯器就會明白,當前處理的是一個結構體數組對象,然后代碼會調用getStructSymbolFromStructArray,這個函數的作用是為結構體數組下標為2的元素生成一個結構體對象實例,并且生成前面講過的指令片段1,我們看看該函數的實現:
private Symbol getStructSymbolFromStructArray(Object object) {
ArrayValueSetter vs = (ArrayValueSetter)object;
Symbol symbol = vs.getSymbol();
int idx = vs.getIndex();
Declarator declarator = symbol.getDeclarator(Declarator.ARRAY);
if (declarator == null) {
return null;
}
ProgramGenerator.getInstance().createStructArray(symbol);
Symbol struct = null;
try {
struct = (Symbol)declarator.getElement(idx);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (struct == null) {
struct = symbol.copy();
try {
declarator.addElement(idx, (Object)struct);
//通過指令為數組中的某個下標處創建結構體實例
ProgramGenerator.getInstance().createInstanceForStructArray(symbol, idx);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return struct;
}
該函數先為myTag生成一個同類型的結構體Symbol對象,其中代碼執行了語句
ProgramGenerator.getInstance().createStructArray(symbol);
這條語句的作用是,生成前面說過的指令片段1,同時上面代碼還執行了語句:
ProgramGenerator.getInstance().createInstanceForStructArray(symbol, idx);
它的作用是生成指令片段2.這兩個函數的具體實現,在后面我們才羅列出來,代碼執行完后,回到getStructSymbolFromStructArray函數的調用處繼續往下執行:
....
//先把結構體變量的作用范圍設置為定義它的函數名
if (isStructArray == true) {
//把結構體數組symbol對象的作用域范圍設置為包含它的函數
ArrayValueSetter vs = (ArrayValueSetter)object;
Symbol structArray = vs.getSymbol();
structArray.addScope(ProgramGenerator.getInstance().getCurrentFuncName());
} else {
symbol.addScope(ProgramGenerator.getInstance().getCurrentFuncName());
}
//如果是第一次訪問結構體成員變量,那么將結構體聲明成一個類
ProgramGenerator.getInstance().putStructToClassDeclaration(symbol);
....
/*
* 假設當前解析的語句是myTag.x, 那么args對應的就是變量x
* 通過調用setStructParent 把args對應的變量x 跟包含它的結構體變量myTag
* 關聯起來
*/
Symbol args = symbol.getArgList();
while (args != null) {
if (args.getName().equals(fieldName)) {
args.setStructParent(symbol);
break;
}
args = args.getNextSymbol();
}
if (args == null) {
System.err.println("access a filed not in struct object!");
System.exit(1);
}
/*
* 把讀取結構體成員變量轉換成對應的java匯編代碼,也就是使用getfield指令把對應的成員變量的值讀取出來,然后壓入堆棧頂部
*/
//TODO 需要區分結構體是否來自于結構體數組
if (args.getValue() != null) {
ProgramGenerator.getInstance().readValueFromStructMember(symbol, args);
}
....
代碼先把變量myTag的作用域設置成包含它的函數,也就是main函數,然后調用ProgramGenerator.getInstance().putStructToClassDeclaration(symbol);
將結構體聲明成一個java的類聲明。當要讀取結構體數組第二個元素的成員變量x的時候,ProgramGenerator.getInstance().readValueFromStructMember(symbol, args);
會被執行,它的作用是生成前面講過的指令片段4.
C語言代碼中myTag[2].x = 1; 這條語句的目標是對結構體數組第二個元素的成員變量x賦值1,編譯器在解釋執行這個賦值操作時,會進入到Symbol.java,執行如下代碼:
@Override
public void setValue(Object obj) {
if (obj != null) {
System.out.println("Assign Value of " + obj.toString() + " to Variable " + name);
}
this.value = obj;
if (this.value != null) {
/*
* 先判斷該變量是否是一個結構體的成員變量,如果是,那么需要通過assignValueToStructMember來實現成員變量
* 的賦值,如果不是,那么就直接通過IStore語句直接賦值
*/
ProgramGenerator generator = ProgramGenerator.getInstance();
if (this.isStructMember() == false) {
int idx = generator.getLocalVariableIndex(this);
if (generator.isPassingArguments() == false) {
generator.emit(Instruction.ISTORE, "" + idx);
}
} else {
if (this.getStructSymbol().getValueSetter() != null) {
generator.assignValueToStructMemberFromArray(this.getStructSymbol().getValueSetter(), this, this.value);
} else {
generator.assignValueToStructMember(this.getStructSymbol(), this, this.value);
}
}
}
}
代碼先判斷,當前的賦值操作是否是針對結構體數組中的某個元素進行的,如果是,那么generator.assignValueToStructMemberFromArray(this.getStructSymbol().getValueSetter(), this, this.value);
這條語句就會執行,它的作用是生成代碼片段3.
由于篇幅原因,generator生成java指令的實現將會在視頻中再做解析,更詳細的講解和代碼調試演示過程,請參看視頻
用java開發C語言編譯器
上面代碼完成后,編譯器會把給定的C語言代碼編譯成如下java匯編語言:
.class public CSourceToJava
.super java/lang/Object
.method public static main([Ljava/lang/String;)V
sipush 2
sipush 5
anewarray CTag
astore 0
aload 0
sipush 2
new CTag
dup
invokespecial CTag/<init>()V
aastore
sipush 1
aload 0
sipush 2
aaload
sipush 1
putfield CTag/x I
sipush 2
aload 0
sipush 2
aaload
getfield CTag/x I
istore 1
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "the x value of second struct object is :"
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
getstatic java/lang/System/out Ljava/io/PrintStream;
iload 1
invokevirtual java/io/PrintStream/print(I)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "
"
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
return
.end method
.end class
.class public CTag
.super java/lang/Object
.field public c C
.field public x I
.method public <init>()V
aload 0
invokespecial java/lang/Object/<init>()V
aload 0
sipush 0
putfield CTag/c C
aload 0
sipush 0
putfield CTag/x I
return
.end method
.end class
把這些匯編語言編譯成字節碼,在虛擬機上運行后,結果如下:
通過運行結果可以得知,編譯器生成的字節碼在虛擬機上運行的結果跟C語言代碼想要的結果是完全一樣的。由于本節代碼邏輯有點復雜,請參看視頻用java開發C語言編譯器 以便加深理解和掌握。
更多技術信息,包括操作系統,編譯器,面試算法,機器學習,人工智能,請關照我的公眾號: