From 791cb363b77332e3abdf4039102dfcdb863ce6c3 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Mon, 2 May 2016 05:19:07 -0700 Subject: Use macro annotation to load native library This also removes the need for third projects to depend on a "loader library". --- .../main/scala/ch/jodersky/jni/annotations.scala | 102 +++++++++++++++++++++ .../ch/jodersky/jni/util/PlatformMacros.scala | 31 +++++++ 2 files changed, 133 insertions(+) create mode 100644 macros/src/main/scala/ch/jodersky/jni/annotations.scala create mode 100644 macros/src/main/scala/ch/jodersky/jni/util/PlatformMacros.scala (limited to 'macros') diff --git a/macros/src/main/scala/ch/jodersky/jni/annotations.scala b/macros/src/main/scala/ch/jodersky/jni/annotations.scala new file mode 100644 index 0000000..c7a6555 --- /dev/null +++ b/macros/src/main/scala/ch/jodersky/jni/annotations.scala @@ -0,0 +1,102 @@ +package ch.jodersky.jni + +import util.PlatformMacros + +import scala.language.experimental.macros + +import scala.reflect.macros.whitebox.Context +import scala.annotation.StaticAnnotation +import scala.annotation.compileTimeOnly + +object nativeLoaderMacro { + + def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { + import c.universe._ + + val nativeLibrary: String = c.prefix.tree match { + case Apply(_, List(Literal(Constant(x: String)))) => x + case Apply(_, xs :: tail) => c.abort(xs.pos, "Native library must be a constant.") + case t => c.abort(t.pos, "Native library not specified.") + } + + def inject(annottees: List[Tree]): List[Tree] = annottees match { + + case ClassDef(mods, name, tparams, Template(parents, self, body)) :: tail => + val extra = q""" + { + ${name.toTermName} + } + """ + + val module: List[Tree] = tail match { + case Nil => inject(List(q"""object ${name.toTermName}""")) + case other => inject(other) + } + + ClassDef(mods, name, tparams, Template(parents, self, body :+ extra)) :: module + + //q"$mods object $name extends ..$parents {$self => ..$body }" :: Nil => + case ModuleDef(mods, name, Template(parents, self, body)) :: Nil => + val loadPackagedDef: DefDef = q""" + private def loadPackaged(): Unit = { + import java.io.File + import java.nio.file.{Files, Path} + + val lib: String = System.mapLibraryName($nativeLibrary) + + val tmp: Path = Files.createTempDirectory("jni-") + val plat: String = ${PlatformMacros.current(c)} + + val resourcePath: String = "/native/" + plat + "/" + lib + val resourceStream = Option($name.getClass.getResourceAsStream(resourcePath)) match { + case Some(s) => s + case None => throw new UnsatisfiedLinkError( + "Native library " + lib + " (" + resourcePath + ") cannot be found on the classpath.") + } + + val extractedPath = tmp.resolve(lib) + + try { + Files.copy(resourceStream, extractedPath) + } catch { + case ex: Exception => throw new UnsatisfiedLinkError( + "Error while extracting native library: " + ex) + } + + System.load(extractedPath.toAbsolutePath.toString) + } + """ + val loadDef: DefDef = q""" + private def load(): Unit = try { + System.loadLibrary($nativeLibrary) + } catch { + case ex: UnsatisfiedLinkError => loadPackaged() + } + """ + + val extraBody = q""" + $loadPackagedDef + $loadDef + load() + """ + + ModuleDef(mods, name, Template(parents, self, body :+ extraBody)) :: Nil + + case _ => + c.abort(c.enclosingPosition, "nativeLoader can only be annotated to classes and singleton objects") + + } + + val outputs = inject(annottees.map(_.tree).toList) + + val result: Tree = Block(outputs, Literal(Constant(()))) + + c.Expr[Any](result) + } + +} + +@compileTimeOnly("Macro Paradise must be enabled to apply annotation.") +class nativeLoader(nativeLibrary: String) extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro nativeLoaderMacro.impl +} diff --git a/macros/src/main/scala/ch/jodersky/jni/util/PlatformMacros.scala b/macros/src/main/scala/ch/jodersky/jni/util/PlatformMacros.scala new file mode 100644 index 0000000..a4af100 --- /dev/null +++ b/macros/src/main/scala/ch/jodersky/jni/util/PlatformMacros.scala @@ -0,0 +1,31 @@ +package ch.jodersky.jni +package util + +import scala.language.experimental.macros + +import scala.reflect.macros.whitebox.Context + +object PlatformMacros { + + // arch-kernel + def current(c: Context): c.Expr[String] = { + import c.universe._ + val result = q""" + val line = try { + scala.sys.process.Process("uname -sm").lineStream.head + } catch { + case ex: Exception => sys.error("Error running `uname` command") + } + val parts = line.split(" ") + if (parts.length != 2) { + sys.error("Could not determine platform: 'uname -sm' returned unexpected string: " + line) + } else { + val arch = parts(1).toLowerCase.replaceAll("\\s", "") + val kernel = parts(0).toLowerCase.replaceAll("\\s", "") + arch + "-" + kernel + } + """ + c.Expr[String](result) + } + +} -- cgit v1.2.3