1
+ package xsbt ;
2
+
3
+ import java .lang .reflect .Field ;
4
+
5
+ import java .net .URL ;
6
+ import java .net .URLClassLoader ;
7
+
8
+ import java .util .WeakHashMap ;
9
+
10
+ /**
11
+ * A classloader to run the compiler
12
+ * <p>
13
+ * A CompilerClassLoader is constructed from a list of `urls` that need to be on
14
+ * the classpath to run the compiler and the classloader used by sbt.
15
+ * <p>
16
+ * To understand why a custom classloader is needed for the compiler, let us
17
+ * describe some alternatives that wouldn't work.
18
+ * <ul>
19
+ * <li>`new URLClassLoader(urls)`:
20
+ * The compiler contains sbt phases that callback to sbt using the `xsbti.*`
21
+ * interfaces. If `urls` does not contain the sbt interfaces we'll get a
22
+ * `ClassNotFoundException` in the compiler when we try to use them, if
23
+ * `urls` does contain the interfaces we'll get a `ClassCastException` or a
24
+ * `LinkageError` because if the same class is loaded by two different
25
+ * classloaders, they are considered distinct by the JVM.
26
+ * <li>`new URLClassLoader(urls, sbtLoader)`:
27
+ * Because of the JVM delegation model, this means that we will only load
28
+ * a class from `urls` if it's not present in the parent `sbtLoader`, but
29
+ * sbt uses its own version of the scala compiler and scala library which
30
+ * is not the one we need to run the compiler.
31
+ * </ul>
32
+ * <p>
33
+ * Our solution is to implement a subclass of URLClassLoader with no parent, instead
34
+ * we override `loadClass` to load the `xsbti.*` interfaces from `sbtLoader`.
35
+ */
36
+ public class CompilerClassLoader extends URLClassLoader {
37
+ private final ClassLoader sbtLoader ;
38
+
39
+ public CompilerClassLoader (URL [] urls , ClassLoader sbtLoader ) {
40
+ super (urls , null );
41
+ this .sbtLoader = sbtLoader ;
42
+ }
43
+
44
+ @ Override
45
+ public Class <?> loadClass (String className , boolean resolve ) throws ClassNotFoundException {
46
+ if (className .startsWith ("xsbti." )) {
47
+ // We can't use the loadClass overload with two arguments because it's
48
+ // protected, but we can do the same by hand (the classloader instance
49
+ // from which we call resolveClass does not matter).
50
+ Class <?> c = sbtLoader .loadClass (className );
51
+ if (resolve )
52
+ resolveClass (c );
53
+ return c ;
54
+ } else {
55
+ return super .loadClass (className , resolve );
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Cache the result of `fixBridgeLoader`.
61
+ * <p>
62
+ * Reusing ClassLoaders is important for warm performance since otherwise the
63
+ * JIT code cache for the compiler will be discarded between every call to
64
+ * the sbt `compile` task.
65
+ */
66
+ private static WeakHashMap <ClassLoader , ClassLoader > fixedLoaderCache = new WeakHashMap <>();
67
+
68
+ /**
69
+ * Fix the compiler bridge ClassLoader
70
+ * <p>
71
+ * Soundtrack: https://www.youtube.com/watch?v=imamcajBEJs
72
+ * <p>
73
+ * The classloader that we get from sbt looks like:
74
+ * <p>
75
+ * URLClassLoader(bridgeURLs,
76
+ * DualLoader(scalaLoader, notXsbtiFilter, sbtLoader, xsbtiFilter))
77
+ * <p>
78
+ * DualLoader will load the `xsbti.*` interfaces using `sbtLoader` and
79
+ * everything else with `scalaLoader`. Once we have loaded the dotty Main
80
+ * class using `scalaLoader`, subsequent classes in the dotty compiler will
81
+ * also be loaded by `scalaLoader` and _not_ by the DualLoader. But the sbt
82
+ * compiler phases are part of dotty and still need access to the `xsbti.*`
83
+ * interfaces in `sbtLoader`, therefore DualLoader does not work for us
84
+ * (this issue is not present with scalac because the sbt phases are
85
+ * currently defined in the compiler bridge itself, not in scalac).
86
+ * <p>
87
+ * CompilerClassLoader is a replacement for DualLoader. Until we can fix
88
+ * this in sbt proper, we need to use reflection to construct our own
89
+ * fixed classloader:
90
+ * <p>
91
+ * URLClassLoader(bridgeURLs,
92
+ * CompilerClassLoader(scalaLoader.getURLs, sbtLoader))
93
+ *
94
+ * @param bridgeLoader The classloader that sbt uses to load the compiler bridge
95
+ * @return A fixed classloader that works with dotty
96
+ */
97
+ synchronized public static ClassLoader fixBridgeLoader (ClassLoader bridgeLoader ) {
98
+ return fixedLoaderCache .computeIfAbsent (bridgeLoader , k -> computeFixedLoader (k ));
99
+ }
100
+
101
+ private static ClassLoader computeFixedLoader (ClassLoader bridgeLoader ) {
102
+ URLClassLoader urlBridgeLoader = (URLClassLoader ) bridgeLoader ;
103
+ ClassLoader dualLoader = urlBridgeLoader .getParent ();
104
+ Class <?> dualLoaderClass = dualLoader .getClass ();
105
+
106
+ try {
107
+ // DualLoader.parentA and DualLoader.parentB are private
108
+ Field parentAField = dualLoaderClass .getDeclaredField ("parentA" );
109
+ parentAField .setAccessible (true );
110
+ Field parentBField = dualLoaderClass .getDeclaredField ("parentB" );
111
+ parentBField .setAccessible (true );
112
+ URLClassLoader scalaLoader = (URLClassLoader ) parentAField .get (dualLoader );
113
+ ClassLoader sbtLoader = (ClassLoader ) parentBField .get (dualLoader );
114
+
115
+ URL [] bridgeURLs = urlBridgeLoader .getURLs ();
116
+ return new URLClassLoader (bridgeURLs ,
117
+ new CompilerClassLoader (scalaLoader .getURLs (), sbtLoader ));
118
+ } catch (NoSuchFieldException | IllegalAccessException e ) {
119
+ throw new RuntimeException (e );
120
+ }
121
+ }
122
+ }
0 commit comments