aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamyar <kammoh@gmail.com>2019-11-24 14:39:23 -0500
committerJakob Odersky <jakob@odersky.com>2019-11-24 14:39:23 -0500
commit221b3843a5a947d803507b8e61778ebfea072390 (patch)
tree3b2419a93d047d5ce7803a865d0c46751a1233b3
parent4a55232d23c47225dc1c12b530636595b4453ac5 (diff)
downloadsbt-jni-221b3843a5a947d803507b8e61778ebfea072390.tar.gz
sbt-jni-221b3843a5a947d803507b8e61778ebfea072390.tar.bz2
sbt-jni-221b3843a5a947d803507b8e61778ebfea072390.zip
Incorporate gjavah internally instead of deprecated javah (#33)v1.4.0
incorporate `gjavah` instead of deprecated javah
-rw-r--r--README.md5
-rw-r--r--build.sbt6
-rw-r--r--plugin/src/main/java/ch/jodersky/sbt/jni/javah/HeaderGenerator.java397
-rw-r--r--plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala27
-rw-r--r--plugin/src/sbt-test/sbt-jni/simple/project/ScriptedHelper.scala2
-rw-r--r--plugin/src/sbt-test/sbt-jni/simple/project/build.properties2
-rw-r--r--project/build.properties2
7 files changed, 419 insertions, 22 deletions
diff --git a/README.md b/README.md
index 4556ff3..79d23d2 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,10 @@ Note that most plugins are enabled in projects by default. Disabling their funct
|--------------------------------|---------------|
| automatic, for all projects | [JniJavah.scala](plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala)|
-This plugin wraps the JDK `javah` command.
+This plugin wraps the JDK `javah` command [^1].
+
+[^1]: Glavo's [gjavah](https://github.com/Glavo/gjavah) is actually used, since `javah` has been
+removed from the JDK since version 1.10.
Run `sbt-javah` to generate C header files with prototypes for any methods marked as native.
E.g. the following scala class
diff --git a/build.sbt b/build.sbt
index 4260980..7563831 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,7 +1,7 @@
import scala.sys.process._
-val scalaVersions = Seq("2.13.0", "2.12.8", "2.11.12")
-val macrosParadiseVersion = "2.1.0"
+val scalaVersions = Seq("2.13.1", "2.12.10", "2.11.12")
+val macrosParadiseVersion = "2.1.1"
// version is derived from latest git tag
version in ThisBuild := ("git describe --always --dirty=-SNAPSHOT --match v[0-9].*" !!).tail.trim
@@ -54,7 +54,7 @@ lazy val plugin = (project in file("plugin"))
.settings(
name := "sbt-jni",
publishMavenStyle := false,
- libraryDependencies += "org.ow2.asm" % "asm" % "6.0",
+ libraryDependencies += "org.ow2.asm" % "asm" % "6.2.1",
// make project settings available to source
sourceGenerators in Compile += Def.task {
val src = s"""|/* Generated by sbt */
diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/HeaderGenerator.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/HeaderGenerator.java
new file mode 100644
index 0000000..a26aa20
--- /dev/null
+++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/HeaderGenerator.java
@@ -0,0 +1,397 @@
+package ch.jodersky.sbt.jni.javah;
+
+/*
+ * Code is copied from (https://github.com/Glavo/gjavah)
+ * MIT License
+ */
+
+import org.objectweb.asm.*;
+
+import java.io.*;
+import java.nio.file.*;
+import java.util.*;
+
+public final class HeaderGenerator {
+ private static final Path[] EMPTY_PATH_ARRAY = new Path[0];
+ private static final List<String> THROWABLE_NAME_LIST = Arrays.asList("Ljava/lang/Throwable;", "Ljava/lang/Error;", "Ljava/lang/Exception");
+ private static final HeaderGenerator generator = new HeaderGenerator();
+
+ private static String escape(String source, Boolean escapeDots) {
+ StringBuilder builder = new StringBuilder();
+ char ch;
+ for (int i = 0; i < source.length(); i++) {
+ switch (ch = source.charAt(i)) {
+ case '_':
+ builder.append("_1");
+ break;
+ case ';':
+ builder.append("_2");
+ break;
+ case '[':
+ builder.append("_3");
+ break;
+ case '/':
+ if(escapeDots)
+ builder.append('_');
+ else
+ builder.append('.');
+ break;
+ default:
+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
+ builder.append(ch);
+ } else {
+ builder.append("_0").append(String.format("%04x", (int) ch));
+ }
+ }
+ }
+ return builder.toString();
+ }
+
+ public static void generateFunctionDeclarations(ClassReader reader, PrintWriter output) {
+ getGenerator().classGenerateFunctionDeclarations(reader, output);
+ }
+
+ public static void generateHeader(ClassReader reader, PrintWriter output) {
+ getGenerator().classGenerateHeader(reader, output);
+ }
+
+ public static void generateHeader(byte[] classFile, PrintWriter output) {
+ getGenerator().classGenerateHeader(classFile, output);
+ }
+
+ public static void generateHeader(byte[] classFileBuffer, int classFileOffset, int classFileLength, PrintWriter output) {
+ getGenerator().classGenerateHeader(classFileBuffer, classFileOffset, classFileLength, output);
+ }
+
+ public static void generateHeader(String className, PrintWriter output) throws IOException {
+ getGenerator().classGenerateHeader(className, output);
+ }
+
+ public static void generateHeader(InputStream input, PrintWriter output) throws IOException {
+ getGenerator().classGenerateHeader(input, output);
+ }
+
+ public static HeaderGenerator getGenerator() {
+ return generator;
+ }
+
+ private Path[] classPaths;
+ private boolean useRuntimeClassPath;
+
+ public HeaderGenerator() {
+ this(EMPTY_PATH_ARRAY, true);
+ }
+
+ public HeaderGenerator(boolean useRuntimeClassPath) {
+ this(EMPTY_PATH_ARRAY, useRuntimeClassPath);
+ }
+
+ public HeaderGenerator(Path[] classPaths) {
+ this(classPaths, true);
+ }
+
+ public HeaderGenerator(Path[] classPaths, boolean useRuntimeClassPath) {
+ Objects.requireNonNull(classPaths);
+ this.classPaths = classPaths;
+ this.useRuntimeClassPath = useRuntimeClassPath;
+ }
+
+ private boolean isThrowable(Type type) {
+ String desc = type.getDescriptor();
+ if (!desc.startsWith("L")) {
+ return false;
+ }
+ if (classPaths.length == 0 && !useRuntimeClassPath) {
+ return THROWABLE_NAME_LIST.contains(type.getDescriptor());
+ }
+ String className = type.getInternalName();
+ while (true) {
+ if (className == null) {
+ return false;
+ }
+ if (className.equals("java/lang/Throwable")) {
+ return true;
+ }
+ try {
+ ClassReader reader = findClass(className);
+ if (reader == null) {
+ return false;
+ }
+ className = reader.getSuperName();
+ } catch (Exception ignored) {
+ return false;
+ }
+ }
+ }
+
+ private String typeToNative(Type tpe) {
+ if (tpe == Type.BOOLEAN_TYPE) {
+ return "jboolean";
+ } else if (tpe == Type.BYTE_TYPE) {
+ return "jbyte";
+ } else if (tpe == Type.CHAR_TYPE) {
+ return "jchar";
+ } else if (tpe == Type.SHORT_TYPE) {
+ return "jshort";
+ } else if (tpe == Type.INT_TYPE) {
+ return "jint";
+ } else if (tpe == Type.LONG_TYPE) {
+ return "jlong";
+ } else if (tpe == Type.FLOAT_TYPE) {
+ return "jfloat";
+ } else if (tpe == Type.DOUBLE_TYPE) {
+ return "jdouble";
+ } else if (tpe == Type.VOID_TYPE) {
+ return "void";
+ } else {
+ String desc = tpe.getDescriptor();
+ if (desc.startsWith("[")) {
+ Type elemTpe = tpe.getElementType();
+ String descriptor = elemTpe.getDescriptor();
+ if (descriptor.startsWith("[") || descriptor.startsWith("L")) {
+ return "jobjectArray";
+ }
+ return typeToNative(elemTpe) + "Array";
+ }
+ if (desc.equals("Ljava/lang/String;")) {
+ return "jstring";
+ }
+ if (desc.equals("Ljava/lang/Class;")) {
+ return "jclass";
+ }
+ if (isThrowable(tpe)) {
+ return "jthrowable";
+ }
+ return "jobject";
+ }
+ }
+
+ private ClassReader findClass(String name) {
+ loop:
+ for (Path path : classPaths) {
+ path = path.toAbsolutePath();
+ if (!Files.exists(path)) {
+ continue;
+ }
+ String[] ps = name.split("/|\\.");
+ if (ps.length == 0) {
+ continue;
+ }
+ ps[ps.length - 1] += ".class";
+ for (String p : ps) {
+ path = path.resolve(p);
+ if (!Files.exists(path)) {
+ continue loop;
+ }
+ }
+ try {
+ return new ClassReader(Files.newInputStream(path));
+ } catch (IOException ignored) {
+ }
+ }
+ try {
+ return new ClassReader(name.replace('/', '.'));
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private void classGenerateFunctionDeclarations(Generator generator, PrintWriter output) {
+ String className = escape(generator.getClassName(), false);
+ String classNameNoDots = escape(generator.getClassName(), true);
+
+ for (Map.Entry<String, Set<MethodDesc>> entry : generator.getMethods().entrySet()) {
+ boolean overload = entry.getValue().size() > 1;
+ for (MethodDesc desc : entry.getValue()) {
+ String methodName = escape(entry.getKey(), false);
+ output.println("/*" + "\n" +
+ " * Class: " + className + "\n" +
+ " * Method: " + entry.getKey() + "\n" +
+ " * Signature: " + desc.descriptor + "\n" +
+ " */"
+ );
+
+ Type[] argTypes = Type.getArgumentTypes(desc.descriptor);
+ Type retType = Type.getReturnType(desc.descriptor);
+
+ output.print(
+ "JNIEXPORT " + typeToNative(retType) + " JNICALL Java_" + classNameNoDots + "_" + methodName
+ );
+
+ if (overload) {
+ output.print("__");
+ for (Type tpe : argTypes) {
+ output.print(escape(tpe.toString(), true));
+ }
+ }
+ output.println();
+
+ output.print(" (JNIEnv *, ");
+ if (desc.isStatic) {
+ output.print("jclass");
+ } else {
+ output.print("jobject");
+ }
+ for (Type tpe : argTypes) {
+ output.print(", " + typeToNative(tpe));
+ }
+ output.println(");\n");
+ }
+
+ }
+ }
+
+ public void classGenerateFunctionDeclarations(ClassReader reader, PrintWriter output) {
+ Generator generator = new Generator();
+ reader.accept(generator, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+ classGenerateFunctionDeclarations(generator, output);
+ }
+
+ private void classGenerateHeaderWithoutInclude(Generator generator, PrintWriter output) {
+ String className = escape(generator.getClassName(), true);
+
+ output.println("/* Header for class " + className + " */");
+
+ String includeHeader = "_Include_" + className;
+ output.println("#ifndef " + includeHeader);
+ output.println("#define " + includeHeader);
+
+ output.println("#ifdef __cplusplus\n" +
+ "extern \"C\" {\n" +
+ "#endif");
+
+ classGenerateFunctionDeclarations(generator, output);
+
+ output.println("#ifdef __cplusplus\n" +
+ "}\n" +
+ "#endif\n" +
+ "#endif\n");
+ }
+
+ private void classGenerateHeaderWithoutInclude(ClassReader reader, PrintWriter output) {
+ Generator generator = new Generator();
+ reader.accept(generator, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+ classGenerateHeaderWithoutInclude(generator, output);
+ }
+
+ private void classGenerateHeader(Generator generator, PrintWriter output) {
+ output.println("/* DO NOT EDIT THIS FILE - it is machine generated */");
+ output.println("#include <jni.h>");
+
+ classGenerateHeaderWithoutInclude(generator, output);
+ }
+
+ public void classGenerateHeader(ClassReader reader, PrintWriter output) {
+ Generator generator = new Generator();
+ reader.accept(generator, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+ classGenerateHeader(generator, output);
+ }
+
+ public void classGenerateHeader(byte[] classFile, PrintWriter output) {
+ classGenerateHeader(new ClassReader(classFile), output);
+ }
+
+ public void classGenerateHeader(byte[] classFileBuffer, int classFileOffset, int classFileLength, PrintWriter output) {
+ classGenerateHeader(new ClassReader(classFileBuffer, classFileOffset, classFileLength), output);
+ }
+
+ public void classGenerateHeader(String className, PrintWriter output) throws IOException {
+ ClassReader reader = findClass(className);
+ if (reader == null) {
+ throw new IOException();
+ }
+ classGenerateHeader(reader, output);
+ }
+
+ public void classGenerateHeader(InputStream input, PrintWriter output) throws IOException {
+ classGenerateHeader(new ClassReader(input), output);
+ }
+
+ public Path[] getClassPaths() {
+ return classPaths;
+ }
+
+ public void setClassPaths(Path[] classPaths) {
+ Objects.requireNonNull(classPaths);
+ this.classPaths = classPaths;
+ }
+
+ public boolean isUseRuntimeClassPath() {
+ return useRuntimeClassPath;
+ }
+
+ public void setUseRuntimeClassPath(boolean useRuntimeClassPath) {
+ this.useRuntimeClassPath = useRuntimeClassPath;
+ }
+
+ public static void run(ArrayList<String> classNames, Path outputDir , ArrayList<Path> cps) throws IOException {
+ HeaderGenerator generator = new HeaderGenerator(cps.toArray(new Path[0]));
+ if (Files.notExists(outputDir)) {
+ Files.createDirectories(outputDir);
+ }
+ for (String name : classNames) {
+ ClassReader reader = generator.findClass(name);
+ if (reader == null) {
+ System.err.println("Error: Could not find class file for '" + name + "'.");
+ return;
+ }
+ Generator g = new Generator();
+ reader.accept(g, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+
+ try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(
+ outputDir.resolve(name.replace('.', '_').replace("$", "__") + ".h"),
+ StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {
+ generator.classGenerateHeader(g, writer);
+ }
+ }
+ }
+
+}
+
+final class MethodDesc {
+ public final boolean isStatic;
+ public final String descriptor;
+
+ public MethodDesc(boolean isStatic, String descriptor) {
+ this.isStatic = isStatic;
+ this.descriptor = descriptor;
+ }
+}
+
+final class Generator extends ClassVisitor {
+ private String className;
+
+ private Map<String, String> constants = new LinkedHashMap<>();
+ private Map<String, Set<MethodDesc>> methods = new LinkedHashMap<String, Set<MethodDesc>>();
+
+ public Generator() {
+ super(Opcodes.ASM5);
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
+ className = name;
+ }
+
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+ if ((access & Opcodes.ACC_NATIVE) != 0) {
+ if (methods.containsKey(name)) {
+ methods.get(name).add(new MethodDesc((access & Opcodes.ACC_STATIC) != 0, descriptor));
+ } else {
+ LinkedHashSet<MethodDesc> set = new LinkedHashSet<>();
+ set.add(new MethodDesc((access & Opcodes.ACC_STATIC) != 0, descriptor));
+ methods.put(name, set);
+ }
+ }
+ return null;
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public Map<String, Set<MethodDesc>> getMethods() {
+ return methods;
+ }
+}
diff --git a/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala b/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala
index e313112..a9832a5 100644
--- a/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala
+++ b/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala
@@ -1,11 +1,14 @@
package ch.jodersky.sbt.jni
package plugins
+import java.nio.file.{Path, Paths}
+
import collection.JavaConverters._
import util.BytecodeUtil
+import java.util
+
import sbt._
import sbt.Keys._
-import sys.process._
/** Adds `javah` header-generation functionality to projects. */
object JniJavah extends AutoPlugin {
@@ -51,25 +54,19 @@ object JniJavah extends AutoPlugin {
(compile in Compile).value; Seq((classDirectory in Compile).value)
}
- val cp = jcp.mkString(sys.props("path.separator"))
val log = streams.value.log
val classes = (javahClasses in javah).value
- if (!classes.isEmpty) {
+ if (classes.nonEmpty) {
log.info("Headers will be generated to " + out.getAbsolutePath)
}
- for (clazz <- classes) {
- log.info("Generating header for " + clazz)
- val parts = Seq(
- "javah",
- "-d", out.getAbsolutePath,
- "-classpath", cp,
- clazz
- )
- val cmd = parts.mkString(" ")
- val ev = Process(cmd) ! log
- if (ev != 0) sys.error(s"Error occured running javah. Exit code: ${ev}")
- }
+
+ import scala.collection.JavaConverters._
+
+ ch.jodersky.sbt.jni.javah.HeaderGenerator.run(new util.ArrayList[String](classes.asJava),
+ Paths.get(out.getAbsolutePath), new util.ArrayList[Path](jcp.map(_.toPath).asJava)
+ )
+
out
}
)
diff --git a/plugin/src/sbt-test/sbt-jni/simple/project/ScriptedHelper.scala b/plugin/src/sbt-test/sbt-jni/simple/project/ScriptedHelper.scala
index d5d1d6e..4fc2c8a 100644
--- a/plugin/src/sbt-test/sbt-jni/simple/project/ScriptedHelper.scala
+++ b/plugin/src/sbt-test/sbt-jni/simple/project/ScriptedHelper.scala
@@ -8,7 +8,7 @@ object ScriptedHelper extends AutoPlugin {
override def projectSettings = Seq(
scalacOptions ++= Seq("-feature", "-deprecation"),
- crossScalaVersions := Seq("2.13.0", "2.12.8", "2.11.12"),
+ crossScalaVersions := Seq("2.13.1", "2.12.10", "2.11.12"),
scalaVersion := crossScalaVersions.value.head
)
diff --git a/plugin/src/sbt-test/sbt-jni/simple/project/build.properties b/plugin/src/sbt-test/sbt-jni/simple/project/build.properties
index c0bab04..6adcdc7 100644
--- a/plugin/src/sbt-test/sbt-jni/simple/project/build.properties
+++ b/plugin/src/sbt-test/sbt-jni/simple/project/build.properties
@@ -1 +1 @@
-sbt.version=1.2.8
+sbt.version=1.3.3
diff --git a/project/build.properties b/project/build.properties
index c0bab04..6adcdc7 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.2.8
+sbt.version=1.3.3