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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
|
/* NSC -- new Scala compiler
* Copyright 2005-2012 LAMP/EPFL
* @author Martin Odersky
*/
package scala
package tools.nsc
package backend
package jvm
import scala.collection.{ mutable, immutable }
import scala.annotation.switch
import scala.tools.asm
/*
* Prepare in-memory representations of classfiles using the ASM Tree API, and serialize them to disk.
*
* Three pipelines are at work, each taking work items from a queue dedicated to that pipeline:
*
* (There's another pipeline so to speak, the one that populates queue-1 by traversing a CompilationUnit until ClassDefs are found,
* but the "interesting" pipelines are the ones described below)
*
* (1) In the first queue, an item consists of a ClassDef along with its arrival position.
* This position is needed at the time classfiles are serialized to disk,
* so as to emit classfiles in the same order CleanUp handed them over.
* As a result, two runs of the compiler on the same files produce jars that are identical on a byte basis.
* See `ant test.stability`
*
* (2) The second queue contains items where a ClassDef has been lowered into:
* (a) an optional mirror class,
* (b) a plain class, and
* (c) an optional bean class.
*
* (3) The third queue contains items ready for serialization.
* It's a priority queue that follows the original arrival order,
* so as to emit identical jars on repeated compilation of the same sources.
*
* Plain, mirror, and bean classes are built respectively by PlainClassBuilder, JMirrorBuilder, and JBeanInfoBuilder.
*
* @author Miguel Garcia, http://lamp.epfl.ch/~magarcia/ScalaCompilerCornerReloaded/
* @version 1.0
*
*/
abstract class GenBCode extends BCodeSyncAndTry {
import global._
val phaseName = "jvm"
override def newPhase(prev: Phase) = new BCodePhase(prev)
final class PlainClassBuilder(cunit: CompilationUnit) extends SyncAndTryBuilder(cunit)
class BCodePhase(prev: Phase) extends StdPhase(prev) {
override def name = phaseName
override def description = "Generate bytecode from ASTs using the ASM library"
override def erasedTypes = true
private var bytecodeWriter : BytecodeWriter = null
private var mirrorCodeGen : JMirrorBuilder = null
private var beanInfoCodeGen : JBeanInfoBuilder = null
/* ---------------- q1 ---------------- */
case class Item1(arrivalPos: Int, cd: ClassDef, cunit: CompilationUnit) {
def isPoison = { arrivalPos == Int.MaxValue }
}
private val poison1 = Item1(Int.MaxValue, null, null)
private val q1 = new java.util.LinkedList[Item1]
/* ---------------- q2 ---------------- */
case class Item2(arrivalPos: Int,
mirror: asm.tree.ClassNode,
plain: asm.tree.ClassNode,
bean: asm.tree.ClassNode,
outFolder: scala.tools.nsc.io.AbstractFile) {
def isPoison = { arrivalPos == Int.MaxValue }
}
private val poison2 = Item2(Int.MaxValue, null, null, null, null)
private val q2 = new _root_.java.util.LinkedList[Item2]
/* ---------------- q3 ---------------- */
/*
* An item of queue-3 (the last queue before serializing to disk) contains three of these
* (one for each of mirror, plain, and bean classes).
*
* @param jclassName internal name of the class
* @param jclassBytes bytecode emitted for the class SubItem3 represents
*/
case class SubItem3(
jclassName: String,
jclassBytes: Array[Byte]
)
case class Item3(arrivalPos: Int,
mirror: SubItem3,
plain: SubItem3,
bean: SubItem3,
outFolder: scala.tools.nsc.io.AbstractFile) {
def isPoison = { arrivalPos == Int.MaxValue }
}
private val i3comparator = new java.util.Comparator[Item3] {
override def compare(a: Item3, b: Item3) = {
if (a.arrivalPos < b.arrivalPos) -1
else if (a.arrivalPos == b.arrivalPos) 0
else 1
}
}
private val poison3 = Item3(Int.MaxValue, null, null, null, null)
private val q3 = new java.util.PriorityQueue[Item3](1000, i3comparator)
/*
* Pipeline that takes ClassDefs from queue-1, lowers them into an intermediate form, placing them on queue-2
*/
class Worker1(needsOutFolder: Boolean) {
val caseInsensitively = mutable.Map.empty[String, Symbol]
def run() {
while (true) {
val item = q1.poll
if (item.isPoison) {
q2 add poison2
return
}
else {
try { visit(item) }
catch {
case ex: Throwable =>
ex.printStackTrace()
error(s"Error while emitting ${item.cunit.source}\n${ex.getMessage}")
}
}
}
}
/*
* Checks for duplicate internal names case-insensitively,
* builds ASM ClassNodes for mirror, plain, and bean classes;
* enqueues them in queue-2.
*
*/
def visit(item: Item1) {
val Item1(arrivalPos, cd, cunit) = item
val claszSymbol = cd.symbol
// GenASM checks this before classfiles are emitted, https://github.com/scala/scala/commit/e4d1d930693ac75d8eb64c2c3c69f2fc22bec739
val lowercaseJavaClassName = claszSymbol.javaClassName.toLowerCase
caseInsensitively.get(lowercaseJavaClassName) match {
case None =>
caseInsensitively.put(lowercaseJavaClassName, claszSymbol)
case Some(dupClassSym) =>
reporter.warning(
claszSymbol.pos,
s"Class ${claszSymbol.javaClassName} differs only in case from ${dupClassSym.javaClassName}. " +
"Such classes will overwrite one another on case-insensitive filesystems."
)
}
// -------------- mirror class, if needed --------------
val mirrorC =
if (isStaticModule(claszSymbol) && isTopLevelModule(claszSymbol)) {
if (claszSymbol.companionClass == NoSymbol) {
mirrorCodeGen.genMirrorClass(claszSymbol, cunit)
} else {
log(s"No mirror class for module with linked class: ${claszSymbol.fullName}")
null
}
} else null
// -------------- "plain" class --------------
val pcb = new PlainClassBuilder(cunit)
pcb.genPlainClass(cd)
val outF = if (needsOutFolder) getOutFolder(claszSymbol, pcb.thisName, cunit) else null;
val plainC = pcb.cnode
// -------------- bean info class, if needed --------------
val beanC =
if (claszSymbol hasAnnotation BeanInfoAttr) {
beanInfoCodeGen.genBeanInfoClass(
claszSymbol, cunit,
fieldSymbols(claszSymbol),
methodSymbols(cd)
)
} else null
// ----------- hand over to pipeline-2
val item2 =
Item2(arrivalPos,
mirrorC, plainC, beanC,
outF)
q2 add item2 // at the very end of this method so that no Worker2 thread starts mutating before we're done.
} // end of method visit(Item1)
} // end of class BCodePhase.Worker1
/*
* Pipeline that takes ClassNodes from queue-2. The unit of work depends on the optimization level:
*
* (a) no optimization involves:
* - converting the plain ClassNode to byte array and placing it on queue-3
*/
class Worker2 {
def run() {
while (true) {
val item = q2.poll
if (item.isPoison) {
q3 add poison3
return
}
else {
try { addToQ3(item) }
catch {
case ex: Throwable =>
ex.printStackTrace()
error(s"Error while emitting ${item.plain.name}\n${ex.getMessage}")
}
}
}
}
private def addToQ3(item: Item2) {
def getByteArray(cn: asm.tree.ClassNode): Array[Byte] = {
val cw = new CClassWriter(extraProc)
cn.accept(cw)
cw.toByteArray
}
val Item2(arrivalPos, mirror, plain, bean, outFolder) = item
val mirrorC = if (mirror == null) null else SubItem3(mirror.name, getByteArray(mirror))
val plainC = SubItem3(plain.name, getByteArray(plain))
val beanC = if (bean == null) null else SubItem3(bean.name, getByteArray(bean))
if (AsmUtils.traceSerializedClassEnabled && plain.name.contains(AsmUtils.traceSerializedClassPattern)) {
if (mirrorC != null) AsmUtils.traceClass(mirrorC.jclassBytes)
AsmUtils.traceClass(plainC.jclassBytes)
if (beanC != null) AsmUtils.traceClass(beanC.jclassBytes)
}
q3 add Item3(arrivalPos, mirrorC, plainC, beanC, outFolder)
}
} // end of class BCodePhase.Worker2
var arrivalPos = 0
/*
* A run of the BCodePhase phase comprises:
*
* (a) set-up steps (most notably supporting maps in `BCodeTypes`,
* but also "the" writer where class files in byte-array form go)
*
* (b) building of ASM ClassNodes, their optimization and serialization.
*
* (c) tear down (closing the classfile-writer and clearing maps)
*
*/
override def run() {
arrivalPos = 0 // just in case
scalaPrimitives.init
initBCodeTypes()
// initBytecodeWriter invokes fullName, thus we have to run it before the typer-dependent thread is activated.
bytecodeWriter = initBytecodeWriter(cleanup.getEntryPoints)
mirrorCodeGen = new JMirrorBuilder
beanInfoCodeGen = new JBeanInfoBuilder
val needsOutfileForSymbol = bytecodeWriter.isInstanceOf[ClassBytecodeWriter]
buildAndSendToDisk(needsOutfileForSymbol)
// closing output files.
bytecodeWriter.close()
/* TODO Bytecode can be verified (now that all classfiles have been written to disk)
*
* (1) asm.util.CheckAdapter.verify()
* public static void verify(ClassReader cr, ClassLoader loader, boolean dump, PrintWriter pw)
* passing a custom ClassLoader to verify inter-dependent classes.
* Alternatively,
* - an offline-bytecode verifier could be used (e.g. Maxine brings one as separate tool).
* - -Xverify:all
*
* (2) if requested, check-java-signatures, over and beyond the syntactic checks in `getGenericSignature()`
*
*/
// clearing maps
clearBCodeTypes()
}
/*
* Sequentially:
* (a) place all ClassDefs in queue-1
* (b) dequeue one at a time from queue-1, convert it to ASM ClassNode, place in queue-2
* (c) dequeue one at a time from queue-2, convert it to byte-array, place in queue-3
* (d) serialize to disk by draining queue-3.
*/
private def buildAndSendToDisk(needsOutFolder: Boolean) {
feedPipeline1()
(new Worker1(needsOutFolder)).run()
(new Worker2).run()
drainQ3()
}
/* Feed pipeline-1: place all ClassDefs on q1, recording their arrival position. */
private def feedPipeline1() {
super.run()
q1 add poison1
}
/* Pipeline that writes classfile representations to disk. */
private def drainQ3() {
def sendToDisk(cfr: SubItem3, outFolder: scala.tools.nsc.io.AbstractFile) {
if (cfr != null){
val SubItem3(jclassName, jclassBytes) = cfr
try {
val outFile =
if (outFolder == null) null
else getFileForClassfile(outFolder, jclassName, ".class")
bytecodeWriter.writeClass(jclassName, jclassName, jclassBytes, outFile)
}
catch {
case e: FileConflictException =>
error(s"error writing $jclassName: ${e.getMessage}")
}
}
}
var moreComing = true
// `expected` denotes the arrivalPos whose Item3 should be serialized next
var expected = 0
while (moreComing) {
val incoming = q3.poll
moreComing = !incoming.isPoison
if (moreComing) {
val item = incoming
val outFolder = item.outFolder
sendToDisk(item.mirror, outFolder)
sendToDisk(item.plain, outFolder)
sendToDisk(item.bean, outFolder)
expected += 1
}
}
// we're done
assert(q1.isEmpty, s"Some ClassDefs remained in the first queue: $q1")
assert(q2.isEmpty, s"Some classfiles remained in the second queue: $q2")
assert(q3.isEmpty, s"Some classfiles weren't written to disk: $q3")
}
override def apply(cunit: CompilationUnit): Unit = {
def gen(tree: Tree) {
tree match {
case EmptyTree => ()
case PackageDef(_, stats) => stats foreach gen
case cd: ClassDef =>
q1 add Item1(arrivalPos, cd, cunit)
arrivalPos += 1
}
}
gen(cunit.body)
}
} // end of class BCodePhase
} // end of class GenBCode
|