手拉手教你实现一门编程语言 Enkel, 系列 15

本文系 Creating JVM language 翻译的第 15 篇。
原文中的代码和原文有不一致的地方均在新的代码仓库中更正过,建议参考新的代码仓库。

源码

Github

语法

Enkel 的构造器声明和调用的语法和 Java 保持一致。
声明实例:

Cat ( String name ) {
}

调用实例:

new Cat ( "Molly" ) 

语法规则更改

Java 中构造器的声明是一个没有返回值的函数。Enkel 中也是一样。

对于构造器的调用呢?解析器如何区别方法调用和构造器调用呢?因此,Enkel 引入了关键字 new:

//other rules
expression : //other rules alternatives
           | 'new' className '('argument? (',' argument)* ')' #constructorCall

匹配 Antlr 上下文对象

新的语法规则 constructCall 带来一个新的解析回调:

@Override
public Expression visitConstructorCall(@NotNull EnkelParser.ConstructorCallContext ctx) {
    String className = ctx.className().getText();
    List<EnkelParser.ArgumentContext> argumentsCtx = ctx.argument();
    List<Expression> arguments = getArgumentsForCall(argumentsCtx, className);
    return new ConstructorCall(className, arguments);
}

方法调用要求名字,返回值,以及参数和持有者的信息。构造器的调用仅仅需要类名和参数。

  • 构造器需要类型吗? 不需要。因为返回值类型都是固定的,就是类本身
  • 构造器需要持有者信息吗?不需要。因为构造器的调用都是通过 new 关键字,someObject.new SomeObject() 这种调用时没有任何意义的

对于方法声明,我们又该如何区分呢? 有一种简单的办法就是对比方法的名字和类型是否一致。这也就是意味着普通方法的命名不能跟类名重复。

@Override
    public Function visitFunction(@NotNull EnkelParser.FunctionContext ctx) {
        List<Type> parameterTypes = ctx.functionDeclaration().functionParameter().stream()
                .map(p -> TypeResolver.getFromTypeName(p.type())).collect(toList());
        FunctionSignature signature = scope.getMethodCallSignature(ctx.functionDeclaration().functionName().getText(),parameterTypes);
        scope.addLocalVariable(new LocalVariable("this",scope.getClassType()));
        addParametersAsLocalVariables(signature);
        Statement block = getBlock(ctx);
        //Check if method is not actually a constructor
        if(signature.getName().equals(scope.getClassName())) {
            return new Constructor(signature,block);
        }
        return new Function(signature, block);
    }

默认的构造器

如果你没有手动创建构造器,Enkel 会创建默认的构造器:

@Override
public ClassDeclaration visitClassDeclaration(@NotNull EnkelParser.ClassDeclarationContext ctx) {
    //some other stuff
    boolean defaultConstructorExists = scope.parameterLessSignatureExists(className);
    addDefaultConstructorSignatureToScope(name, defaultConstructorExists);
    //other stuff
    if(!defaultConstructorExists) {
        methods.add(getDefaultConstructor());
    }
}
        
private void addDefaultConstructorSignatureToScope(String name, boolean defaultConstructorExists) {
    if(!defaultConstructorExists) {
        FunctionSignature constructorSignature = new FunctionSignature(name, Collections.emptyList(), BultInType.VOID);
        scope.addSignature(constructorSignature);
    }
}

private Constructor getDefaultConstructor() {
    FunctionSignature signature = scope.getMethodCallSignatureWithoutParameters(scope.getClassName());
    Constructor constructor = new Constructor(signature, Block.empty(scope));
    return constructor;
}

你或许好奇为何构造器返回 void。简单来说就是 JVM 把对象的创建分为两个步骤:首先分配内存空间,然后才是调用构造器(构造器主要职责是做初始化,因此我们可以在构造函数内调用 this 变量)。

生成字节码

到目前为止,我们已经可以解析构造函数的声明以及调用了。接下来就是如何生成字节码了。

对象的创建的字节码有两个指令:

  • NEW 在堆中分类内存,初始化成员变量为默认值
  • INVOKESPECIAL 调用构造器

Java 中你无需在构造器中手动调用 super() 。实际上这是必须的,否则无法创建对象,但是 Java 编译器帮我们做了这一步。

调用 super 会用到 INVOKESPECIAL 指令,Enkel 编译器跟 Java 编译器保持一致,也会自动处理调用。

构造器调用的字节码生成

public void generate(ConstructorCall constructorCall) {
        String ownerDescriptor = scope.getClassInternalName(); //example : java/lang/String
        methodVisitor.visitTypeInsn(Opcodes.NEW, ownerDescriptor); //NEW instruction takes object decriptor as an input
        methodVisitor.visitInsn(Opcodes.DUP); //Duplicate (we do not want invokespecial to "eat" our brand new object
        FunctionSignature methodCallSignature = scope.getMethodCallSignature(constructorCall.getIdentifier(),constructorCall.getArguments());
        String methodDescriptor = DescriptorFactory.getMethodDescriptor(methodCallSignature);
        generateArguments(constructorCall);
        methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, ownerDescriptor, "<init>", methodDescriptor, false);
    }

你可能会好奇为什么用到了 DUP 指令。在 NEW 指令执行后,栈中保存了新建创的对象。INVOKESPECIAL 指令会从栈顶取数据,然后初始化。如果我们不赋值对象,这样会导致新创建的对象被构造器指令出栈,然后对象会丢失在堆中等待 GC 去做垃圾回收。

如下的语句:
new Cat().meow()

会生成如下的字节码:

0: new           #2                  // class Cat
3: dup
4: invokespecial #23                 // Method "<init>":()V
7: invokevirtual #26                 // Method meow:()V

构造器声明的字节码生成

public void generate(Constructor constructor) {
    Block block = (Block) constructor.getRootStatement();
    Scope scope = block.getScope();
    int access = Opcodes.ACC_PUBLIC;
    String description = DescriptorFactory.getMethodDescriptor(constructor);
    MethodVisitor mv = classWriter.visitMethod(access, "<init>", description, null, null);
    mv.visitCode();
    StatementGenerator statementScopeGenrator = new StatementGenerator(mv,scope);
    new SuperCall().accept(statementScopeGenrator); //CALL SUPER IMPLICITILY BEFORE BODY ITSELF
    block.accept(statementScopeGenrator); //CALL THE BODY DEFINED BY PROGRAMMER
    appendReturnIfNotExists(constructor, block,statementScopeGenrator);
    mv.visitMaxs(-1,-1);
    mv.visitEnd();
}

前面我们提到,构造器中的 super 调用时必须的,Java 中我们没有手动调用(除非父类没有无参构造器)。这样做不是非必须的而是 Java 编译器帮我们做了自动生成。Enkel 也要有这么炫酷的功能。

new SuperCall().accept(statementScopeGenrator);

触发:

public void generate(SuperCall superCall) {
    methodVisitor.visitVarInsn(Opcodes.ALOAD,0); //LOAD "this" object
    generateArguments(superCall);
    String ownerDescriptor = scope.getSuperClassInternalName();
    methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, ownerDescriptor, "<init>", "()V" , false);
}

每个方法(甚至是构造器)把参数当做帧中的局部变量来对待。如果方法 int add(int x,int y) 在静态上下文中被调用,他的初始 frame 中存在两个变量(x, y)。如果在非静态上下文中,this(被调用者)也存在局部变量中。因此,如果 add 方法是在非静态上下文中被调用,那么有三个局部变量(this, x, y)。

Cat 类的构造器(构造器内没有内容)生成的字节码如下:

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

推荐阅读更多精彩内容