java開發編譯器:把結構體數組編譯成java字節碼

由于本節代碼邏輯有點復雜,請參看視頻用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語言編譯器 以便加深理解和掌握。

更多技術信息,包括操作系統,編譯器,面試算法,機器學習,人工智能,請關照我的公眾號:


這里寫圖片描述
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容