Skip to content

Commit

Permalink
Scoped resource management (#416)
Browse files Browse the repository at this point in the history
This should make it impossible to forget to close resources in
our codebase ever again, by construction. For example we used to
forget to close directories before which caused
`java.nio.file.FileSystemAlreadyExistsException` to be thrown.
  • Loading branch information
densh committed Nov 29, 2016
1 parent bffcec4 commit 76032ce
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ScalaNativePlugin.autoImport._
import scalanative.nir
import scalanative.tools
import scalanative.io.VirtualDirectory
import scalanative.util.{Scope => ResourceScope}

import sbt._, Keys._, complete.DefaultParsers._
import xsbti.{Maybe, Reporter, Position, Severity, Problem}
Expand Down Expand Up @@ -199,7 +200,7 @@ object ScalaNativePluginInternal {
artifactPath in nativeLink := {
(crossTarget in Compile).value / (moduleName.value + "-out")
},
nativeLink := {
nativeLink := ResourceScope { implicit in =>
val mainClass = (selectMainClass in Compile).value.getOrElse(
throw new MessageOnlyException("No main class detected.")
)
Expand Down
4 changes: 1 addition & 3 deletions tools/src/main/scala/scala/scalanative/linker/Linker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import scala.collection.mutable
import nir._
import nir.serialization._
import nir.Shows._
import util.sh
import util.Scope

sealed trait Linker {

Expand Down Expand Up @@ -85,8 +85,6 @@ object Linker {
processConditional
}

config.paths.foreach(_.close)

(unresolved.toSeq, links.toSeq, defns.sortBy(_.name.toString).toSeq)
}
}
Expand Down
6 changes: 1 addition & 5 deletions tools/src/main/scala/scala/scalanative/linker/Path.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import nir.{Global, Dep, Attr, Defn}
import nir.serialization.BinaryDeserializer
import java.nio.file.FileSystems
import scalanative.io.VirtualDirectory
import scalanative.util.Scope

sealed trait Path {

Expand All @@ -13,9 +14,6 @@ sealed trait Path {

/** Load given global and info about its dependencies. */
def load(name: Global): Option[(Seq[Dep], Seq[Attr.Link], Defn)]

/** Dispose of given nir path. */
def close(): Unit
}

object Path {
Expand Down Expand Up @@ -45,7 +43,5 @@ object Path {
entries.get(name.top).flatMap { deserializer =>
deserializer.deserialize(name)
}

def close = directory.close()
}
}
32 changes: 14 additions & 18 deletions util/src/main/scala/scala/scalanative/io/VirtualDirectory.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import java.net.URI
import java.nio.ByteBuffer
import java.nio.file._
import java.nio.channels._
import scalanative.util.{acquire, defer, Scope}

sealed trait VirtualDirectory {

Expand All @@ -30,19 +31,16 @@ sealed trait VirtualDirectory {

/** List all files in this directory. */
def files: Seq[VirtualFile]

/** Dispose of virtual directoy. */
def close(): Unit
}

object VirtualDirectory {

/** Map-backed virtual directory. */
def virtual(): VirtualDirectory =
def virtual()(implicit in: Scope): VirtualDirectory =
new MapDirectory()

/** Either real, non-virtual directory or real jar-backed virtual directory. */
def real(file: File): VirtualDirectory = {
def real(file: File)(implicit in: Scope): VirtualDirectory = {
val exists = file.exists
val isDir = file.isDirectory
val isJar = file.getAbsolutePath.endsWith(".jar")
Expand All @@ -59,12 +57,13 @@ object VirtualDirectory {
}

/** Root file system directory. */
val root: VirtualDirectory = real(new File("/"))
val root: VirtualDirectory = real(new File("/"))(Scope.forever)

/** Empty directory that contains no files. */
val empty: VirtualDirectory = EmptyDirectory

private final class MapDirectory extends VirtualDirectory {
private final class MapDirectory(implicit in: Scope)
extends VirtualDirectory {
private val entries = mutable.Map.empty[Path, VirtualFile]
private val contents = mutable.Map.empty[Path, ByteBuffer]

Expand All @@ -86,7 +85,7 @@ object VirtualDirectory {
contents(path) = cloneBuffer(buffer)
}

override def close(): Unit = {
defer {
entries.clear()
contents.clear()
}
Expand Down Expand Up @@ -114,7 +113,8 @@ object VirtualDirectory {
}
}

private final class LocalDirectory(path: Path) extends NioDirectory {
private final class LocalDirectory(path: Path)(implicit in: Scope)
extends NioDirectory {
override protected def resolve(path: Path): Path =
this.path.resolve(path)

Expand All @@ -131,14 +131,14 @@ object VirtualDirectory {
channel.close()
VirtualFile(this, path)
}

override def close(): Unit = ()
}

private final class JarDirectory(path: Path) extends NioDirectory {
private final class JarDirectory(path: Path)(implicit in: Scope)
extends NioDirectory {
private val fileSystem: FileSystem =
FileSystems.newFileSystem(URI.create(s"jar:${path.toUri}"),
Map("create" -> "false").asJava)
acquire(
FileSystems.newFileSystem(URI.create(s"jar:${path.toUri}"),
Map("create" -> "false").asJava))

override def files: Seq[VirtualFile] = {
val roots = fileSystem.getRootDirectories.asScala.toSeq
Expand All @@ -154,8 +154,6 @@ object VirtualDirectory {
override def create(path: Path): VirtualFile =
throw new UnsupportedOperationException(
"Can't create files in jar directory.")

override def close(): Unit = fileSystem.close()
}

private final object EmptyDirectory extends VirtualDirectory {
Expand All @@ -169,7 +167,5 @@ object VirtualDirectory {

override def write(path: Path, buffer: ByteBuffer): Unit =
throw new UnsupportedOperationException("Can't write to jar directory.")

override def close(): Unit = ()
}
}
59 changes: 59 additions & 0 deletions util/src/main/scala/scala/scalanative/util/Scope.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package scala.scalanative
package util

/** Scoped implicit lifetime.
*
* The main idea behind the Scope is to encode resource lifetimes through
* a concept of an implicit scope. Scopes are necessary to acquire resources.
* They are responsible for disposal of the resources once the evaluation exits
* the demarkated block in the source code.
*
* See https://www.youtube.com/watch?v=MV2eJkwarT4 for details.
*/
@annotation.implicitNotFound(msg = "Resource acquisition requires a scope.")
trait Scope {

/** Push resource onto the resource stack. */
def acquire(res: Resource): Unit

/** Clean up all the resources in FIFO order. */
def close(): Unit
}

object Scope {

/** Opens an implicit scope, evaluates the function and cleans up all the
* resources as soon as execution leaves the demercated block.
*/
def apply[T](f: Scope => T): T = {
val scope = new Impl()
try f(scope)
finally scope.close()
}

/** Scope that never closes. Resources allocated in this scope are
* going to be acquired as long as application is running.
*/
val forever: Scope = new Impl {
override def close(): Unit =
throw new UnsupportedOperationException("Can't close forever Scope.")
}

private sealed class Impl extends Scope {
private[this] var resources: List[Resource] = Nil

def acquire(res: Resource): Unit = {
resources ::= res
}

def close(): Unit = resources match {
case Nil =>
()

case first :: rest =>
resources = rest
try first.close()
finally close()
}
}
}
16 changes: 16 additions & 0 deletions util/src/main/scala/scala/scalanative/util/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,20 @@ package object util {
final case class UnsupportedException(msg: String) extends Exception(msg)
def unsupported(v: Any = "") =
throw UnsupportedException(s"$v (${v.getClass})")

/** Scope-managed resource. */
type Resource = java.lang.AutoCloseable

/** Acquire given resource in implicit scope. */
def acquire[R <: Resource](res: R)(implicit in: Scope): R = {
in.acquire(res)
res
}

/** Defer cleanup until the scope closes. */
def defer(f: => Unit)(implicit in: Scope): Unit = {
in.acquire(new Resource {
def close(): Unit = f
})
}
}

0 comments on commit 76032ce

Please sign in to comment.