从Scala宏更改编译器的类路径?

时间:2013-05-27 12:03:35

标签: scala macros classpath scala-2.10

假设我们想在Scala中构建像许多脚本语言中所知的require这样的功能。

如果我们需要这样的图书馆......

require("some-library.jar")

...当执行require宏时,我们需要一种方法将这个jar添加到编译器的类路径中。怎么办呢?

2 个答案:

答案 0 :(得分:5)

另一个有趣的@ kim-stebel问题。

我的第一个想法是,您的编译器可以使用findMacroClassLoader自定义宏类路径。 REPL使用了这个感谢@extempore。

这对于像需要(“myfoo.jar”){mymacro}这样的成语很有用。

您的问题是您是否可以更新编译器的类路径。这可以通过从您的上下文Universe向下转换到编译器来实现,其中isCompilerUniverse == true。然后你可以使用platform.updateClassPath。

使用代码更新:

这就是在类路径中为所需宏使用特殊占位符的想法。

下面提到的奇特方法是使用可以报告该位置所有必需类的自定义类路径。下面提到的旧代码是针对类路径中的虚拟目录。

这种快速而俗气的方式使用文件系统来解压缩jar并只要求编译器重新扫描它。

由于invalidateClassPathEntries的限制,占位符必须是实际目录,而后者想要检查规范文件路径是否在类路径上。

package alacs

import scala.language.experimental.macros
import scala.reflect.macros.Context

import scala.sys.process._
import java.io.File

/** A require macro to dynamically fudge the compilation classpath.  */
object PathMaker {
  // special place to unpack required libs, must be on the initial classpath
  val entry = "required"

  // whether to report updated syms without overly verbose -verbose
  val talky = true

  def require(c: Context)(name: c.Expr[String]): c.Expr[Unit] = {
    import c.universe._
    val st = c.universe.asInstanceOf[scala.reflect.internal.SymbolTable]
    if (st.isCompilerUniverse) {
      val Literal(Constant(what: String)) = name.tree
      if (update(what)) {
        val global = st.asInstanceOf[scala.tools.nsc.Global]
        val (updated, _) = global invalidateClassPathEntries entry
        c.info(c.enclosingPosition, s"Updated symbols $updated", force = talky)
      } else {
        c.abort(c.enclosingPosition, s"Couldn't unpack '$what' into '$entry'")
      }
    }
    reify { () }
  }

  // figure out where name is, and update the special class path entry
  def update(name: String): Boolean = {
    // Process doesn't parse the command, it just splits on space,
    // something about working on Windows
    //val status = s"sh -c \"mkdir $entry ; ( cd $entry ; jar xf ../$name )\"".!

    // but Process can set cwd for you
    val command = s"jar xf ../$name"
    val status = Process(command, new File(entry)).!
    (status == 0)
  }
}

必需的API:

package alacs

import scala.language.experimental.macros

object Require {
  def require(name: String): Unit = macro PathMaker.require
}

用法:

package sample

import alacs.Require._

/** Sample app requiring something not on the class path. */
object Test extends App {
  require("special.jar")
  import special._
  Console println Special(7, "seven")
}

包装在special.jar中的东西

package special

case class Special(i: Int, s: String)

以这种方式测试:

rm -rf required
mkdir required
skalac pathmaker.scala
skalac -cp .:required require.scala sample.scala
skala -cp .:special.jar sample.Test

apm@mara:~/tmp/pathmaker$ . ./b
Unpack special.jar
sample.scala:8: Updated symbols List(package special)
  require("special.jar")
         ^
Special(7,seven)

宏不会将它潜入运行时路径,这是一个需要处理的脚本。

但我认为require宏可以做一些方便的事情,比如有条件地抓住不同版本的jar,具有不同的编译时特性(常量等)。

更新,只是验证它非常疯狂:

  require("fast.jar")
  import constants._
  Console println speed

  require("slow.jar")
  Console println speed

,其中

package object constants {
  //final val speed = 55
  final val speed = 85
}

$ skalac -d fast.jar constants.scala

并使用内联常量

运行它
85
55

新警告:这是我的第一个宏,我正在查看另一个应用程序的invalidateClassPathEntries,所以我还没有探讨限制。

更新:一个限制是控制宏何时展开。我想展示一些东西如何编译旧api与新api,并且必须将代码包装在块中以确保符号在需要之前可用:

require("oldfoo.jar")
locally {
  import foo._
  // something
  require("newfoo.jar")
  // try again
}

老警告: 对于模糊的回答,我知道你不遵守这个问题。我稍后会试一试,但同时也许有人会明确地向前迈进。

以前,我已经深入探讨了编译器全局的“平台”实现,但是对于这个用例来说,这有望过度。那时,你可以用类路径做任何你想做的事情,但我想你想要更开箱即用的东西。

答案 1 :(得分:0)

正如Eugene Burmanko在this question中所述,目前定义(Scala 2.10)不能在宏之外添加/删除。如果您愿意使用macro paradise,可能会有办法。