Skip to content

Commit

Permalink
SD-194 Tweak module initialization to comply with JVM spec
Browse files Browse the repository at this point in the history
Top level modules in Scala currently desugar as:

```
class C; object O extends C { toString }
```

```
public final class O$ extends C {
  public static final O$ MODULE$;

  public static {};
    Code:
       0: new           #2                  // class O$
       3: invokespecial scala#12                 // Method "<init>":()V
       6: return

  private O$();
    Code:
       0: aload_0
       1: invokespecial scala#13                 // Method C."<init>":()V
       4: aload_0
       5: putstatic     scala#15                 // Field MODULE$:LO$;
       8: aload_0
       9: invokevirtual scala#21                 // Method java/lang/Object.toString:()Ljava/lang/String;
      12: pop
      13: return
}
```

The static initalizer `<clinit>` calls the constructor `<init>`, which
invokes superclass constructor, assigns `MODULE$= this`, and then runs
the remainder of the object's constructor (`toString` in the example
above.)

It turns out that this relies on a bug in the JVM's verifier: assignment to a
static final must occur lexically within the <clinit>, not from within `<init>`
(even if the latter is happens to be called by the former).

I'd like to move the assignment to <clinit> but that would
change behaviour of "benign" cyclic references between modules.

Example:

```
package p1; class CC { def foo = O.bar}; object O {new CC().foo; def bar = println(1)};

// Exiting paste mode, now interpreting.

scala> p1.O
1
```

This relies on the way that we assign MODULE$ field after the super class constructors
are finished, but before the rest of the module constructor is called.

Instead, this commit removes the ACC_FINAL bit from the field. It actually wasn't
behaving as final at all, precisely the issue that the stricter verifier
now alerts us to.

```
scala> :paste -raw
// Entering paste mode (ctrl-D to finish)

package p1; object O

// Exiting paste mode, now interpreting.

scala> val O1 = p1.O
O1: p1.O.type = p1.O$@ee7d9f1

scala> scala.reflect.ensureAccessible(p1.O.getClass.getDeclaredConstructor()).newInstance()
res0: p1.O.type = p1.O$@64cee07

scala> O1 eq p1.O
res1: Boolean = false
```

We will still achieve safe publication of the assignment to other threads
by virtue of the fact that `<clinit>` is executed within the scope of
an initlization lock, as specified by:

  https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5

Fixes scala/scala-dev#SD-194
  • Loading branch information
retronym committed Aug 18, 2016
1 parent 804133f commit bb882e7
Show file tree
Hide file tree
Showing 2 changed files with 18 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,15 @@ abstract class BCodeSkelBuilder extends BCodeHelpers {
* can-multi-thread
*/
private def addModuleInstanceField() {
// TODO confirm whether we really don't want ACC_SYNTHETIC nor ACC_DEPRECATED
// SD-194 This can't be FINAL on JVM 1.9+ because we assign it from within the
// instance constructor, not from <clinit> directly. Assignment from <clinit>,
// after the constructor has completely finished, seems like the principled
// thing to do, but it would change behaviour when "benign" cyclic references
// between modules exist.
val mods = GenBCode.PublicStatic
val fv =
cnode.visitField(GenBCode.PublicStaticFinal, // TODO confirm whether we really don't want ACC_SYNTHETIC nor ACC_DEPRECATED
cnode.visitField(mods,
strMODULE_INSTANCE_FIELD,
thisBType.descriptor,
null, // no java-generic-signature
Expand Down
Loading

0 comments on commit bb882e7

Please sign in to comment.