Skip to content

Commit

Permalink
Improve DualLoader for Scala 3
Browse files Browse the repository at this point in the history
Change the implementation of `DualLoader` to fix the `LinkageError`
exception that occurs in the Scala 3 compiler when the `xsbti.*` classes
are loaded. The problem is that those classes are loaded by two
different `ClassLoaders`: the sbt loader and the scala instance loader.

In the fixed implementation, the compiler classes are loaded by
the `DualLoader` itself, which extends `URLClassLoader`, so that the
subsequent `xsbti.*` classes can be loaded by the underlying `dual`
loader which is the sbt loader.

See https://github.com/lampepfl/dotty/blob/master/sbt-bridge/src/xsbt/CompilerClassLoader.java
for more details.
  • Loading branch information
adpi2 committed Nov 26, 2020
1 parent 2936ebb commit be9af9e
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,116 +14,42 @@ package internal
package inc
package classpath

import java.net.URL
import java.util.Enumeration
import java.util.Collections

/** A class loader that always fails to load classes and resources. */
final class NullLoader extends ClassLoader {
override final def loadClass(className: String, resolve: Boolean): Class[_] =
throw new ClassNotFoundException("No classes can be loaded from the null loader")
override def getResource(name: String): URL = null
override def getResources(name: String): Enumeration[URL] =
Collections.enumeration(Collections.emptyList())
override def toString = "NullLoader"
}

/** Exception thrown when `loaderA` and `loaderB` load a different Class for the same name. */
class DifferentLoaders(message: String, val loaderA: ClassLoader, val loaderB: ClassLoader)
extends ClassNotFoundException(message)
import java.net.{ URL, URLClassLoader }
import java.util

/**
* A ClassLoader with two parents `parentA` and `parentB`. The predicates direct lookups towards one parent or the other.
* A [[DualLoader]] is an `URLClassLoader`` that also contains a dual `ClassLoader`
* The `dual` loader preempts `super` on some class or resource lookups.
*
* If `aOnlyClasses` returns `true` for a class name, class lookup delegates to `parentA` only.
* Otherwise, if `bOnlyClasses` returns `true` for a class name, class lookup delegates to `parentB` only.
* If both `aOnlyClasses` and `bOnlyClasses` are `false` for a given class name, both class loaders must load the same Class or
* a [[DifferentLoaders]] exception is thrown.
* If `isDualClass` returns `true` for a class name, class lookup delegates to `dual`.
* Otherwise class lookup is performed by `super`.
*
* If `aOnlyResources` is `true` for a resource path, lookup delegates to `parentA` only.
* Otherwise, if `bOnlyResources` is `true` for a resource path, lookup delegates to `parentB` only.
* If neither are `true` for a resource path and either `parentA` or `parentB` return a valid URL, that valid URL is returned.
* If `isDualResource` is `true` for a resource path, lookup delegates to `dual` only.
* Otherwise resource lookup is performed by `super`.
*/
class DualLoader(
parentA: ClassLoader,
aOnlyClasses: String => Boolean,
aOnlyResources: String => Boolean,
parentB: ClassLoader,
bOnlyClasses: String => Boolean,
bOnlyResources: String => Boolean
) extends ClassLoader(new NullLoader) {
def this(
parentA: ClassLoader,
aOnly: String => Boolean,
parentB: ClassLoader,
bOnly: String => Boolean
) =
this(parentA, aOnly, aOnly, parentB, bOnly, bOnly)
override final def loadClass(className: String, resolve: Boolean): Class[_] = {
val c =
if (aOnlyClasses(className))
parentA.loadClass(className)
else if (bOnlyClasses(className))
parentB.loadClass(className)
else {
val classA = parentA.loadClass(className)
val classB = parentB.loadClass(className)
if (classA.getClassLoader eq classB.getClassLoader)
classA
else
throw new DifferentLoaders(
"Parent class loaders returned different classes for '" + className + "'",
classA.getClassLoader,
classB.getClassLoader
)
}
if (resolve)
resolveClass(c)
c
urls: Array[URL],
dual: ClassLoader,
isDualClass: String => Boolean,
isDualResource: String => Boolean
) extends URLClassLoader(urls, null) {
override def loadClass(name: String, resolve: Boolean): Class[_] = {
if (isDualClass(name)) {
val c = dual.loadClass(name)
if (resolve) resolveClass(c)
c
} else super.loadClass(name, resolve)
}

override def getResource(name: String): URL = {
if (aOnlyResources(name))
parentA.getResource(name)
else if (bOnlyResources(name))
parentB.getResource(name)
else {
val urlA = parentA.getResource(name)
val urlB = parentB.getResource(name)
if (urlA eq null)
urlB
else
urlA
}
if (isDualResource(name)) dual.getResource(name)
else super.getResource(name)
}
override def getResources(name: String): Enumeration[URL] = {
if (aOnlyResources(name))
parentA.getResources(name)
else if (bOnlyResources(name))
parentB.getResources(name)
else {
val urlsA = parentA.getResources(name)
val urlsB = parentB.getResources(name)
if (!urlsA.hasMoreElements)
urlsB
else if (!urlsB.hasMoreElements)
urlsA
else
new DualEnumeration(urlsA, urlsB)
}
}

override def toString = s"DualLoader(a = $parentA, b = $parentB)"
}

/** Concatenates `a` and `b` into a single `Enumeration`.*/
final class DualEnumeration[T](a: Enumeration[T], b: Enumeration[T]) extends Enumeration[T] {
// invariant: current.hasMoreElements or current eq b
private[this] var current = if (a.hasMoreElements) a else b
def hasMoreElements = current.hasMoreElements
def nextElement = {
val element = current.nextElement
if (!current.hasMoreElements)
current = b
element
override def getResources(name: String): util.Enumeration[URL] = {
if (isDualResource(name)) dual.getResources(name)
else super.getResources(name)
}

override def toString = s"DualLoader(urls = $urls, dual = $dual)"
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,16 @@ trait FixedResources extends ClassLoader {
}
}
}

/** Concatenates `a` and `b` into a single `Enumeration`.*/
final class DualEnumeration[T](a: Enumeration[T], b: Enumeration[T]) extends Enumeration[T] {
// invariant: current.hasMoreElements or current eq b
private[this] var current = if (a.hasMoreElements) a else b
def hasMoreElements = current.hasMoreElements
def nextElement = {
val element = current.nextElement
if (!current.hasMoreElements)
current = b
element
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,25 @@ package internal
package inc

import java.lang.reflect.InvocationTargetException
import java.nio.file.Path
import java.net.URLClassLoader
import java.nio.file.Path
import java.util.{ Optional, ServiceLoader }

import com.github.ghik.silencer.silent
import sbt.util.{ InterfaceUtil, Logger }
import sbt.io.syntax._
import sbt.internal.inc.classpath.ClassLoaderCache
import sbt.internal.inc.classpath.{ ClassLoaderCache, DualLoader }
import sbt.internal.util.ManagedLogger
import sbt.io.syntax._
import sbt.util.{ InterfaceUtil, Logger }
import xsbti.compile._
import xsbti.{
AnalysisCallback,
FileConverter,
InteractiveConsoleFactory,
Reporter,
Logger => xLogger,
VirtualFile
VirtualFile,
Logger => xLogger
}
import xsbti.compile._

import scala.language.existentials

/**
Expand Down Expand Up @@ -340,11 +341,13 @@ final class AnalyzingCompiler(

private[this] def getLoader(log: Logger): ClassLoader = {
val interfaceJar = provider.fetchCompiledBridge(scalaInstance, log)
def createInterfaceLoader =

def createInterfaceLoader = {
new URLClassLoader(
Array(interfaceJar.toURI.toURL),
createDualLoader(scalaInstance.loader(), getClass.getClassLoader)
createDualLoader(scalaInstance, getClass.getClassLoader)
)
}

classLoaderCache match {
case Some(cache) =>
Expand All @@ -359,27 +362,37 @@ final class AnalyzingCompiler(
private[this] def getBridgeClass(name: String, loader: ClassLoader) =
Class.forName(name, true, loader)

/**
* The created `DualLoader` must load the xsbti.*` classes from the `sbtLoader`
* and everything else from the `scalaInstance` jars.
*
* We cannot use the `scalaInstance.loader` direclty because the Scala 3 compiler
* needs access to the `xsbti.*` classes.
* If the same class is loaded by two different loaders (`sbtLodaer` and
* `scalaInstance.loader`) they are considered distinct by the JVM, resulting in
* `ClassCastException` or `LinkageError`.
*
* The compiler classes are directly loaded by the `DualLoader` so that the
* subsequent classes are also loaded by the `DualLoader`, ensuring consistency
* for `xsbti.*` classes.
*/
protected def createDualLoader(
scalaLoader: ClassLoader,
scalaInstance: xsbti.compile.ScalaInstance,
sbtLoader: ClassLoader
): ClassLoader = {
val xsbtiFilter = (name: String) => name.startsWith("xsbti.")
val notXsbtiFilter = (name: String) => !xsbtiFilter(name)
new classpath.DualLoader(
scalaLoader,
notXsbtiFilter,
_ => true,
new DualLoader(
scalaInstance.allJars.map(_.toURI.toURL),
sbtLoader,
xsbtiFilter,
_ => false
)
}

override def toString = s"Analyzing compiler (Scala ${scalaInstance.actualVersion})"
}

object AnalyzingCompiler {
import sbt.io.IO.{ copy, zip, unzip, withTemporaryDirectory }
import sbt.io.IO.{ copy, unzip, withTemporaryDirectory, zip }

/**
* Compile a Scala bridge from the sources of the compiler as follows:
Expand Down

0 comments on commit be9af9e

Please sign in to comment.