aboutsummaryrefslogtreecommitdiff
path: root/sbt-bridge/src/xsbt/CompilerClassLoader.scala
blob: 071141dcfac3ff4159a5e96cb3f7d4d6f4858dd1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package xsbt

import java.net.{URL, URLClassLoader}

import scala.collection.mutable

/**  A classloader to run the compiler
 *
 *   A CompilerClassLoader is constructed from a list of `urls` that need to be on
 *   the classpath to run the compiler and the classloader used by sbt.
 *
 *   To understand why a custom classloader is needed for the compiler, let us
 *   describe some alternatives that wouldn't work.
 *   - `new URLClassLoader(urls)`:
 *     The compiler contains sbt phases that callback to sbt using the `xsbti.*`
 *     interfaces. If `urls` does not contain the sbt interfaces we'll get a
 *     `ClassNotFoundException` in the compiler when we try to use them, if
 *     `urls` does contain the interfaces we'll get a `ClassCastException` or a
 *     `LinkageError` because if the same class is loaded by two different
 *     classloaders, they are considered distinct by the JVM.
 *    - `new URLClassLoader(urls, sbtLoader)`:
 *      Because of the JVM delegation model, this means that we will only load
 *      a class from `urls` if it's not present in the parent `sbtLoader`, but
 *      sbt uses its own version of the scala compiler and scala library which
 *      is not the one we need to run the compiler.
 *
 *  Our solution is to implement a subclass of URLClassLoader with no parent, instead
 *  we override `loadClass` to load the `xsbti.*` interfaces from `sbtLoader`.
 */
class CompilerClassLoader(urls: Array[URL], sbtLoader: ClassLoader)
    extends URLClassLoader(urls, null) {
  override def loadClass(className: String, resolve: Boolean): Class[_] =
    if (className.startsWith("xsbti.")) {
      // We can't use the loadClass overload with two arguments because it's
      // protected, but we can do the same by hand (the classloader instance
      // from which we call resolveClass does not matter).
      val c = sbtLoader.loadClass(className)
      if (resolve)
        resolveClass(c)
      c
    } else {
      super.loadClass(className, resolve)
    }
}

object CompilerClassLoader {
  /** Cache the result of `fixBridgeLoader`.
   *
   *  Reusing ClassLoaders is important for warm performance since otherwise the
   *  JIT code cache for the compiler will be discarded between every call to
   *  the sbt `compile` task.
   */
  private[this] val fixedLoaderCache = new mutable.WeakHashMap[ClassLoader, ClassLoader]

  /** Fix the compiler bridge ClassLoader
   *
   *  Soundtrack: https://www.youtube.com/watch?v=imamcajBEJs
   *
   *  The classloader that we get from sbt looks like:
   *
   *    URLClassLoader(bridgeURLs,
   *      DualLoader(scalaLoader, notXsbtiFilter, sbtLoader, xsbtiFilter))
   *
   *  DualLoader will load the `xsbti.*` interfaces using `sbtLoader` and
   *  everything else with `scalaLoader`. Once we have loaded the dotty Main
   *  class using `scalaLoader`, subsequent classes in the dotty compiler will
   *  also be loaded by `scalaLoader` and _not_ by the DualLoader. But the sbt
   *  compiler phases are part of dotty and still need access to the `xsbti.*`
   *  interfaces in `sbtLoader`, therefore DualLoader does not work for us
   *  (this issue is not present with scalac because the sbt phases are
   *  currently defined in the compiler bridge itself, not in scalac).
   *
   *  CompilerClassLoader is a replacement for DualLoader. Until we can fix
   *  this in sbt proper, we need to use reflection to construct our own
   *  fixed classloader:
   *
   *    URLClassLoader(bridgeURLs,
   *      CompilerClassLoader(scalaLoader.getURLs, sbtLoader))
   *
   *  @param bridgeLoader  The classloader that sbt uses to load the compiler bridge
   *  @return A fixed classloader that works with dotty
   */
  def fixBridgeLoader(bridgeLoader: ClassLoader): ClassLoader = synchronized {
    fixedLoaderCache.getOrElseUpdate(bridgeLoader, computeFixedLoader(bridgeLoader))
  }

  private[this] def computeFixedLoader(bridgeLoader: ClassLoader) = bridgeLoader match {
    case bridgeLoader: URLClassLoader =>
      val dualLoader = bridgeLoader.getParent
      val dualLoaderClass = dualLoader.getClass

      // DualLoader#parentA and DualLoader#parentB are private
      val parentAField = dualLoaderClass.getDeclaredField("parentA")
      parentAField.setAccessible(true)
      val parentBField = dualLoaderClass.getDeclaredField("parentB")
      parentBField.setAccessible(true)
      val scalaLoader = parentAField.get(dualLoader).asInstanceOf[URLClassLoader]
      val sbtLoader = parentBField.get(dualLoader).asInstanceOf[URLClassLoader]

      val bridgeURLs = bridgeLoader.getURLs
      new URLClassLoader(bridgeURLs,
        new CompilerClassLoader(scalaLoader.getURLs, sbtLoader))
  }
}