Decompiling Traits with Scala 2.11 and 2.12

Introduction

When working with Scala, one quickly gets familiar with the notation of Traits. Traits enable powerful language features such as Subtype Polymorphism and Mixins.

Scala 2.12 (currently at preview stage M4) takes advantage of Java 8 Default Methods to generate optimized JVM byte code for traits. We’ll take a short detour of how Scala 2.11 generates byte code for traits and then look at Scala 2.12 , both compiled with Java 8.

Compiling traits with Scala 2.11

Traits in Scala allow us to provide a default implementation to interface methods:

trait X {
  def helloWorld: String = "hello world"
}

Using Scala on the JVM, the compiler needs to work around the fact that Java <= v7 doesn’t allow default implementations on interfaces, which is the relative cousin of traits. Lets see what the Scala compiler does to work around that. We’ll compile trait X and then look at the generated byte code:

[root@localhost yuvie]# scalac X.scala
[root@localhost yuvie]# cd yuvie/
[root@localhost yuvie]# ll
total 12
-rw-r--r--. 1 root   root   448 Jun 24 17:58 X.class
-rw-r--r--. 1 root   root   416 Jun 24 17:58 X$class.class

Scala generated two class files for our X trait. One called X.class, and one called X$class.class. Let’s look at what each of these contain:

[root@localhost yuvie]# javap -c -p X
Warning: Binary file X contains yuvie.X
Compiled from "X.scala"
public interface yuvie.X {
  public abstract java.lang.String helloWorld();
}

[root@localhost yuvie]# javap -c -p X\$class.class 
Compiled from "X.scala"
public abstract class yuvie.X$class {
  public static java.lang.String helloWorld(yuvie.X);
    Code:
       0: ldc           #9                  // String hello world
       2: areturn

  public static void $init$(yuvie.X);
    Code:
       0: return
}

Looking at the code we can see that Scalas compiler generates:

  1. An interface called X, matching the trait declaration. This interface has a single method called helloWorld without the implementation.
  2. An abstract class called X$class with a static helloWorld method, which provides the implementation.

Lets see what happens when we extend X with some class M:

class M extends X

Bytecode:

[root@localhost yuvie]# javap -c -p M.class 
Compiled from "M.scala"
public class yuvie.M implements yuvie.X {
  public java.lang.String helloWorld();
    Code:
       0: aload_0
       1: invokestatic  #17                 // Method yuvie/X$class.helloWorld:(Lyuvie/X;)Ljava/lang/String;
       4: areturn

  public yuvie.M();
    Code:
       0: aload_0
       1: invokespecial #23                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: invokestatic  #27                 // Method yuvie/X$class.$init$:(Lyuvie/X;)V
       8: return
}

When we extend / mixin X and want to invoke helloWorld, a call to the abstract class X$class is made which provides the implementation for helloWorld. This duo of interface and abstract class allows Scala to generate valid byte code.

Leveraging Default Methods with Scala 2.12

We just saw how Scala 2.11 deals with compiling traits, let’s see how Scala 2.12 leverages default methods. Taking the same code and compiling it again only now with Scala 2.12-M4 yields the following:

[root@localhost test-project]# cd target/scala-2.12.0-M4/classes/yuvie/
[root@localhost yuvie]# ll
total 16
-rw-rw-r--. 1 yuvali yuvali  680 Jun 24 18:17 X.class

[root@localhost yuvie]# javap -c -p X.class
Compiled from "X.scala"
public interface yuvie.X {
  public java.lang.String helloWorld();
    Code:
       0: ldc           #12                 // String hello world
       2: areturn

  public void $init$();
    Code:
       0: return
}

The compiler generates a single interface called X which has the default implementation of the trait. Let’s see what happens now when we extend M with X:

[root@localhost yuvie]# javap -c -p M.class 
Compiled from "M.scala"
public class yuvie.M implements yuvie.X {
  public java.lang.String helloWorld();
    Code:
       0: aload_0
       1: invokespecial #14                 // Method yuvie/X.helloWorld:()Ljava/lang/String;
       4: areturn

  public yuvie.M();
    Code:
       0: aload_0
       1: invokespecial #20                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: invokespecial #23                 // Method yuvie/X.$init$:()V
       8: return
}

The compiler now generates an invokespecial instruction for the instance method created in X, instead of invokestatic previously for the static method created inside X$class.class.

Conclusion

Thanks to Java 8 and Default Method, Scala 2.12 is now able to emit less byte code for traits. My example was very simplified, and in real case scenarios this should save decent amount of bytecode generation for the Scala compiler and enables Scala to be more aligned with Java without needing to jump the hoops to make traits work.

Tweet
comments powered by Disqus