本文系 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 argumentsCtx = ctx.argument();
List arguments = getArgumentsForCall(argumentsCtx, className);
return new ConstructorCall(className, arguments);
}
方法调用要求名字,返回值,以及参数和持有者的信息。构造器的调用仅仅需要类名和参数。
- 构造器需要类型吗? 不需要。因为返回值类型都是固定的,就是类本身
- 构造器需要持有者信息吗?不需要。因为构造器的调用都是通过 new 关键字,
someObject.new SomeObject()
这种调用时没有任何意义的
对于方法声明,我们又该如何区分呢? 有一种简单的办法就是对比方法的名字和类型是否一致。这也就是意味着普通方法的命名不能跟类名重复。
@Override
public Function visitFunction(@NotNull EnkelParser.FunctionContext ctx) {
List 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, "", methodDescriptor, false);
}
你可能会好奇为什么用到了 DUP 指令。在 NEW 指令执行后,栈中保存了新建创的对象。INVOKESPECIAL 指令会从栈顶取数据,然后初始化。如果我们不赋值对象,这样会导致新创建的对象被构造器指令出栈,然后对象会丢失在堆中等待 GC 去做垃圾回收。
如下的语句:
new Cat().meow()
会生成如下的字节码:
0: new #2 // class Cat
3: dup
4: invokespecial #23 // Method "":()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, "", 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, "", "()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."":()V - call super on "this" (the Cat dervies from Object)
12: return