Skip to content

Commit

Permalink
Merge pull request #10543 from som-snytt/issue/12643-access-Unsafe
Browse files Browse the repository at this point in the history
`-Yrelease` supplements `--release`, allows access to additional JVM packages
  • Loading branch information
lrytz committed Jan 24, 2024
2 parents 1bfec6d + 8f55db5 commit 8d598d1
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 35 deletions.
7 changes: 4 additions & 3 deletions src/compiler/scala/tools/nsc/Global.scala
Expand Up @@ -146,9 +146,10 @@ class Global(var currentSettings: Settings, reporter0: Reporter)
def optimizerClassPath(base: ClassPath): ClassPath =
base match {
case AggregateClassPath(entries) if entries.head.isInstanceOf[CtSymClassPath] =>
JrtClassPath(release = None, closeableRegistry)
.map(jrt => AggregateClassPath(entries.drop(1).prepended(jrt)))
.getOrElse(base)
JrtClassPath(release = None, unsafe = None, closeableRegistry) match {
case jrt :: Nil => AggregateClassPath(entries.drop(1).prepended(jrt))
case _ => base
}
case _ => base
}

Expand Down
80 changes: 52 additions & 28 deletions src/compiler/scala/tools/nsc/classpath/DirectoryClassPath.scala
Expand Up @@ -14,14 +14,17 @@ package scala.tools.nsc.classpath

import java.io.{Closeable, File}
import java.net.{URI, URL}
import java.nio.file._

import scala.reflect.io.{AbstractFile, PlainFile, PlainNioFile}
import scala.tools.nsc.util.{ClassPath, ClassRepresentation, EfficientClassPath}
import FileUtils._
import scala.jdk.CollectionConverters._
import scala.reflect.internal.JDK9Reflectors
import scala.reflect.io.{AbstractFile, PlainFile, PlainNioFile}
import scala.tools.nsc.CloseableRegistry
import scala.tools.nsc.classpath.PackageNameUtils.{packageContains, separatePkgAndClassNames}
import scala.tools.nsc.util.{ClassPath, ClassRepresentation, EfficientClassPath}
import scala.util.Properties.{isJavaAtLeast, javaHome}
import scala.util.control.NonFatal
import FileUtils._

/**
* A trait allowing to look for classpath entries in directories. It provides common logic for
Expand Down Expand Up @@ -129,12 +132,10 @@ trait JFileDirectoryLookup[FileEntryType <: ClassRepresentation] extends Directo
}

object JrtClassPath {
import java.nio.file._, java.net.URI
private val jrtClassPathCache = new FileBasedCache[Unit, JrtClassPath]()
private val ctSymClassPathCache = new FileBasedCache[String, CtSymClassPath]()
def apply(release: Option[String], closeableRegistry: CloseableRegistry): Option[ClassPath] = {
import scala.util.Properties._
if (!isJavaAtLeast("9")) None
def apply(release: Option[String], unsafe: Option[List[String]], closeableRegistry: CloseableRegistry): List[ClassPath] =
if (!isJavaAtLeast("9")) Nil
else {
// TODO escalate errors once we're sure they are fatal
// I'm hesitant to do this immediately, because -release will still work for multi-release JARs
Expand All @@ -145,28 +146,52 @@ object JrtClassPath {

val currentMajorVersion: Int = JDK9Reflectors.runtimeVersionMajor(JDK9Reflectors.runtimeVersion()).intValue()
release match {
case Some(v) if v.toInt < currentMajorVersion =>
try {
val ctSym = Paths.get(javaHome).resolve("lib").resolve("ct.sym")
if (Files.notExists(ctSym)) None
else {
val classPath = ctSymClassPathCache.getOrCreate(v, ctSym :: Nil, () => new CtSymClassPath(ctSym, v.toInt), closeableRegistry, checkStamps = true)
Some(classPath)
}
} catch {
case _: Throwable => None
case Some(version) if version.toInt < currentMajorVersion =>
val ct = createCt(version, closeableRegistry)
unsafe match {
case Some(pkgs) if pkgs.nonEmpty =>
createJrt(closeableRegistry) match {
case Nil => ct
case jrts => ct.appended(new FilteringJrtClassPath(jrts.head, pkgs: _*))
}
case _ => ct
}
case _ =>
try {
val fs = FileSystems.getFileSystem(URI.create("jrt:/"))
val classPath = jrtClassPathCache.getOrCreate((), Nil, () => new JrtClassPath(fs), closeableRegistry, checkStamps = false)
Some(classPath)
} catch {
case _: ProviderNotFoundException | _: FileSystemNotFoundException => None
}
createJrt(closeableRegistry)
}
}
}
private def createCt(v: String, closeableRegistry: CloseableRegistry): List[ClassPath] =
try {
val ctSym = Paths.get(javaHome).resolve("lib").resolve("ct.sym")
if (Files.notExists(ctSym)) Nil
else {
val classPath = ctSymClassPathCache.getOrCreate(v, ctSym :: Nil, () => new CtSymClassPath(ctSym, v.toInt), closeableRegistry, checkStamps = true)
List(classPath)
}
} catch {
case NonFatal(_) => Nil
}
private def createJrt(closeableRegistry: CloseableRegistry): List[JrtClassPath] =
try {
val fs = FileSystems.getFileSystem(URI.create("jrt:/"))
val classPath = jrtClassPathCache.getOrCreate((), Nil, () => new JrtClassPath(fs), closeableRegistry, checkStamps = false)
List(classPath)
} catch {
case _: ProviderNotFoundException | _: FileSystemNotFoundException => Nil
}
}

final class FilteringJrtClassPath(delegate: JrtClassPath, allowed: String*) extends ClassPath with NoSourcePaths {
private val allowedPackages = allowed
private def packagePrefix(p: String, q: String) = p.startsWith(q) && (p.length == q.length || p.charAt(q.length) == '.')
private def ok(pkg: PackageName) = pkg.dottedString.isEmpty || allowedPackages.exists(packagePrefix(_, pkg.dottedString))
def asClassPathStrings: Seq[String] = delegate.asClassPathStrings
def asURLs: Seq[java.net.URL] = delegate.asURLs
private[nsc] def classes(inPackage: PackageName) = if (ok(inPackage)) delegate.classes(inPackage) else Nil
def findClassFile(className: String) = if (ok(PackageName(separatePkgAndClassNames(className)._1))) delegate.findClassFile(className) else None
private[nsc] def hasPackage(pkg: PackageName) = ok(pkg) && delegate.hasPackage(pkg)
private[nsc] def list(inPackage: PackageName) = if (ok(inPackage)) delegate.list(inPackage) else ClassPathEntries(Nil, Nil)
private[nsc] def packages(inPackage: PackageName) = if (ok(inPackage)) delegate.packages(inPackage) else Nil
}

/**
Expand All @@ -177,8 +202,7 @@ object JrtClassPath {
*
* The implementation assumes that no classes exist in the empty package.
*/
final class JrtClassPath(fs: java.nio.file.FileSystem) extends ClassPath with NoSourcePaths {
import java.nio.file.Path, java.nio.file._
final class JrtClassPath(fs: FileSystem) extends ClassPath with NoSourcePaths {
type F = Path
private val dir: Path = fs.getPath("/packages")

Expand Down Expand Up @@ -246,7 +270,7 @@ final class CtSymClassPath(ctSym: java.nio.file.Path, release: Int) extends Clas
// e.g. "java.lang" -> Seq(/876/java/lang, /87/java/lang, /8/java/lang))
private val packageIndex: scala.collection.Map[String, scala.collection.Seq[Path]] = {
val index = collection.mutable.AnyRefMap[String, collection.mutable.ListBuffer[Path]]()
val isJava12OrHigher = scala.util.Properties.isJavaAtLeast("12")
val isJava12OrHigher = isJavaAtLeast("12")
rootsForRelease.foreach(root => Files.walk(root).iterator().asScala.filter(Files.isDirectory(_)).foreach { p =>
val moduleNamePathElementCount = if (isJava12OrHigher) 1 else 0
if (p.getNameCount > root.getNameCount + moduleNamePathElementCount) {
Expand Down
1 change: 1 addition & 0 deletions src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
Expand Up @@ -264,6 +264,7 @@ trait ScalaSettings extends StandardScalaSettings with Warnings { _: MutableSett

val Youtline = BooleanSetting ("-Youtline", "Don't compile method bodies. Use together with `-Ystop-after:pickler` to generate the pickled signatures for all source files.").internalOnly()

val unsafe = MultiStringSetting("-Yrelease", "packages", "Expose platform packages hidden under --release")
val exposeEmptyPackage = BooleanSetting ("-Yexpose-empty-package", "Internal only: expose the empty package.").internalOnly()
val Ydelambdafy = ChoiceSetting ("-Ydelambdafy", "strategy", "Strategy used for translating lambdas into JVM code.", List("inline", "method"), "method")

Expand Down
7 changes: 4 additions & 3 deletions src/compiler/scala/tools/util/PathResolver.scala
Expand Up @@ -257,8 +257,9 @@ final class PathResolver(settings: Settings, closeableRegistry: CloseableRegistr

// Assemble the elements!
def basis = List[Iterable[ClassPath]](
jrt // 0. The Java 9+ classpath (backed by the ct.sym or jrt:/ virtual system, if available)
.filter(_ => !settings.javabootclasspath.isSetByUser), // respect explicit `-javabootclasspath rt.jar`
if (settings.javabootclasspath.isSetByUser) // respect explicit `-javabootclasspath rt.jar`
Nil
else jrt, // 0. The Java 9+ classpath (backed by the ct.sym or jrt:/ virtual system, if available)
classesInPath(javaBootClassPath), // 1. The Java bootstrap class path.
contentsOfDirsInPath(javaExtDirs), // 2. The Java extension class path.
classesInExpandedPath(javaUserClassPath), // 3. The Java application class path.
Expand All @@ -269,7 +270,7 @@ final class PathResolver(settings: Settings, closeableRegistry: CloseableRegistr
sourcesInPath(sourcePath) // 7. The Scala source path.
)

private def jrt: Option[ClassPath] = JrtClassPath.apply(settings.releaseValue, closeableRegistry)
private def jrt: List[ClassPath] = JrtClassPath.apply(settings.releaseValue, settings.unsafe.valueSetByUser, closeableRegistry)

lazy val containers = basis.flatten.distinct

Expand Down
4 changes: 4 additions & 0 deletions test/files/neg/unsafe.check
@@ -0,0 +1,4 @@
unsafe.scala:9: error: value threadId is not a member of Thread
def f(t: Thread) = t.threadId
^
1 error
10 changes: 10 additions & 0 deletions test/files/neg/unsafe.scala
@@ -0,0 +1,10 @@

// scalac: --release:8 -Yrelease:java.lang
// javaVersion: 19+

// -Yrelease opens packages but does not override class definitions
// because ct.sym comes first

class C {
def f(t: Thread) = t.threadId
}
21 changes: 21 additions & 0 deletions test/files/pos/unsafe.scala
@@ -0,0 +1,21 @@

// scalac: --release:8 -Yrelease:sun.misc

import sun.misc.Unsafe

class C {
val f = classOf[Unsafe].getDeclaredField("theUnsafe")
f.setAccessible(true)
val unsafe = f.get(null).asInstanceOf[Unsafe]

val k = unsafe.allocateInstance(classOf[K]).asInstanceOf[K]
assert(k.value == 0)
}

class K {
val value = 42
}

object Test extends App {
new C
}
Expand Up @@ -27,7 +27,7 @@ class JrtClassPathTest {
val elements = new ClassPathFactory(settings, closeableRegistry).classesInPath(resolver.Calculated.javaBootClassPath)
AggregateClassPath(elements)
}
else JrtClassPath(None, closeableRegistry).get
else JrtClassPath(None, None, closeableRegistry).head

assertEquals(Nil, cp.classes(""))
assertTrue(cp.packages("java").toString, cp.packages("java").exists(_.name == "java.lang"))
Expand Down

0 comments on commit 8d598d1

Please sign in to comment.