java开发C编译器:把struct编译成class

更详细的讲解和代码调试演示过程,请参看视频
用java开发C语言编译器

C语言是一种面向过程的语言,由于不像java那样具备面向对象的特性,所以在C语言中不存在类这样的对象,但C语言中的struct结构体跟java的类具有很多相通之处,struct本质上等价于一个没有方法只有数据,并且数据属性全是public的类。

本节我们要实现的目标是将包含struct定义的C源程序编译成java字节码,我们将会把struct编译成对应的java类,当完成本节代码后,我们的编译器能将下面C代码编译成java字节码并在jvm上正确运行:

struct CTag {
    int x;
    char c;
};

void main() {
   struct CTag myTag;
   myTag.x = 1;
   printf("value of x in myTag is %d", myTag.x);
}

我们先了解jvm用于创建和操作类对象的相关指令。当虚拟机创建一个具体类的实例之时,它需要指令new, 假设有个类,其名为ClassName,那么在虚拟机上创建一个它的实例对应的指令就是:

new ClassName

执行上面语句后,在虚拟机的堆栈顶部就会有一个对象实例,但代码还不能直接使用这个实例,该实例的使用必须要先初始化。我们知道,每个类必然都有自己的构造函数,例如下面这个类:

public ClassName {
    public ClassName(){}
    public ClassName(String name){}
}

该类有两个构造函数,一个不带参数,一个带有一个String类型的参数。在初始化一个该类的实例时,这两个构造函数中,必有一个会被调用。从代码上看,每个类的构造函数都是跟类的名字是一样的,但在虚拟机内部,所有类的构造函数名一律转换为init,所以上面类的构造函数在虚拟机内部是这样的:

<init>() V
<init>(Ljava/lang/Strin;)V

第一个init对应的是类定义里不带参数的构造函数,第二个init对应的是带String类型参数的构造函数。假设虚拟机通过new 指令在堆栈上构建了一个ClassName的实例对象,那么接下来它要调用不带输入参数的构造函数来初始化实例对象时,它会这么做:

new ClassName
dup
invokespecial ClassName/<init>() V

上面指令中, new ClassName现在堆栈顶部创建一个类的实例,执行后堆栈情况如下:
stack:
ClassName

接着dup指令的作用是,把堆栈顶部的对象复制一份后再次压入栈顶,执行这条指令后,堆栈情况如下:
stack:
ClassName
ClassName

invokespecial 是调用指定某个类实例中成员函数的指令,如果我们想调用某个类的相关接口,那么需要把该类的实例压入堆栈顶部,然后执行指令invokespecial, 该指令后面跟着的是要调用的类的接口名称,它的格式如下:

类名/接口名

因为我们要调用ClassName实例对象的无参数构造函数,根据上面原理,虚拟机就需要使用invokespecial指令.指令执行后,压入堆栈的类实例就会从堆栈顶部移除,所以调用完构造函数后,堆栈顶部就只剩下一个类的实例.
stack:
ClassName

接下来,我们看看java一个类的定义是如何在虚拟机里定义的,假设我们有一个类定义如下:

public class CTag {
    public int x;
    public char c;
    public CTag() {
        this.x = 0;
        this.c = 0;
    }
}

这个类的定义很简单,它只含有两个公开成员变量,同时有一个不带输入参数的构造函数,那么上面代码转换成java汇编代码时,情况如下:
public class CTag
这句类声明会被转换成如下代码:

.class public CTag
.super java/lang/Object

.class是java汇编语言的专有指令,它用来声明一个类,.super也是专有指令,用来表示一个类的父类,在java中,Object类是所以其他类的父类,所以上面代码转换成java汇编后会带有.super对应的语句,用来声明该类的父类。

接下来就是对类的成员变量进行声明,声明类成员变量的指令是.field 于是两个公开类型的成员变量在java汇编中会变成如下形式:

.field public c C
.field public x I

跟着就是要将构造函数转换成Java汇编了,我们前面讲解过,当某个函数被调用的时候,相关输入参数会存放到局部变量队列。当类的成员函数被调用时,有点特别,那就是类实例本身会被当做参数存放到局部变量队列的第0个位置,这其实就相当于this指针。

完成了对成员变量的声明后,接下来就是构造函数的实现,首先是构造函数的接口声明:

.method public <init>()V

了解面向对象编程原理的话,我们就知道子类在初始化自己时,必须先调用父类的构造函数,所以当初始化构造函数init执行时,必须先执行父类构造函数,代码如下:

aload   0
invokespecial   java/lang/Object/<init>()V

前面我们说过,当类的成员函数被调用时,类的实例对象会被存储在局部变量队列的第0个位置,所以指令aload 0 作用是把类的实例对象先压入栈顶,
invokespecial java/lang/Object/<init>() V
的作用就是调用父类Object类的构造函数,完成这个步骤后,代码就要将两个成员变量赋初值为0.

要想改变一个类成员变量的值,jvm需要执行三个步骤,首先是把类的实例加载到堆栈顶部,然后把要赋值的内容压入堆栈,最后使用putfield指令把数值存入类的成员变量,所以对于与代码this.c = 0; 它转换成java汇编后,代码如下:

aload 0
sipush 0
putfield CTag/c  C

同理可得,this.x = 0;这条语句对应的java汇编代码为:

aload 0
sipush 0
putfield CTag/x  I

上面代码中putfield指令最后的C和I对应的是成员变量的数据类型,x是整形,所以它对应I, c是字符,所以它对应的类型就是C.终上所述,整个构造函数的java汇编实现如下:

.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

最后,整个类对应的java汇编代码如下:

.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

到这里你可能就明白,当我们要把struct CTag转换成java字节码时,我们只要把CTag转换成对应的类,然后把它编译成上面的java汇编代码也就可以了。剩下的问题是,我们如何访问一个类的成员变量。在jvm中,访问一个类的成员变量,要分两步走,首先把类的实例压入堆栈,然后使用getfield指令将对应的类成员变量的值读入堆栈顶部。如果我们想要读取CTag.x的值,那么对应的java汇编代码如下:

aload 0  ;假设CTag实例位于具备变量队列第0个位置
putfield CTag/x  I

执行上面语句后,CTag.x的值就会存储在堆栈顶部。有了这些理论知识后,我们就可以着手实现代码的编译了。

当我们编译器在解析代码,遇到语句myTag.x 时,我们先看看myTag对应的结构体是否被编译成对应的java类,如果已经被编译过了,那么我们直接通过指令读取myTag.x的值,如果还没有被编译过,那么我们就生成对应的java类定义,由此,在ProgramGenerator.java中,添加如下代码:

public class ProgramGenerator extends CodeGenerator {
....
private ArrayList<String> structNameList = new ArrayList<String>();
    
    
    
    public void putStructToClassDeclaration(Symbol symbol) {
    
    private ArrayList<String> structNameList = new ArrayList<String>();
    public void putStructToClassDeclaration(Symbol symbol) {
        //判断传入的Symbol变量是否是结构体变量,不是的话立刻返回
        Specifier sp = symbol.getSpecifierByType(Specifier.STRUCTURE);
        if (sp == null) {
            return;
        }
        
        /*
         * 在队列structNameList中查询Symbol对应的结构体名字是否已经存储在队列中,如果在队列中有了
         * 那表明该结构体已经被转换成java类,并且类的定义已经转换成java汇编语言了
         */
        StructDefine struct = sp.getStructObj();
        if (structNameList.contains(struct.getTag())) {
            return;
        } else {
            structNameList.add(struct.getTag());
        }
        
        /*
         * 输出相应指令,把结构体转换成java类
         */
        this.emit(Instruction.NEW, struct.getTag());
        this.emit(Instruction.DUP);
        this.emit(Instruction.INVOKESPECIAL, struct.getTag()+"/"+"<init>()V");
        int idx = this.getLocalVariableIndex(symbol);
        this.emit(Instruction.ASTORE, ""+idx);
        
        //这条语句的作用是,把接下来生成的指令先缓存起来,而不是直接写入到文件里
        this.setClassDefinition(true);
        
        this.emitDirective(Directive.CLASS_PUBLIC, struct.getTag());
        this.emitDirective(Directive.SUPER, "java/lang/Object");
        
        /*
         * 把结构体中的每个成员转换成相应的具有public性质的java类成员
         */
        Symbol fields = struct.getFields();
        do {
            String fieldName = fields.getName() + " ";
            if (fields.getDeclarator(Declarator.ARRAY) != null) {
                fieldName += "[";
            }
            
            if (fields.hasType(Specifier.INT)) {
                fieldName += "I";
            } else if (fields.hasType(Specifier.CHAR)) {
                fieldName += "C";
            } else if (fields.hasType(Specifier.CHAR) && fields.getDeclarator(Declarator.POINTER) != null) {
                fieldName += "Ljava/lang/String;";
            }
            
            this.emitDirective(Directive.FIELD_PUBLIC, fieldName);
            fields = fields.getNextSymbol();
        }while (fields != null);
        
        /*
         * 实现类的初始构造函数,它调用父类的构造函数后,接下来通过putfield指令,把类的每个成员都初始化为0
         */
        this.emitDirective(Directive.METHOD_PUBLIC, "<init>()V");
        
        this.emit(Instruction.ALOAD, "0");
        String superInit = "java/lang/Object/<init>()V";
        this.emit(Instruction.INVOKESPECIAL, superInit);
        
        fields = struct.getFields();
        do {
            this.emit(Instruction.ALOAD, "0");
            String fieldName = struct.getTag() + "/" + fields.getName();
            String fieldType = "";
            if (fields.hasType(Specifier.INT)) {
                fieldType = "I";
                this.emit(Instruction.SIPUSH, "0");
            } else if (fields.hasType(Specifier.CHAR)) {
                fieldType = "C";
                this.emit(Instruction.SIPUSH, "0");
            } else if (fields.hasType(Specifier.CHAR) && fields.getDeclarator(Declarator.POINTER) != null) {
                fieldType = "Ljava/lang/String;";
                this.emit(Instruction.LDC, " ");
            }
            
            String classField = fieldName + " " + fieldType;
            this.emit(Instruction.PUTFIELD, classField);
            
            fields = fields.getNextSymbol();
        }while (fields != null);
        
        this.emit(Instruction.RETURN);
        this.emitDirective(Directive.END_METHOD);
        
        this.emitDirective(Directive.END_CLASS);
        
        this.setClassDefinition(false);
    }
....
}

上面代码的作用是把struct定义转换成java的class,并转换成前面讲解过的java类定义的汇编代码,实现的每个步骤都有相应的注释,更详细的讲解和调试请参看视频:用java开发C语言编译器

我们再看看如何实现对结构体成员变量值的修改:

public void assignValueToStructMember(Symbol structSym, Symbol field, Object val) {
        //先把类的实例压入堆栈顶部
        int idx = getLocalVariableIndex(structSym);
        this.emit(Instruction.ALOAD, ""+idx);
        
        /*
         * field是要写入的结构体成员对象,假设我们要对myTag.x 赋值,那么下面的代码把myTag.x转换为
         * CTag/x  I
         */
        String value = "";
        String fieldType = "";
        if (field.hasType(Specifier.INT)) {
            fieldType = "I";
            value += (Integer)val;
            this.emit(Instruction.SIPUSH, value);
        } else if (field.hasType(Specifier.CHAR)) {
            fieldType = "C";
            value += (Integer)val;
            this.emit(Instruction.SIPUSH, value);
        } else if (field.hasType(Specifier.CHAR) && field.getDeclarator(Declarator.POINTER) != null) {
            fieldType = "Ljava/lang/String;";
            value += (String)val;
            this.emit(Instruction.LDC, value);
        }
        
        //执行putfield指令,把要修改的值写入结构体成员变量
        Specifier sp = structSym.getSpecifierByType(Specifier.STRUCTURE);
        StructDefine struct = sp.getStructObj();
        String fieldContent = struct.getTag() + "/" + field.getName() + " " + fieldType;
        this.emit(Instruction.PUTFIELD, fieldContent);
        
    }

实现读取结构体成员变量的代码如下:

public void readValueFromStructMember(Symbol structSym, Symbol field) {
        /*
         * 先把类的实例加载到堆栈顶部
         */
        int idx = getLocalVariableIndex(structSym);
        this.emit(Instruction.ALOAD, ""+idx);
        
        /*
         * 如果我们要读取myTag.x 下面的语句会构造出
         * CTag/x  I
         */
        String fieldType = "";
        if (field.hasType(Specifier.INT)) {
            fieldType = "I";
        } else if (field.hasType(Specifier.CHAR)) {
            fieldType = "C";
        } else if (field.hasType(Specifier.CHAR) && field.getDeclarator(Declarator.POINTER) != null) {
            fieldType = "Ljava/lang/String;";
        }
        
        //通过getfield指令把结构体的成员变量读出来后压入堆栈顶部
        Specifier sp = structSym.getSpecifierByType(Specifier.STRUCTURE);
        StructDefine struct = sp.getStructObj();
        String fieldContent = struct.getTag() + "/" + field.getName() + " " + fieldType;
        this.emit(Instruction.GETFIELD, fieldContent);
    }

有了实现结构体定义,结构体成员变量的修改和读取等功能的实现后,我们只要在编译器解析到相应的地方,要执行对应操作时,调用上面代码就可以了。当编译器读取到语句 myTag.x 时,它知道此时程序的目的是想读取结构体成员变量的值,负责解析这条语句的代码是在UnaryNodeExecutor.java中:

public class UnaryNodeExecutor extends BaseExecutor implements IExecutorReceiver{
....
    public Object Execute(ICodeNode root) {
    ....
    case CGrammarInitializer.Unary_StructOP_Name_TO_Unary:
            /*
             * 当编译器读取到myTag.x 这种类型的语句时,会走入到这里
             */
            child = root.getChildren().get(0);
            String fieldName = (String)root.getAttribute(ICodeKey.TEXT);
            symbol = (Symbol)child.getAttribute(ICodeKey.SYMBOL);
            
            //先把结构体变量的作用范围设置为定义它的函数名
            symbol.addScope(ProgramGenerator.getInstance().getCurrentFuncName());
            //如果是第一次访问结构体成员变量,那么将结构体声明成一个类
            ProgramGenerator.getInstance().putStructToClassDeclaration(symbol);
            
            if (isSymbolStructPointer(symbol)) {
                copyBetweenStructAndMem(symbol, false);
            }
            
            /*
             * 假设当前解析的语句是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指令把对应的成员变量的值读取出来,然后压入堆栈顶部
             */
            if (args.getValue() != null) {
                ProgramGenerator.getInstance().readValueFromStructMember(symbol, args);
            }
        ....
    ....
    }
....
}

当代码要对结构体的成员变量赋值时,也就是要执行语句myTag.x = 1;时,编译器的代码会进入Symbol.setValue中,所以在该函数里,我们需要做相应修改如下:

public class Symbol implements IValueSetter{
....
    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 {
                generator.assignValueToStructMember(this.getStructSymbol(), this, this.value);
            }
            
        }
        
    }
....
}

上面代码完成后,将程序运行起来,前面给定的C语言代码会被编译成如下java汇编代码:

.class public CSourceToJava
.super java/lang/Object

.method public static main([Ljava/lang/String;)V
    new CTag
    dup
    invokespecial   CTag/<init>()V
    astore  0
    sipush  1
    aload   0
    sipush  1
    putfield    CTag/x I
    aload   0
    getfield    CTag/x I
    istore  1
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    ldc "value of x in myTag 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

把上面的java汇编代码编译成字节码之后运行,结果如下:


这里写图片描述

运行结果跟C语言代码的目标是一致的,也就是说,我们把带有struct结构体的C语言代码编译成java字节码是成功的。

更详细的讲解和代码调试,请参看视频:
用java开发C语言编译器

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:


这里写图片描述
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容

  • 由于本节代码逻辑有点复杂,请参看视频用java开发C语言编译器 以便加深理解和掌握 上一节,我们在C程序中引入结构...
    望月从良阅读 1,036评论 0 1
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,631评论 18 399
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,246评论 11 349
  • 一句话介绍今天我们要干什么:通过Nginx+tomcat+redis实现反向代理 、负载均衡及session同步 ...
    Bottle丶Fish阅读 886评论 0 1
  • 坚持不懈的做一件事,说起来很容易,做起来就非常困难,不仅需要毅力,还特别要具有一种精神层面的东西,彼如希望和期望。...
    尚简人生阅读 183评论 0 1