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

231 阅读4分钟

本文系 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